1
1
mirror of https://github.com/danbee/persephone synced 2025-03-04 08:39:11 +00:00

Add album view

This commit is contained in:
Daniel Barber 2019-02-10 14:52:15 -05:00
parent 3cbb24f50d
commit 447432120a
Signed by: danbarber
GPG Key ID: 931D8112E0103DD8
15 changed files with 347 additions and 27 deletions

View File

@ -15,6 +15,9 @@
E408D3B6220DD8970006D9BE /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = E408D3B5220DD8970006D9BE /* Notification.swift */; };
E408D3B9220DE98F0006D9BE /* NSUserInterfaceItemIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E408D3B8220DE98F0006D9BE /* NSUserInterfaceItemIdentifier.swift */; };
E408D3BE220E03EE0006D9BE /* RawRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E408D3BD220E03EE0006D9BE /* RawRepresentable.swift */; };
E408D3C2220E134F0006D9BE /* AlbumViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E408D3C1220E134F0006D9BE /* AlbumViewController.swift */; };
E408D3CA220E341D0006D9BE /* AlbumItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E408D3C8220E341D0006D9BE /* AlbumItem.swift */; };
E408D3CB220E341D0006D9BE /* AlbumItem.xib in Resources */ = {isa = PBXBuildFile; fileRef = E408D3C9220E341D0006D9BE /* AlbumItem.xib */; };
E41B22C021FB6BBA00D544F6 /* libmpdclient.2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = E41B22BF21FB6BBA00D544F6 /* libmpdclient.2.dylib */; settings = {ATTRIBUTES = (Required, ); }; };
E41B22C121FB6C3300D544F6 /* libmpdclient.2.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = E41B22BF21FB6BBA00D544F6 /* libmpdclient.2.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
E41B22C621FB932700D544F6 /* MPDClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = E41B22C521FB932700D544F6 /* MPDClient.swift */; };
@ -24,6 +27,8 @@
E4E8CC922204F4B80024217A /* QueueViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4E8CC912204F4B80024217A /* QueueViewController.swift */; };
E4E8CC942206097F0024217A /* NotificationsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4E8CC932206097F0024217A /* NotificationsController.swift */; };
E4E8CC9A22075D370024217A /* Song.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4E8CC9922075D370024217A /* Song.swift */; };
E4EB2379220F10B8008C70C0 /* Pair.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4EB2378220F10B8008C70C0 /* Pair.swift */; };
E4EB237B220F7CF1008C70C0 /* Album.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4EB237A220F7CF1008C70C0 /* Album.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -73,6 +78,9 @@
E408D3B5220DD8970006D9BE /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = "<group>"; };
E408D3B8220DE98F0006D9BE /* NSUserInterfaceItemIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSUserInterfaceItemIdentifier.swift; sourceTree = "<group>"; };
E408D3BD220E03EE0006D9BE /* RawRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RawRepresentable.swift; sourceTree = "<group>"; };
E408D3C1220E134F0006D9BE /* AlbumViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumViewController.swift; sourceTree = "<group>"; };
E408D3C8220E341D0006D9BE /* AlbumItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumItem.swift; sourceTree = "<group>"; };
E408D3C9220E341D0006D9BE /* AlbumItem.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AlbumItem.xib; sourceTree = "<group>"; };
E41B22BF21FB6BBA00D544F6 /* libmpdclient.2.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libmpdclient.2.dylib; path = libmpdclient/output/libmpdclient.2.dylib; sourceTree = "<group>"; };
E41B22C421FB715A00D544F6 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = "<group>"; };
E41B22C521FB932700D544F6 /* MPDClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPDClient.swift; sourceTree = "<group>"; };
@ -117,6 +125,8 @@
E4E8CC912204F4B80024217A /* QueueViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueViewController.swift; sourceTree = "<group>"; };
E4E8CC932206097F0024217A /* NotificationsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsController.swift; sourceTree = "<group>"; };
E4E8CC9922075D370024217A /* Song.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Song.swift; sourceTree = "<group>"; };
E4EB2378220F10B8008C70C0 /* Pair.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pair.swift; sourceTree = "<group>"; };
E4EB237A220F7CF1008C70C0 /* Album.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Album.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -172,6 +182,7 @@
E408D3B7220DE8CC0006D9BE /* Extensions */,
E4D1B598220BA3C90026F233 /* Resources */,
E4D1B597220BA3A20026F233 /* Controllers */,
E408D3C3220E138B0006D9BE /* Views */,
E41B22C721FB966C00D544F6 /* include */,
E407861B2110CE6E006887B1 /* AppDelegate.swift */,
E40786242110CE70006887B1 /* Info.plist */,
@ -216,6 +227,16 @@
path = Extensions;
sourceTree = "<group>";
};
E408D3C3220E138B0006D9BE /* Views */ = {
isa = PBXGroup;
children = (
E40786212110CE70006887B1 /* Main.storyboard */,
E408D3C8220E341D0006D9BE /* AlbumItem.swift */,
E408D3C9220E341D0006D9BE /* AlbumItem.xib */,
);
path = Views;
sourceTree = "<group>";
};
E41B22BE21FB6B3300D544F6 /* Frameworks */ = {
isa = PBXGroup;
children = (
@ -291,6 +312,8 @@
children = (
E4E8CC9922075D370024217A /* Song.swift */,
E4A642D922090CBE00067D21 /* Status.swift */,
E4EB2378220F10B8008C70C0 /* Pair.swift */,
E4EB237A220F7CF1008C70C0 /* Album.swift */,
);
path = Models;
sourceTree = "<group>";
@ -306,6 +329,7 @@
E4D1B597220BA3A20026F233 /* Controllers */ = {
isa = PBXGroup;
children = (
E408D3C1220E134F0006D9BE /* AlbumViewController.swift */,
E4E8CC932206097F0024217A /* NotificationsController.swift */,
E4E8CC912204F4B80024217A /* QueueViewController.swift */,
E465049921E94DF500A70F4C /* WindowController.swift */,
@ -317,7 +341,6 @@
isa = PBXGroup;
children = (
E407861F2110CE70006887B1 /* Assets.xcassets */,
E40786212110CE70006887B1 /* Main.storyboard */,
);
path = Resources;
sourceTree = "<group>";
@ -433,6 +456,7 @@
buildActionMask = 2147483647;
files = (
E40786202110CE70006887B1 /* Assets.xcassets in Resources */,
E408D3CB220E341D0006D9BE /* AlbumItem.xib in Resources */,
E40786232110CE70006887B1 /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -458,17 +482,21 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
E408D3C2220E134F0006D9BE /* AlbumViewController.swift in Sources */,
E4A642DA22090CBE00067D21 /* Status.swift in Sources */,
E4E8CC942206097F0024217A /* NotificationsController.swift in Sources */,
E408D3B6220DD8970006D9BE /* Notification.swift in Sources */,
E408D3B9220DE98F0006D9BE /* NSUserInterfaceItemIdentifier.swift in Sources */,
E4EB2379220F10B8008C70C0 /* Pair.swift in Sources */,
E465049A21E94DF500A70F4C /* WindowController.swift in Sources */,
E41B22C621FB932700D544F6 /* MPDClient.swift in Sources */,
E407861C2110CE6E006887B1 /* AppDelegate.swift in Sources */,
E4E8CC9A22075D370024217A /* Song.swift in Sources */,
E408D3CA220E341D0006D9BE /* AlbumItem.swift in Sources */,
E4E8CC902204EC7F0024217A /* Delegate.swift in Sources */,
E408D3BE220E03EE0006D9BE /* RawRepresentable.swift in Sources */,
E4E8CC922204F4B80024217A /* QueueViewController.swift in Sources */,
E4EB237B220F7CF1008C70C0 /* Album.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@ -0,0 +1,56 @@
//
// AlbumViewController.swift
// Persephone
//
// Created by Daniel Barber on 2019/2/08.
// Copyright © 2019 Dan Barber. All rights reserved.
//
import Cocoa
class AlbumViewController: NSViewController,
NSCollectionViewDataSource,
NSCollectionViewDelegate {
var albums: [MPDClient.Album] = []
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(
self,
selector: #selector(updateAlbums(_:)),
name: Notification.loadedAlbums,
object: AppDelegate.mpdClient
)
}
@objc func updateAlbums(_ notification: Notification) {
guard let albums = notification.userInfo?[Notification.albumsKey] as? [MPDClient.Album]
else { return }
print("Loaded albums")
self.albums = albums
albumCollectionView.reloadData()
}
func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int {
return albums.count
}
func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem {
let item = collectionView.makeItem(
withIdentifier: NSUserInterfaceItemIdentifier("AlbumItem"),
for: indexPath
)
guard let albumItem = item as? AlbumItem else { return item }
albumItem.view.wantsLayer = true
albumItem.setAlbumTitle(albums[indexPath.item].title)
albumItem.setAlbumArtist(albums[indexPath.item].artist)
return albumItem
}
@IBOutlet var albumCollectionView: NSCollectionView!
}

View File

@ -32,6 +32,13 @@ class NotificationsController: MPDClientDelegate {
)
}
func didLoadAlbums(mpdClient: MPDClient, albums: [MPDClient.Album]) {
sendNotification(
name: Notification.loadedAlbums,
userInfo: [Notification.albumsKey: albums]
)
}
private func sendNotification(name: Notification.Name, userInfo: [AnyHashable : Any]) {
self.notificationQueue.async {
NotificationCenter.default.post(

View File

@ -12,8 +12,10 @@ extension Notification {
static let stateChanged = Notification.Name("MPDClientStateChanged")
static let queueChanged = Notification.Name("MPDClientQueueChanged")
static let queuePosChanged = Notification.Name("MPDClientQueuePosChanged")
static let loadedAlbums = Notification.Name("MPDClientLoadedAlbums")
static let stateKey = "state"
static let queueKey = "queue"
static let queuePosKey = "song"
static let albumsKey = "albums"
}

View File

@ -22,7 +22,8 @@ class MPDClient {
private let commandQueue = DispatchQueue(label: "commandQueue")
enum Command {
case prevTrack, nextTrack, playPause, stop, fetchStatus, fetchQueue
case prevTrack, nextTrack, playPause, stop, fetchStatus, fetchQueue,
fetchAllAlbums
}
struct Idle: OptionSet {
@ -57,6 +58,8 @@ class MPDClient {
fetchQueue()
fetchAllAlbums()
self.delegate?.didUpdateState(mpdClient: self, state: self.status!.state)
self.delegate?.didUpdateQueue(mpdClient: self, queue: self.queue)
self.delegate?.didUpdateQueuePos(mpdClient: self, song: self.status!.song)
@ -78,6 +81,10 @@ class MPDClient {
sendCommand(command: .fetchQueue)
}
func fetchAllAlbums() {
sendCommand(command: .fetchAllAlbums)
}
func playPause() {
queueCommand(command: .playPause)
}
@ -103,6 +110,7 @@ class MPDClient {
}
func sendCommand(command: Command) {
print("Command:", command)
switch command {
// Transport commands
@ -114,19 +122,12 @@ class MPDClient {
sendStop()
case .playPause:
sendPlay()
case .fetchStatus:
guard let status = mpd_run_status(connection) else { break }
self.status = Status(status)
sendRunStatus()
case .fetchQueue:
self.queue = []
mpd_send_list_queue_meta(connection)
while let mpdSong = mpd_recv_song(connection) {
let song = Song(mpdSong)
self.queue.append(song)
}
sendFetchQueue()
case .fetchAllAlbums:
allAlbums()
}
}
@ -158,6 +159,84 @@ class MPDClient {
}
}
func sendRunStatus() {
guard let status = mpd_run_status(connection) else { return }
self.status = Status(status)
}
func sendFetchQueue() {
self.queue = []
mpd_send_list_queue_meta(connection)
while let mpdSong = mpd_recv_song(connection) {
let song = Song(mpdSong)
self.queue.append(song)
}
}
func allAlbums() {
var albums: [Album] = []
var artist: String = ""
mpd_search_db_tags(self.connection, MPD_TAG_ALBUM)
mpd_search_add_group_tag(self.connection, MPD_TAG_ALBUM_ARTIST)
mpd_search_commit(self.connection)
while let mpdPair = mpd_recv_pair(self.connection) {
let name = String(cString: mpdPair.pointee.name)
let value = String(cString: mpdPair.pointee.value)
switch name {
case "AlbumArtist":
artist = value
case "Album":
albums.append(Album(title: value, artist: artist))
default:
break
}
mpd_return_pair(self.connection, mpdPair)
}
delegate?.didLoadAlbums(mpdClient: self, albums: albums)
}
func albumsForArtist(_ artist: String) -> [Album] {
var albums: [Album] = []
mpd_search_db_tags(self.connection, MPD_TAG_ALBUM)
mpd_search_add_tag_constraint(self.connection, MPD_OPERATOR_DEFAULT, MPD_TAG_ARTIST, artist)
mpd_search_commit(self.connection)
while let mpdAlbumPair = mpd_recv_pair_tag(self.connection, MPD_TAG_ALBUM) {
print(artist, "-", String(cString: mpdAlbumPair.pointee.value))
albums.append(Album(title: String(cString: mpdAlbumPair.pointee.value), artist: artist))
mpd_return_pair(self.connection, mpdAlbumPair)
}
return albums
}
func allArtists() -> [String] {
var artists: [String] = []
mpd_search_db_tags(self.connection, MPD_TAG_ARTIST)
mpd_search_commit(self.connection)
while let mpdArtistPair = mpd_recv_pair_tag(self.connection, MPD_TAG_ARTIST) {
artists.append(String(cString: mpdArtistPair.pointee.value))
mpd_return_pair(self.connection, mpdArtistPair)
}
return artists
}
func sendSearchDbTags(_ tagType: mpd_tag_type) {
mpd_search_db_tags(self.connection, tagType)
mpd_search_commit(self.connection)
while let mpdPair = mpd_recv_pair_tag(self.connection, tagType) {
print(String(cString: mpdPair.pointee.value))
mpd_return_pair(self.connection, mpdPair)
}
}
func noIdle() {
mpd_send_noidle(connection)
}
@ -184,6 +263,7 @@ class MPDClient {
self.delegate?.didUpdateQueuePos(mpdClient: self, song: self.status!.song)
}
if !mpdIdle.isEmpty {
print("Status")
self.idle()
}
}

View File

@ -0,0 +1,21 @@
//
// Album.swift
// Persephone
//
// Created by Daniel Barber on 2019/2/09.
// Copyright © 2019 Dan Barber. All rights reserved.
//
import Foundation
extension MPDClient {
class Album {
let title: String
let artist: String
init(title: String, artist: String) {
self.title = title
self.artist = artist
}
}
}

View File

@ -0,0 +1,18 @@
//
// Pair.swift
// Persephone
//
// Created by Daniel Barber on 2019/2/09.
// Copyright © 2019 Dan Barber. All rights reserved.
//
import Foundation
import mpdclient
class Pair {
let mpdPair: UnsafeMutablePointer<mpd_pair>
init(_ mpdPair: UnsafeMutablePointer<mpd_pair>) {
self.mpdPair = mpdPair
}
}

View File

@ -12,4 +12,5 @@ protocol MPDClientDelegate {
func didUpdateState(mpdClient: MPDClient, state: MPDClient.Status.State)
func didUpdateQueue(mpdClient: MPDClient, queue: [MPDClient.Song])
func didUpdateQueuePos(mpdClient: MPDClient, song: Int)
func didLoadAlbums(mpdClient: MPDClient, albums: [MPDClient.Album])
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "blankAlbum.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,27 @@
//
// AlbumItem.swift
// Persephone
//
// Created by Daniel Barber on 2019/2/08.
// Copyright © 2019 Dan Barber. All rights reserved.
//
import Cocoa
class AlbumItem: NSCollectionViewItem {
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
}
func setAlbumTitle(_ title: String) {
albumTitle.stringValue = title
}
func setAlbumArtist(_ artist: String) {
albumArtist.stringValue = artist
}
@IBOutlet var albumTitle: NSTextField!
@IBOutlet var albumArtist: NSTextField!
}

View File

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14460.31"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="AlbumItem" customModule="Persephone" customModuleProvider="target">
<connections>
<outlet property="albumArtist" destination="5Uu-j1-qyT" id="2Et-tX-InT"/>
<outlet property="albumTitle" destination="KEh-NL-c2W" id="SI3-hm-H2B"/>
<outlet property="view" destination="Hz6-mo-xeY" id="v7W-XA-Emc"/>
</connections>
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customView id="Hz6-mo-xeY">
<rect key="frame" x="0.0" y="0.0" width="206" height="248"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews>
<textField identifier="albumTitle" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="KEh-NL-c2W">
<rect key="frame" x="-2" y="20" width="37" height="17"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="Label" id="pDs-0t-e1j">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField identifier="albumArtist" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="5Uu-j1-qyT">
<rect key="frame" x="-2" y="0.0" width="37" height="17"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="Label" id="yZn-e9-zyP">
<font key="font" metaFont="system"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<imageView identifier="albumArtwork" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="Kfb-8f-ean">
<rect key="frame" x="0.0" y="42" width="206" height="206"/>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyUpOrDown" image="blankAlbum" id="FsA-JX-BFh"/>
</imageView>
</subviews>
<constraints>
<constraint firstItem="KEh-NL-c2W" firstAttribute="leading" secondItem="5Uu-j1-qyT" secondAttribute="leading" id="Iji-NN-ju3"/>
<constraint firstItem="Kfb-8f-ean" firstAttribute="leading" secondItem="Hz6-mo-xeY" secondAttribute="leading" id="JMi-4i-dgs"/>
<constraint firstItem="KEh-NL-c2W" firstAttribute="top" secondItem="Kfb-8f-ean" secondAttribute="bottom" constant="5" id="Jdb-sY-lnB"/>
<constraint firstAttribute="trailing" secondItem="Kfb-8f-ean" secondAttribute="trailing" id="KQC-Wz-Bsg"/>
<constraint firstItem="Kfb-8f-ean" firstAttribute="top" secondItem="Hz6-mo-xeY" secondAttribute="top" id="Qbk-jx-zAi"/>
<constraint firstItem="Kfb-8f-ean" firstAttribute="leading" secondItem="KEh-NL-c2W" secondAttribute="leading" id="diY-bi-0Yg"/>
<constraint firstAttribute="bottom" secondItem="5Uu-j1-qyT" secondAttribute="bottom" id="puG-ym-5hh"/>
<constraint firstAttribute="bottom" secondItem="KEh-NL-c2W" secondAttribute="bottom" constant="20" symbolic="YES" id="yIJ-L1-2c5"/>
</constraints>
<point key="canvasLocation" x="3" y="142"/>
</customView>
<collectionViewItem id="Qgu-aI-55A" customClass="AlbumItem" customModule="Persephone" customModuleProvider="target"/>
</objects>
<resources>
<image name="blankAlbum" width="128" height="128"/>
</resources>
</document>

View File

@ -908,47 +908,55 @@
</objects>
<point key="canvasLocation" x="744" y="459"/>
</scene>
<!--View Controller-->
<!--Album View Controller-->
<scene sceneID="7Ua-Hj-zWt">
<objects>
<viewController id="gPn-fP-LFc" sceneMemberID="viewController">
<viewController id="gPn-fP-LFc" customClass="AlbumViewController" customModule="Persephone" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" id="v1d-6d-Fl3">
<rect key="frame" x="0.0" y="0.0" width="450" height="300"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<scrollView wantsLayer="YES" borderType="none" autohidesScrollers="YES" horizontalLineScroll="10" horizontalPageScroll="10" verticalLineScroll="10" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="wia-Qk-FEw">
<scrollView wantsLayer="YES" borderType="none" autohidesScrollers="YES" horizontalLineScroll="10" horizontalPageScroll="10" verticalLineScroll="10" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="i5f-35-7x8">
<rect key="frame" x="0.0" y="0.0" width="450" height="300"/>
<clipView key="contentView" id="leP-Jn-VMY">
<clipView key="contentView" id="iXE-Xl-zu6">
<rect key="frame" x="0.0" y="0.0" width="450" height="300"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<collectionView id="2XB-gk-sPO">
<collectionView id="lfq-AB-epE">
<rect key="frame" x="0.0" y="0.0" width="450" height="158"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES"/>
<collectionViewFlowLayout key="collectionViewLayout" minimumInteritemSpacing="10" minimumLineSpacing="10" id="OKj-Q2-LS1">
<size key="itemSize" width="50" height="50"/>
<collectionViewFlowLayout key="collectionViewLayout" minimumInteritemSpacing="20" minimumLineSpacing="20" id="K0l-P8-OW0">
<size key="itemSize" width="206" height="248"/>
<edgeInsets key="sectionInset" left="40" right="40" top="20" bottom="60"/>
</collectionViewFlowLayout>
<color key="primaryBackgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
<connections>
<outlet property="dataSource" destination="gPn-fP-LFc" id="2VB-5V-ltv"/>
<outlet property="delegate" destination="gPn-fP-LFc" id="LQ2-Vl-r08"/>
</connections>
</collectionView>
</subviews>
</clipView>
<scroller key="horizontalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="Dqn-NC-UFS">
<rect key="frame" x="1" y="144" width="233" height="15"/>
<scroller key="horizontalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="bK3-Ei-Odz">
<rect key="frame" x="1" y="213" width="348" height="16"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
<scroller key="verticalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" doubleValue="1" horizontal="NO" id="Enn-qw-4lt">
<scroller key="verticalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" doubleValue="1" horizontal="NO" id="SkF-fO-hfN">
<rect key="frame" x="234" y="1" width="15" height="143"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
</scrollView>
</subviews>
<constraints>
<constraint firstItem="wia-Qk-FEw" firstAttribute="top" secondItem="v1d-6d-Fl3" secondAttribute="top" id="NG4-cV-AAv"/>
<constraint firstAttribute="bottom" secondItem="wia-Qk-FEw" secondAttribute="bottom" id="emh-fE-n6N"/>
<constraint firstAttribute="trailing" secondItem="wia-Qk-FEw" secondAttribute="trailing" id="izX-0D-aEA"/>
<constraint firstItem="wia-Qk-FEw" firstAttribute="leading" secondItem="v1d-6d-Fl3" secondAttribute="leading" id="v7v-S1-h5Z"/>
<constraint firstItem="i5f-35-7x8" firstAttribute="top" secondItem="v1d-6d-Fl3" secondAttribute="top" id="BXd-LQ-OWX"/>
<constraint firstItem="i5f-35-7x8" firstAttribute="leading" secondItem="v1d-6d-Fl3" secondAttribute="leading" id="Bgm-NV-0Ft"/>
<constraint firstAttribute="bottom" secondItem="i5f-35-7x8" secondAttribute="bottom" id="G8K-PG-9Zq"/>
<constraint firstAttribute="trailing" secondItem="i5f-35-7x8" secondAttribute="trailing" id="vcm-wb-Ghi"/>
</constraints>
</view>
<connections>
<outlet property="albumCollectionView" destination="lfq-AB-epE" id="p69-Fs-hCN"/>
</connections>
</viewController>
<customObject id="uex-Ws-5X4" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>

Binary file not shown.

Binary file not shown.