├── Images ├── set │ └── set.png ├── concentration │ └── concentration.png ├── graphical-set │ └── graphical-set.png ├── image-gallery │ ├── image-gallery.png │ ├── image-gallery-details.png │ └── image-gallery-storyboard.png ├── animated-set │ ├── animated-set-ipad.png │ ├── animated-set-iphone.png │ ├── amimated-set-storyboard.png │ ├── animated-set-iphone-animating.png │ ├── animated-set-concentration-ipad.png │ ├── animated-set-concentration-iphone.png │ └── animated-set-concentration-iphone-animating.png └── persistent-image-gallery │ ├── persistent-image-gallery.png │ ├── persitent-image-gallery-animals.png │ ├── persistent-image-gallery-details.png │ └── persistent-image-gallery-storyboard.png ├── ImageGallery ├── ImageGallery │ ├── Supporting Files │ │ ├── Assets.xcassets │ │ │ ├── Contents.json │ │ │ ├── icon_trash.imageset │ │ │ │ ├── icon_trash@2x.png │ │ │ │ ├── icon_trash@3x.png │ │ │ │ └── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ ├── Base.lproj │ │ │ └── LaunchScreen.storyboard │ │ ├── AppDelegate.swift │ │ └── Utilities.swift │ ├── Views │ │ ├── ImageCollectionViewCell.swift │ │ └── GallerySelectionTableViewCell.swift │ ├── Controllers │ │ ├── ImageDisplayViewController.swift │ │ └── GallerySelectionTableViewController.swift │ ├── Models │ │ └── ImageGallery.swift │ ├── Info.plist │ └── Stores │ │ └── ImageGalleryStore.swift ├── ImageGallery.xcodeproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── ImageGalleryTests │ ├── Info.plist │ └── ImageGalleryTests.swift ├── PersistentImageGallery ├── PersistentImageGallery │ ├── Supporting Files │ │ ├── Assets.xcassets │ │ │ ├── Contents.json │ │ │ ├── icon_trash.imageset │ │ │ │ ├── icon_trash@2x.png │ │ │ │ ├── icon_trash@3x.png │ │ │ │ └── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ ├── ImageGalleryDocument.swift │ │ ├── Base.lproj │ │ │ └── LaunchScreen.storyboard │ │ ├── AppDelegate.swift │ │ └── Utilities.swift │ ├── Views │ │ └── ImageCollectionViewCell.swift │ ├── Controllers │ │ ├── ImageDisplayViewController.swift │ │ ├── UIViewController+Alerts.swift │ │ └── ImageGalleryDocumentBrowserViewController.swift │ ├── Managers │ │ └── ImageRequestManager.swift │ ├── Model │ │ └── ImageGallery.swift │ └── Info.plist ├── PersistentImageGallery.xcodeproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── PersistentImageGalleryTests │ ├── Info.plist │ └── PersistentImageGalleryTests.swift ├── SetGame ├── SetGame.xcodeproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── SetGameTests │ ├── Info.plist │ └── SetGameTests.swift └── SetGame │ ├── Info.plist │ ├── Supporting files │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ └── AppDelegate.swift │ ├── Models │ ├── SetCard.swift │ └── SetGame.swift │ └── ViewController.swift ├── Concentration ├── Concentration.xcodeproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Concentration │ ├── Int+arc4random.swift │ ├── Info.plist │ ├── Supporting files │ │ ├── Base.lproj │ │ │ └── LaunchScreen.storyboard │ │ ├── Assets.xcassets │ │ │ └── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ └── AppDelegate.swift │ ├── Card.swift │ ├── ViewController.swift │ └── Concentration.swift └── ConcentrationTests │ ├── Info.plist │ └── ConcentrationTests.swift ├── AnimatedSetGame ├── AnimatedSetGame.xcodeproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── AnimatedSetGame │ ├── Extensions │ │ └── Int+arc4random.swift │ ├── Info.plist │ ├── Views │ │ ├── Concentration │ │ │ ├── ConcentrationCardButton.swift │ │ │ └── ConcentrationCardsContainerView.swift │ │ ├── General Views │ │ │ └── CardButton.swift │ │ └── Set │ │ │ └── SetCardsContainerView.swift │ ├── Supporting files │ │ ├── Base.lproj │ │ │ └── LaunchScreen.storyboard │ │ ├── Assets.xcassets │ │ │ └── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ └── AppDelegate.swift │ ├── Controllers │ │ └── ConcentrationThemeChooserViewController.swift │ └── Models │ │ └── Set │ │ └── SetCard.swift └── AnimatedSetGameTests │ ├── Info.plist │ └── SetGameTests.swift ├── GraphicalSetGame ├── GraphicalSetGame.xcodeproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── GraphicalSetGameTests │ ├── Info.plist │ └── SetGameTests.swift └── GraphicalSetGame │ ├── Info.plist │ ├── Supporting files │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ └── AppDelegate.swift │ ├── Views │ ├── CardContainerView.swift │ └── SetCardButton.swift │ ├── Models │ ├── SetCard.swift │ └── SetGame.swift │ └── Controllers │ └── ViewController.swift ├── .gitignore └── README.md /Images/set/set.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TiagoMaiaL/cs193p-UIKit/HEAD/Images/set/set.png -------------------------------------------------------------------------------- /Images/concentration/concentration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TiagoMaiaL/cs193p-UIKit/HEAD/Images/concentration/concentration.png -------------------------------------------------------------------------------- /Images/graphical-set/graphical-set.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TiagoMaiaL/cs193p-UIKit/HEAD/Images/graphical-set/graphical-set.png -------------------------------------------------------------------------------- /Images/image-gallery/image-gallery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TiagoMaiaL/cs193p-UIKit/HEAD/Images/image-gallery/image-gallery.png -------------------------------------------------------------------------------- /Images/animated-set/animated-set-ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TiagoMaiaL/cs193p-UIKit/HEAD/Images/animated-set/animated-set-ipad.png -------------------------------------------------------------------------------- /Images/animated-set/animated-set-iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TiagoMaiaL/cs193p-UIKit/HEAD/Images/animated-set/animated-set-iphone.png -------------------------------------------------------------------------------- /Images/animated-set/amimated-set-storyboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TiagoMaiaL/cs193p-UIKit/HEAD/Images/animated-set/amimated-set-storyboard.png -------------------------------------------------------------------------------- /Images/image-gallery/image-gallery-details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TiagoMaiaL/cs193p-UIKit/HEAD/Images/image-gallery/image-gallery-details.png -------------------------------------------------------------------------------- /ImageGallery/ImageGallery/Supporting Files/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Images/image-gallery/image-gallery-storyboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TiagoMaiaL/cs193p-UIKit/HEAD/Images/image-gallery/image-gallery-storyboard.png -------------------------------------------------------------------------------- /Images/animated-set/animated-set-iphone-animating.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TiagoMaiaL/cs193p-UIKit/HEAD/Images/animated-set/animated-set-iphone-animating.png -------------------------------------------------------------------------------- /Images/animated-set/animated-set-concentration-ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TiagoMaiaL/cs193p-UIKit/HEAD/Images/animated-set/animated-set-concentration-ipad.png -------------------------------------------------------------------------------- /Images/animated-set/animated-set-concentration-iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TiagoMaiaL/cs193p-UIKit/HEAD/Images/animated-set/animated-set-concentration-iphone.png -------------------------------------------------------------------------------- /PersistentImageGallery/PersistentImageGallery/Supporting Files/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Images/persistent-image-gallery/persistent-image-gallery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TiagoMaiaL/cs193p-UIKit/HEAD/Images/persistent-image-gallery/persistent-image-gallery.png -------------------------------------------------------------------------------- /Images/animated-set/animated-set-concentration-iphone-animating.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TiagoMaiaL/cs193p-UIKit/HEAD/Images/animated-set/animated-set-concentration-iphone-animating.png -------------------------------------------------------------------------------- /Images/persistent-image-gallery/persitent-image-gallery-animals.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TiagoMaiaL/cs193p-UIKit/HEAD/Images/persistent-image-gallery/persitent-image-gallery-animals.png -------------------------------------------------------------------------------- /Images/persistent-image-gallery/persistent-image-gallery-details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TiagoMaiaL/cs193p-UIKit/HEAD/Images/persistent-image-gallery/persistent-image-gallery-details.png -------------------------------------------------------------------------------- /Images/persistent-image-gallery/persistent-image-gallery-storyboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TiagoMaiaL/cs193p-UIKit/HEAD/Images/persistent-image-gallery/persistent-image-gallery-storyboard.png -------------------------------------------------------------------------------- /ImageGallery/ImageGallery/Supporting Files/Assets.xcassets/icon_trash.imageset/icon_trash@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TiagoMaiaL/cs193p-UIKit/HEAD/ImageGallery/ImageGallery/Supporting Files/Assets.xcassets/icon_trash.imageset/icon_trash@2x.png -------------------------------------------------------------------------------- /ImageGallery/ImageGallery/Supporting Files/Assets.xcassets/icon_trash.imageset/icon_trash@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TiagoMaiaL/cs193p-UIKit/HEAD/ImageGallery/ImageGallery/Supporting Files/Assets.xcassets/icon_trash.imageset/icon_trash@3x.png -------------------------------------------------------------------------------- /SetGame/SetGame.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Concentration/Concentration.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ImageGallery/ImageGallery.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /PersistentImageGallery/PersistentImageGallery/Supporting Files/Assets.xcassets/icon_trash.imageset/icon_trash@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TiagoMaiaL/cs193p-UIKit/HEAD/PersistentImageGallery/PersistentImageGallery/Supporting Files/Assets.xcassets/icon_trash.imageset/icon_trash@2x.png -------------------------------------------------------------------------------- /PersistentImageGallery/PersistentImageGallery/Supporting Files/Assets.xcassets/icon_trash.imageset/icon_trash@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TiagoMaiaL/cs193p-UIKit/HEAD/PersistentImageGallery/PersistentImageGallery/Supporting Files/Assets.xcassets/icon_trash.imageset/icon_trash@3x.png -------------------------------------------------------------------------------- /PersistentImageGallery/PersistentImageGallery.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SetGame/SetGame.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ImageGallery/ImageGallery.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /AnimatedSetGame/AnimatedSetGame.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Concentration/Concentration.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /AnimatedSetGame/AnimatedSetGame.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /GraphicalSetGame/GraphicalSetGame.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /GraphicalSetGame/GraphicalSetGame.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /PersistentImageGallery/PersistentImageGallery.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Concentration/Concentration/Int+arc4random.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Int+arc4random.swift 3 | // Concentration 4 | // 5 | // Created by Tiago Maia Lopes on 1/21/18. 6 | // Copyright © 2018 Tiago Maia Lopes. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Int { 12 | 13 | /// A facility property for accessing a random 14 | /// value from the current Int instance. 15 | var arc4random: Int { 16 | return Int(arc4random_uniform(UInt32(self))) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /AnimatedSetGame/AnimatedSetGame/Extensions/Int+arc4random.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Int+arc4random.swift 3 | // Concentration 4 | // 5 | // Created by Tiago Maia Lopes on 1/21/18. 6 | // Copyright © 2018 Tiago Maia Lopes. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Int { 12 | 13 | /// A facility property for accessing a random 14 | /// value from the current Int instance. 15 | var arc4random: Int { 16 | return Int(arc4random_uniform(UInt32(self))) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ImageGallery/ImageGallery/Supporting Files/Assets.xcassets/icon_trash.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "icon_trash@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "icon_trash@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /PersistentImageGallery/PersistentImageGallery/Supporting Files/Assets.xcassets/icon_trash.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "icon_trash@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "icon_trash@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /SetGame/SetGameTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /ImageGallery/ImageGalleryTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Concentration/ConcentrationTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /AnimatedSetGame/AnimatedSetGameTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /GraphicalSetGame/GraphicalSetGameTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /PersistentImageGallery/PersistentImageGalleryTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSAppTransportSecurity 6 | 7 | NSAllowsArbitraryLoads 8 | 9 | 10 | CFBundleDevelopmentRegion 11 | $(DEVELOPMENT_LANGUAGE) 12 | CFBundleExecutable 13 | $(EXECUTABLE_NAME) 14 | CFBundleIdentifier 15 | $(PRODUCT_BUNDLE_IDENTIFIER) 16 | CFBundleInfoDictionaryVersion 17 | 6.0 18 | CFBundleName 19 | $(PRODUCT_NAME) 20 | CFBundlePackageType 21 | BNDL 22 | CFBundleShortVersionString 23 | 1.0 24 | CFBundleVersion 25 | 1 26 | 27 | 28 | -------------------------------------------------------------------------------- /ImageGallery/ImageGallery/Views/ImageCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageCollectionViewCell.swift 3 | // ImageGallery 4 | // 5 | // Created by Tiago Maia Lopes on 21/03/18. 6 | // Copyright © 2018 Tiago Maia Lopes. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// The cell in charge of displaying a single image of the gallery. 12 | class ImageCollectionViewCell: UICollectionViewCell { 13 | 14 | // MARK: - Properties 15 | 16 | /// The cell's image view. 17 | @IBOutlet weak var imageView: UIImageView! 18 | 19 | /// The cell's loading spinner. 20 | @IBOutlet weak var activityIndicator: UIActivityIndicatorView! 21 | 22 | /// The cell's loading flag. 23 | var isLoading = true { 24 | didSet { 25 | if isLoading { 26 | activityIndicator.startAnimating() 27 | } else { 28 | activityIndicator.stopAnimating() 29 | } 30 | } 31 | } 32 | 33 | // MARK: - Life cycle 34 | 35 | override func prepareForReuse() { 36 | super.prepareForReuse() 37 | imageView.image = nil 38 | isLoading = true 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /PersistentImageGallery/PersistentImageGallery/Views/ImageCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageCollectionViewCell.swift 3 | // ImageGallery 4 | // 5 | // Created by Tiago Maia Lopes on 21/03/18. 6 | // Copyright © 2018 Tiago Maia Lopes. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// The cell in charge of displaying a single image of the gallery. 12 | class ImageCollectionViewCell: UICollectionViewCell { 13 | 14 | // MARK: - Properties 15 | 16 | /// The cell's image view. 17 | @IBOutlet weak var imageView: UIImageView! 18 | 19 | /// The cell's loading spinner. 20 | @IBOutlet weak var activityIndicator: UIActivityIndicatorView! 21 | 22 | /// The cell's loading flag. 23 | var isLoading = true { 24 | didSet { 25 | if isLoading { 26 | activityIndicator.startAnimating() 27 | } else { 28 | activityIndicator.stopAnimating() 29 | } 30 | } 31 | } 32 | 33 | // MARK: - Life cycle 34 | 35 | override func prepareForReuse() { 36 | super.prepareForReuse() 37 | imageView.image = nil 38 | isLoading = true 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /SetGame/SetGameTests/SetGameTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SetGameTests.swift 3 | // SetGameTests 4 | // 5 | // Created by Tiago Maia Lopes on 1/23/18. 6 | // Copyright © 2018 Tiago Maia Lopes. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import SetGame 11 | 12 | class SetGameTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | // Use XCTAssert and related functions to verify your tests produce the correct results. 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /AnimatedSetGame/AnimatedSetGameTests/SetGameTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SetGameTests.swift 3 | // SetGameTests 4 | // 5 | // Created by Tiago Maia Lopes on 1/23/18. 6 | // Copyright © 2018 Tiago Maia Lopes. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import SetGame 11 | 12 | class SetGameTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | // Use XCTAssert and related functions to verify your tests produce the correct results. 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /GraphicalSetGame/GraphicalSetGameTests/SetGameTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SetGameTests.swift 3 | // SetGameTests 4 | // 5 | // Created by Tiago Maia Lopes on 1/23/18. 6 | // Copyright © 2018 Tiago Maia Lopes. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import SetGame 11 | 12 | class SetGameTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | // Use XCTAssert and related functions to verify your tests produce the correct results. 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /ImageGallery/ImageGalleryTests/ImageGalleryTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageGalleryTests.swift 3 | // ImageGalleryTests 4 | // 5 | // Created by Tiago Maia Lopes on 21/03/18. 6 | // Copyright © 2018 Tiago Maia Lopes. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import ImageGallery 11 | 12 | class ImageGalleryTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | // Use XCTAssert and related functions to verify your tests produce the correct results. 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /Concentration/ConcentrationTests/ConcentrationTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConcentrationTests.swift 3 | // ConcentrationTests 4 | // 5 | // Created by Tiago Maia Lopes on 1/16/18. 6 | // Copyright © 2018 Tiago Maia Lopes. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Concentration 11 | 12 | class ConcentrationTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | // Use XCTAssert and related functions to verify your tests produce the correct results. 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /PersistentImageGallery/PersistentImageGalleryTests/PersistentImageGalleryTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PersistentImageGalleryTests.swift 3 | // PersistentImageGalleryTests 4 | // 5 | // Created by Tiago Maia Lopes on 03/04/18. 6 | // Copyright © 2018 Tiago Maia Lopes. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import PersistentImageGallery 11 | 12 | class PersistentImageGalleryTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | // Use XCTAssert and related functions to verify your tests produce the correct results. 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /ImageGallery/ImageGallery/Controllers/ImageDisplayViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageDisplayViewController.swift 3 | // ImageGallery 4 | // 5 | // Created by Tiago Maia Lopes on 21/03/18. 6 | // Copyright © 2018 Tiago Maia Lopes. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ImageDisplayViewController: UIViewController, UIScrollViewDelegate { 12 | 13 | // MARK: - Properties 14 | 15 | /// The imageView displaying the passed image. 16 | @IBOutlet weak var imageView: UIImageView! 17 | 18 | /// The scrollView containing the view. 19 | @IBOutlet weak var scrollView: UIScrollView! { 20 | didSet { 21 | scrollView.minimumZoomScale = 1/8 22 | scrollView.maximumZoomScale = 1 23 | } 24 | } 25 | 26 | /// The image being displayed. 27 | var image: ImageGallery.Image! 28 | 29 | // MARK: - Life cycle 30 | 31 | override func viewDidLoad() { 32 | super.viewDidLoad() 33 | } 34 | 35 | override func viewWillAppear(_ animated: Bool) { 36 | super.viewWillAppear(animated) 37 | 38 | if let data = image?.imageData { 39 | imageView?.image = UIImage(data: data) 40 | } 41 | } 42 | 43 | // MARK: - scroll view delegate 44 | 45 | func viewForZooming(in scrollView: UIScrollView) -> UIView? { 46 | return imageView 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /PersistentImageGallery/PersistentImageGallery/Controllers/ImageDisplayViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageDisplayViewController.swift 3 | // ImageGallery 4 | // 5 | // Created by Tiago Maia Lopes on 21/03/18. 6 | // Copyright © 2018 Tiago Maia Lopes. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ImageDisplayViewController: UIViewController, UIScrollViewDelegate { 12 | 13 | // MARK: - Properties 14 | 15 | /// The imageView displaying the passed image. 16 | @IBOutlet weak var imageView: UIImageView! 17 | 18 | /// The scrollView containing the view. 19 | @IBOutlet weak var scrollView: UIScrollView! { 20 | didSet { 21 | scrollView.minimumZoomScale = 1/8 22 | scrollView.maximumZoomScale = 1 23 | } 24 | } 25 | 26 | /// The image being displayed. 27 | var image: UIImage! { 28 | didSet { 29 | imageView?.image = image 30 | } 31 | } 32 | 33 | // MARK: - Life cycle 34 | 35 | override func viewDidLoad() { 36 | super.viewDidLoad() 37 | } 38 | 39 | override func viewWillAppear(_ animated: Bool) { 40 | super.viewWillAppear(animated) 41 | imageView.image = image 42 | } 43 | 44 | // MARK: - scroll view delegate 45 | 46 | func viewForZooming(in scrollView: UIScrollView) -> UIView? { 47 | return imageView 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /PersistentImageGallery/PersistentImageGallery/Supporting Files/ImageGalleryDocument.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageGalleryDocument.swift 3 | // PersistentImageGallery 4 | // 5 | // Created by Tiago Maia Lopes on 03/04/18. 6 | // Copyright © 2018 Tiago Maia Lopes. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ImageGalleryDocument: UIDocument { 12 | 13 | // MARK: - Properties 14 | 15 | /// The document thumbnail. 16 | var thumbnail: UIImage? 17 | 18 | /// The gallery stored by this document. 19 | var gallery: ImageGallery? 20 | 21 | // MARK: - Life cycle 22 | 23 | override func contents(forType typeName: String) throws -> Any { 24 | return gallery?.json ?? Data() 25 | } 26 | 27 | override func load(fromContents contents: Any, ofType typeName: String?) throws { 28 | if let data = contents as? Data { 29 | gallery = ImageGallery(json: data) 30 | } 31 | } 32 | 33 | override func fileAttributesToWrite(to url: URL, for saveOperation: UIDocumentSaveOperation) throws -> [AnyHashable : Any] { 34 | var attributes = try super.fileAttributesToWrite(to: url, for: saveOperation) 35 | if let thumbnail = thumbnail { 36 | attributes[URLResourceKey.thumbnailDictionaryKey] = [URLThumbnailDictionaryItem.NSThumbnail1024x1024SizeKey : thumbnail] 37 | } 38 | 39 | return attributes 40 | } 41 | } 42 | 43 | -------------------------------------------------------------------------------- /PersistentImageGallery/PersistentImageGallery/Controllers/UIViewController+Alerts.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+Alerts.swift 3 | // PersistentImageGallery 4 | // 5 | // Created by Tiago Maia Lopes on 11/04/18. 6 | // Copyright © 2018 Tiago Maia Lopes. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// Adds alert capabilities to all view controllers. 12 | extension UIViewController { 13 | 14 | // MARK: - Factories 15 | 16 | /// Returns an alert controller with the passed title and message. 17 | func makeAlertWith(title: String, message: String) -> UIAlertController { 18 | return UIAlertController(title: title, message: message, preferredStyle: .alert) 19 | } 20 | 21 | // MARK: - Imperatives 22 | 23 | /// Presents a warning alert with the provided title and message. 24 | func presentWarningWith(title: String, message: String, handler: Optional<() -> ()> = nil) { 25 | let alert = makeAlertWith(title: title, message: message) 26 | _ = alert.addActionWith(title: "Ok", style: .default) 27 | 28 | present(alert, animated: true, completion: handler) 29 | } 30 | } 31 | 32 | extension UIAlertController { 33 | 34 | // MARK: - Imperatives 35 | 36 | /// Adds a new alert action to the alert controller. 37 | func addActionWith(title: String, style: UIAlertActionStyle = .default, handler: Optional<(UIAlertAction) -> Swift.Void> = nil) -> UIAlertAction { 38 | let action = UIAlertAction(title: title, style: style, handler: handler) 39 | addAction(action) 40 | 41 | return action 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /SetGame/SetGame/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Concentration/Concentration/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /GraphicalSetGame/GraphicalSetGame/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /ImageGallery/ImageGallery/Models/ImageGallery.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageGallery.swift 3 | // ImageGallery 4 | // 5 | // Created by Tiago Maia Lopes on 21/03/18. 6 | // Copyright © 2018 Tiago Maia Lopes. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Model representing a gallery with it's images. 12 | struct ImageGallery: Hashable, Codable { 13 | 14 | /// Model representing a gallery's image. 15 | struct Image: Hashable, Codable { 16 | 17 | // MARK: - Hashable 18 | 19 | var hashValue: Int { 20 | return imagePath?.hashValue ?? 0 21 | } 22 | 23 | static func ==(lhs: ImageGallery.Image, rhs: ImageGallery.Image) -> Bool { 24 | return lhs.imagePath == rhs.imagePath 25 | } 26 | 27 | // MARK: - Properties 28 | 29 | /// The image's URL. 30 | var imagePath: URL? 31 | 32 | /// The image's aspect ratio. 33 | var aspectRatio: Double 34 | 35 | /// The fetched image's data. 36 | var imageData: Data? 37 | 38 | /// MARK: - Initializer 39 | 40 | init(imagePath: URL?, aspectRatio: Double) { 41 | self.imagePath = imagePath 42 | self.aspectRatio = aspectRatio 43 | } 44 | } 45 | 46 | // MARK: - Properties 47 | 48 | /// The gallery's identifier. 49 | let identifier: String = UUID().uuidString 50 | 51 | /// The gallery's images. 52 | var images: [Image] 53 | 54 | /// The gallery's title. 55 | var title: String 56 | 57 | // MARK: - Hashable 58 | 59 | var hashValue: Int { 60 | return identifier.hashValue 61 | } 62 | 63 | static func ==(lhs: ImageGallery, rhs: ImageGallery) -> Bool { 64 | return lhs.identifier == rhs.identifier 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /AnimatedSetGame/AnimatedSetGame/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | UIInterfaceOrientationPortraitUpsideDown 37 | 38 | UISupportedInterfaceOrientations~ipad 39 | 40 | UIInterfaceOrientationPortrait 41 | UIInterfaceOrientationPortraitUpsideDown 42 | UIInterfaceOrientationLandscapeLeft 43 | UIInterfaceOrientationLandscapeRight 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | .DS_Store 6 | 7 | ## Build generated 8 | build/ 9 | DerivedData/ 10 | 11 | ## Various settings 12 | *.pbxuser 13 | !default.pbxuser 14 | *.mode1v3 15 | !default.mode1v3 16 | *.mode2v3 17 | !default.mode2v3 18 | *.perspectivev3 19 | !default.perspectivev3 20 | xcuserdata/ 21 | 22 | ## Other 23 | *.moved-aside 24 | *.xccheckout 25 | *.xcscmblueprint 26 | 27 | ## Obj-C/Swift specific 28 | *.hmap 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | .build/ 43 | 44 | # CocoaPods 45 | # 46 | # We recommend against adding the Pods directory to your .gitignore. However 47 | # you should judge for yourself, the pros and cons are mentioned at: 48 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 49 | # 50 | # Pods/ 51 | 52 | # Carthage 53 | # 54 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 55 | # Carthage/Checkouts 56 | 57 | Carthage/Build 58 | 59 | # fastlane 60 | # 61 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 62 | # screenshots whenever they are needed. 63 | # For more information about the recommended setup visit: 64 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 65 | 66 | fastlane/report.xml 67 | fastlane/Preview.html 68 | fastlane/screenshots 69 | fastlane/test_output 70 | -------------------------------------------------------------------------------- /AnimatedSetGame/AnimatedSetGame/Views/Concentration/ConcentrationCardButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConcentrationCardButton.swift 3 | // AnimatedSetGamee 4 | // 5 | // Created by Tiago Maia Lopes on 08/03/18. 6 | // Copyright © 2018 Tiago Maia Lopes. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ConcentrationCardButton: CardButton, NSCopying { 12 | 13 | typealias Emoji = String 14 | 15 | // MARK: Properties 16 | 17 | /// The concentration text emoji. 18 | var buttonText: Emoji? { 19 | didSet { 20 | if isFaceUp { 21 | setNeedsDisplay() 22 | } 23 | } 24 | } 25 | 26 | /// The color of the back of the card when flipped down. 27 | var backColor: CGColor? { 28 | didSet { 29 | if !isFaceUp { 30 | setNeedsDisplay() 31 | } 32 | } 33 | } 34 | 35 | // MARK: Imperatives 36 | 37 | override func drawFront() { 38 | layer.backgroundColor = UIColor.white.cgColor 39 | titleLabel?.font = UIFont.systemFont(ofSize: 50) 40 | 41 | if let buttonText = buttonText { 42 | setTitle(buttonText, for: .normal) 43 | } 44 | } 45 | 46 | override func drawBack() { 47 | layer.backgroundColor = backColor ?? UIColor.gray.cgColor 48 | setTitle(nil, for: .normal) 49 | } 50 | 51 | // MARK: NSCopying implementation 52 | 53 | func copy(with zone: NSZone? = nil) -> Any { 54 | let newCardButton = ConcentrationCardButton() 55 | newCardButton.frame = frame 56 | newCardButton.layer.backgroundColor = layer.backgroundColor 57 | newCardButton.isActive = isActive 58 | newCardButton.isFaceUp = isFaceUp 59 | newCardButton.backColor = backColor 60 | newCardButton.buttonText = buttonText 61 | 62 | return newCardButton 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /SetGame/SetGame/Supporting files/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /ImageGallery/ImageGallery/Supporting Files/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /AnimatedSetGame/AnimatedSetGame/Supporting files/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /GraphicalSetGame/GraphicalSetGame/Supporting files/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /PersistentImageGallery/PersistentImageGallery/Supporting Files/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /PersistentImageGallery/PersistentImageGallery/Managers/ImageRequestManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageRequestManager.swift 3 | // PersistentImageGallery 4 | // 5 | // Created by Tiago Maia Lopes on 08/04/18. 6 | // Copyright © 2018 Tiago Maia Lopes. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Class in charge of requesting the provided images 12 | /// from the internet and cache them. 13 | class ImageRequestManager { 14 | 15 | // MARK: - Properties 16 | 17 | /// The session used to make each data task. 18 | private(set) lazy var session: URLSession = { 19 | let cache = URLCache(memoryCapacity: 4 * 1024 * 1024, diskCapacity: 80 * 1024 * 1024, diskPath: nil) 20 | 21 | let configuration = URLSessionConfiguration.default 22 | configuration.urlCache = cache 23 | configuration.requestCachePolicy = .returnCacheDataElseLoad 24 | 25 | return URLSession(configuration: configuration, delegate: nil, delegateQueue: nil) 26 | }() 27 | 28 | // MARK: - Imperatives 29 | 30 | /// Requests an image at the provided URL. 31 | func request( 32 | at url: URL, 33 | withCompletionHandler completion: @escaping (Data) -> (), 34 | andErrorHandler onError: @escaping (Error?, URLResponse?) -> () 35 | ) { 36 | let task = session.dataTask(with: url) { (data, response, transportError) in 37 | 38 | guard transportError == nil, let data = data else { 39 | onError(transportError, nil) 40 | return 41 | } 42 | 43 | guard let httpResponse = response as? HTTPURLResponse, 44 | (200...299).contains(httpResponse.statusCode), 45 | ["image/jpeg", "image/png"].contains(httpResponse.mimeType) else { 46 | onError(nil, response) 47 | return 48 | } 49 | 50 | completion(data) 51 | } 52 | 53 | task.resume() 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /ImageGallery/ImageGallery/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDocumentTypes 8 | 9 | 10 | 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | $(PRODUCT_NAME) 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | 1.0 23 | CFBundleVersion 24 | 1 25 | LSApplicationCategoryType 26 | 27 | LSRequiresIPhoneOS 28 | 29 | NSAppTransportSecurity 30 | 31 | NSAllowsArbitraryLoads 32 | 33 | 34 | UILaunchStoryboardName 35 | LaunchScreen 36 | UIMainStoryboardFile 37 | Main 38 | UIRequiredDeviceCapabilities 39 | 40 | armv7 41 | 42 | UISupportedInterfaceOrientations 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationLandscapeLeft 46 | UIInterfaceOrientationLandscapeRight 47 | 48 | UISupportedInterfaceOrientations~ipad 49 | 50 | UIInterfaceOrientationPortrait 51 | UIInterfaceOrientationPortraitUpsideDown 52 | UIInterfaceOrientationLandscapeLeft 53 | UIInterfaceOrientationLandscapeRight 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /Concentration/Concentration/Supporting files/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Concentration/Concentration/Card.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Card.swift 3 | // Concentration 4 | // 5 | // Created by Tiago Maia Lopes on 1/17/18. 6 | // Copyright © 2018 Tiago Maia Lopes. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Card { 12 | 13 | // MARK: Properties 14 | 15 | /// The card's identifier. 16 | /// Used to check for a match. 17 | private let identifier: Int 18 | 19 | /// Determines if the card has already been matched. 20 | var isMatched = false 21 | 22 | /// Indicates whether the card is faced up or not. 23 | var isFaceUp = false 24 | 25 | /// Indicates if the card has already been flipped. 26 | /// Might be used in a score system. 27 | var hasBeenFlipped = false 28 | 29 | // MARK: Initializer 30 | 31 | /// Prepares a card with a brand new identifier. 32 | init() { 33 | identifier = Card.makeIdentifier() 34 | } 35 | 36 | // MARK: Imperatives 37 | 38 | /// Toggles the flipped state of the card. 39 | /// If it's face up, set it face down, and vice versa. 40 | mutating func flipCard() { 41 | isFaceUp = !isFaceUp 42 | } 43 | 44 | /// Flips a card to the face down state. 45 | mutating func setFaceDown() { 46 | if isFaceUp { 47 | isFaceUp = false 48 | } 49 | } 50 | 51 | // MARK: Static properties and methods 52 | 53 | /// The identifier count, used to retrieve an 54 | /// identifier for each initialized card. 55 | private static var identifiersCount = -1 56 | 57 | /// Resets the current identifier count. 58 | static func resetIdentifiersCount() { 59 | identifiersCount = -1 60 | } 61 | 62 | /// Returns a new identifier for model usage. 63 | static func makeIdentifier() -> Int { 64 | identifiersCount += 1 65 | return identifiersCount 66 | } 67 | 68 | } 69 | 70 | // MARK: Hashable protocol implementation 71 | 72 | extension Card: Hashable { 73 | 74 | var hashValue: Int { 75 | return identifier 76 | } 77 | 78 | static func ==(lhs: Card, rhs: Card) -> Bool { 79 | return lhs.identifier == rhs.identifier 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /SetGame/SetGame/Supporting files/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /ImageGallery/ImageGallery/Supporting Files/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /AnimatedSetGame/AnimatedSetGame/Supporting files/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Concentration/Concentration/Supporting files/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /GraphicalSetGame/GraphicalSetGame/Supporting files/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /PersistentImageGallery/PersistentImageGallery/Supporting Files/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /PersistentImageGallery/PersistentImageGallery/Model/ImageGallery.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageGallery.swift 3 | // ImageGallery 4 | // 5 | // Created by Tiago Maia Lopes on 21/03/18. 6 | // Copyright © 2018 Tiago Maia Lopes. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Model representing a gallery with it's images. 12 | struct ImageGallery: Hashable, Codable { 13 | 14 | /// Model representing a gallery's image. 15 | struct Image: Hashable, Codable { 16 | 17 | // MARK: - Hashable 18 | 19 | var hashValue: Int { 20 | return imagePath?.hashValue ?? 0 21 | } 22 | 23 | static func ==(lhs: ImageGallery.Image, rhs: ImageGallery.Image) -> Bool { 24 | return lhs.imagePath == rhs.imagePath 25 | } 26 | 27 | // MARK: - Properties 28 | 29 | /// The image's URL. 30 | var imagePath: URL? 31 | 32 | /// The image's aspect ratio. 33 | var aspectRatio: Double 34 | 35 | /// MARK: - Initializer 36 | 37 | init(imagePath: URL?, aspectRatio: Double) { 38 | self.imagePath = imagePath 39 | self.aspectRatio = aspectRatio 40 | } 41 | } 42 | 43 | // MARK: - Properties 44 | 45 | /// The gallery's identifier. 46 | let identifier: String 47 | 48 | /// The gallery's images. 49 | var images: [Image] 50 | 51 | /// The gallery's title. 52 | var title: String 53 | 54 | /// This instance's encoded value. 55 | var json: Data? { 56 | return try? JSONEncoder().encode(self) 57 | } 58 | 59 | // MARK: - Initializers 60 | 61 | init(images: [Image], title: String) { 62 | identifier = UUID().uuidString 63 | self.images = images 64 | self.title = title 65 | } 66 | 67 | /// Returns the instance from the passed json data. 68 | /// - Parameter json: The json data used to instantiate the instance. 69 | init?(json: Data) { 70 | if let decodedSelf = try? JSONDecoder().decode(ImageGallery.self, from: json) { 71 | self = decodedSelf 72 | } else { 73 | return nil 74 | } 75 | } 76 | 77 | // MARK: - Hashable 78 | 79 | var hashValue: Int { 80 | return identifier.hashValue 81 | } 82 | 83 | static func ==(lhs: ImageGallery, rhs: ImageGallery) -> Bool { 84 | return lhs.identifier == rhs.identifier 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /SetGame/SetGame/Supporting files/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // SetGame 4 | // 5 | // Created by Tiago Maia Lopes on 1/23/18. 6 | // Copyright © 2018 Tiago Maia Lopes. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /AnimatedSetGame/AnimatedSetGame/Supporting files/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // SetGame 4 | // 5 | // Created by Tiago Maia Lopes on 1/23/18. 6 | // Copyright © 2018 Tiago Maia Lopes. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /Concentration/Concentration/Supporting files/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Concentration 4 | // 5 | // Created by Tiago Maia Lopes on 1/16/18. 6 | // Copyright © 2018 Tiago Maia Lopes. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /GraphicalSetGame/GraphicalSetGame/Supporting files/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // SetGame 4 | // 5 | // Created by Tiago Maia Lopes on 1/23/18. 6 | // Copyright © 2018 Tiago Maia Lopes. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /ImageGallery/ImageGallery/Views/GallerySelectionTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GallerySelectionTableViewCell.swift 3 | // ImageGallery 4 | // 5 | // Created by Tiago Maia Lopes on 26/03/18. 6 | // Copyright © 2018 Tiago Maia Lopes. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol GallerySelectionTableViewCellDelegate { 12 | func titleDidChange(_ title: String, in cell: UITableViewCell) 13 | } 14 | 15 | class GallerySelectionTableViewCell: UITableViewCell, UITextFieldDelegate { 16 | 17 | // MARK: - Properties 18 | 19 | /// The cell's delegate 20 | var delegate: GallerySelectionTableViewCellDelegate? 21 | 22 | /// The text field used to edit the title's row. 23 | @IBOutlet weak var titleTextField: UITextField! { 24 | didSet { 25 | titleTextField.addTarget(self, 26 | action: #selector(titleDidChange(_:)), 27 | for: .editingDidEnd) 28 | titleTextField.returnKeyType = .done 29 | titleTextField.delegate = self 30 | } 31 | } 32 | 33 | /// The row's title. 34 | var title: String { 35 | set { 36 | titleTextField?.text = newValue 37 | } 38 | get { 39 | return titleTextField.text ?? "" 40 | } 41 | } 42 | 43 | /// - Note: Change this property to enable/disable the internal textField. 44 | override var isEditing: Bool { 45 | didSet { 46 | titleTextField.isEnabled = isEditing 47 | 48 | if isEditing == true { 49 | titleTextField.becomeFirstResponder() 50 | } else { 51 | titleTextField.resignFirstResponder() 52 | } 53 | } 54 | } 55 | 56 | // MARK: - Imperatives 57 | 58 | private func endEditing() { 59 | isEditing = false 60 | } 61 | 62 | // MARK: - Actions 63 | 64 | @objc func titleDidChange(_ sender: UITextField) { 65 | guard let title = sender.text, title != "" else { 66 | return 67 | } 68 | 69 | delegate?.titleDidChange(sender.text ?? "", in: self) 70 | } 71 | 72 | // MARK: - Text field delegate 73 | 74 | override var canBecomeFirstResponder: Bool { 75 | return isEditing 76 | } 77 | 78 | func textFieldDidEndEditing(_ textField: UITextField) { 79 | endEditing() 80 | } 81 | 82 | func textFieldShouldReturn(_ textField: UITextField) -> Bool { 83 | endEditing() 84 | return true 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /PersistentImageGallery/PersistentImageGallery/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDocumentTypes 8 | 9 | 10 | CFBundleTypeIconFiles 11 | 12 | CFBundleTypeName 13 | ImageGallery 14 | CFBundleTypeRole 15 | Editor 16 | LSHandlerRank 17 | Owner 18 | LSItemContentTypes 19 | 20 | tiago.maia.PersistentImageGallery.imageGallery 21 | 22 | 23 | 24 | CFBundleExecutable 25 | $(EXECUTABLE_NAME) 26 | CFBundleIdentifier 27 | $(PRODUCT_BUNDLE_IDENTIFIER) 28 | CFBundleInfoDictionaryVersion 29 | 6.0 30 | CFBundleName 31 | $(PRODUCT_NAME) 32 | CFBundlePackageType 33 | APPL 34 | CFBundleShortVersionString 35 | 1.0 36 | CFBundleVersion 37 | 1 38 | LSRequiresIPhoneOS 39 | 40 | UILaunchStoryboardName 41 | LaunchScreen 42 | UIMainStoryboardFile 43 | Main 44 | UIRequiredDeviceCapabilities 45 | 46 | armv7 47 | 48 | UISupportedInterfaceOrientations 49 | 50 | UIInterfaceOrientationPortrait 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | 54 | UISupportedInterfaceOrientations~ipad 55 | 56 | UIInterfaceOrientationPortrait 57 | UIInterfaceOrientationPortraitUpsideDown 58 | UIInterfaceOrientationLandscapeLeft 59 | UIInterfaceOrientationLandscapeRight 60 | 61 | UISupportsDocumentBrowser 62 | 63 | UTExportedTypeDeclarations 64 | 65 | 66 | UTTypeConformsTo 67 | 68 | public.data 69 | 70 | UTTypeDescription 71 | ImageGallery 72 | UTTypeIconFiles 73 | 74 | UTTypeIdentifier 75 | tiago.maia.PersistentImageGallery.imageGallery 76 | UTTypeTagSpecification 77 | 78 | public.filename-extension 79 | imagegallery 80 | 81 | 82 | 83 | 84 | UTImportedTypeDeclarations 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /ImageGallery/ImageGallery/Supporting Files/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // ImageGallery 4 | // 5 | // Created by Tiago Maia Lopes on 21/03/18. 6 | // Copyright © 2018 Tiago Maia Lopes. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 18 | 19 | let galleriesStore = ImageGalleryStore() 20 | 21 | if let gallerySelectionController = (window?.rootViewController as? UISplitViewController)?.viewControllers.first?.contents as? GallerySelectionTableViewController { 22 | gallerySelectionController.galleriesStore = galleriesStore 23 | } 24 | 25 | if let galleryDisplayController = (window?.rootViewController as? UISplitViewController)?.viewControllers.last?.contents as? GalleryDisplayCollectionViewController { 26 | galleryDisplayController.galleriesStore = galleriesStore 27 | galleryDisplayController.gallery = galleriesStore.galleries.first 28 | } 29 | 30 | return true 31 | } 32 | 33 | func applicationWillResignActive(_ application: UIApplication) { 34 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 35 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 36 | } 37 | 38 | func applicationDidEnterBackground(_ application: UIApplication) { 39 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 40 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 41 | } 42 | 43 | func applicationWillEnterForeground(_ application: UIApplication) { 44 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 45 | } 46 | 47 | func applicationDidBecomeActive(_ application: UIApplication) { 48 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 49 | } 50 | 51 | func applicationWillTerminate(_ application: UIApplication) { 52 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 53 | } 54 | 55 | 56 | } 57 | 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CS193P-assignments 2 | The solutions for each assignment presented in Stanford's "developing iOS 11 apps with swift" course. 3 | 4 | ## Concentration: 5 | 6 | drawing 7 | 8 | ## Set: 9 | 10 | Set 11 | 12 | ## Graphical set: 13 | 14 | Graphical set 15 | 16 | ## Animated set: 17 | 18 | A blog post about my experiences while building the project: https://tiagomaiadotblog.wordpress.com/2018/03/16/animated-set-cs193p-fall-of-2017-assignment-iv-solution/ 19 | 20 | Animated set storyboard 21 | Animated set iphone 22 | Animated set iphone animating 23 | Concentration iphone 24 | Concentration iphone animating 25 | Animated set ipad 26 | Animated set ipad 27 | 28 | ## Image gallery: 29 | 30 | A blog post about my experiences while building the project: https://tiagomaiadotblog.wordpress.com/2018/04/03/image-gallery-cs193p-fall-of-2017-assignment-v-solution/ 31 | 32 | Image gallery storyboard 33 | Image gallery main screen 34 | Image gallery details screen 35 | 36 | ## Persistent image gallery: 37 | 38 | A blog post about my experiences while building the project: https://tiagomaiadotblog.wordpress.com/2018/04/11/persistent-image-gallery-cs193p-fall-of-2017-assignment-vi-solution/ 39 | 40 | Persistent image gallery storyboard 42 | Persistent image gallery 43 | Persistent image gallery images screen 44 | Persistent image gallery details screen 45 | 46 | ## Final project 47 | 48 | My final project is located at the following repository: https://github.com/TiagoMaiaL/Habit-Calendar. 49 | 50 | ### Was this repository useful to you? If so, give it a star, so other students can more easily find it. 51 | -------------------------------------------------------------------------------- /GraphicalSetGame/GraphicalSetGame/Views/CardContainerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardContainerView.swift 3 | // GraphicalSetGamee 4 | // 5 | // Created by Tiago Maia Lopes on 09/02/18. 6 | // Copyright © 2018 Tiago Maia Lopes. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// The view responsible for holding and displaying the cardButtons. 12 | class CardContainerView: UIView { 13 | 14 | // MARK: Properties 15 | 16 | /// The contained buttons. 17 | private(set) var buttons = [SetCardButton]() 18 | 19 | /// The grid in charge of generating the calculated 20 | /// frame of each contained button. 21 | private(set) var grid = Grid(layout: Grid.Layout.aspectRatio(3/2)) 22 | 23 | /// The centered rect in which the buttons are going to be positioned. 24 | private var centeredRect: CGRect { 25 | get { 26 | return CGRect(x: bounds.size.width * 0.025, 27 | y: bounds.size.height * 0.025, 28 | width: bounds.size.width * 0.95, 29 | height: bounds.size.height * 0.95) 30 | } 31 | } 32 | 33 | // MARK: View life cycle 34 | 35 | override func layoutSubviews() { 36 | super.layoutSubviews() 37 | 38 | grid.frame = centeredRect 39 | 40 | for (i, button) in buttons.enumerated() { 41 | if let frame = grid[i] { 42 | button.frame = frame 43 | button.layer.cornerRadius = 10 44 | button.layer.borderColor = #colorLiteral(red: 0.501960814, green: 0.501960814, blue: 0.501960814, alpha: 1) 45 | button.layer.borderWidth = 0.5 46 | } 47 | } 48 | } 49 | 50 | // MARK: Imperatives 51 | 52 | /// Adds new buttons to the UI. 53 | /// - Parameter byAmount: The number of buttons to be added. 54 | func addCardButtons(byAmount numberOfButtons: Int = 3) { 55 | let cardButtons = (0..= numberOfCards else { return } 71 | 72 | for index in 0.. Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | func application(_ app: UIApplication, open inputURL: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool { 45 | // Ensure the URL is a file URL 46 | guard inputURL.isFileURL else { return false } 47 | 48 | // Reveal / import the document at the URL 49 | guard let documentBrowserViewController = window?.rootViewController as? ImageGalleryDocumentBrowserViewController else { return false } 50 | 51 | documentBrowserViewController.revealDocument(at: inputURL, importIfNeeded: true) { (revealedDocumentURL, error) in 52 | if let error = error { 53 | // Handle the error appropriately 54 | print("Failed to reveal the document at URL \(inputURL) with error: '\(error)'") 55 | return 56 | } 57 | 58 | // Present the Document View Controller for the revealed URL 59 | documentBrowserViewController.presentDocument(at: revealedDocumentURL!) 60 | } 61 | 62 | return true 63 | } 64 | 65 | 66 | } 67 | 68 | -------------------------------------------------------------------------------- /AnimatedSetGame/AnimatedSetGame/Views/General Views/CardButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardButton.swift 3 | // AnimatedSetGamee 4 | // 5 | // Created by Tiago Maia Lopes on 07/03/18. 6 | // Copyright © 2018 Tiago Maia Lopes. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class CardButton: UIButton { 12 | 13 | // MARK: Properties 14 | 15 | /// The default color for the card button when it's not selected or face down. 16 | var defaultBackgroundColor = UIColor.white.cgColor 17 | 18 | /// Tells if the button is face up or not, changing 19 | /// this property will flip the card. 20 | @IBInspectable var isFaceUp: Bool = true { 21 | didSet { 22 | if isFaceUp { 23 | layer.backgroundColor = defaultBackgroundColor 24 | } 25 | setNeedsDisplay() 26 | } 27 | } 28 | 29 | /// Tells if the button is active or not. Changing this 30 | /// property will change the alpha accordingly. 31 | var isActive: Bool = true { 32 | didSet { 33 | if isActive { 34 | alpha = 1 35 | } else { 36 | alpha = 0 37 | } 38 | } 39 | } 40 | 41 | /// Tells if the button is selected or not. 42 | @IBInspectable override var isSelected: Bool { 43 | didSet { 44 | if isSelected { 45 | layer.backgroundColor = #colorLiteral(red: 0.9764705896, green: 0.850980401, blue: 0.5490196347, alpha: 1).cgColor 46 | } else { 47 | layer.backgroundColor = defaultBackgroundColor 48 | } 49 | } 50 | } 51 | 52 | // MARK: Drawing 53 | 54 | override func draw(_ rect: CGRect) { 55 | layer.cornerRadius = 10 56 | layer.borderColor = #colorLiteral(red: 0.501960814, green: 0.501960814, blue: 0.501960814, alpha: 1) 57 | layer.borderWidth = 0.5 58 | 59 | if isFaceUp { 60 | drawFront() 61 | } else { 62 | drawBack() 63 | } 64 | } 65 | 66 | /// Draws the front of the card. 67 | func drawFront() {} 68 | 69 | /// Draws the back of the card. 70 | func drawBack() {} 71 | 72 | // MARK: Imperatives 73 | 74 | /// Flips the card. 75 | /// 76 | /// - Parameter animated: flips with a transition from left to right. 77 | /// - Paramater completion: completion block called after the end of the transition animation. 78 | func flipCard(animated: Bool = false, completion: Optional<(CardButton) -> ()> = nil) { 79 | if animated { 80 | UIView.transition(with: self, 81 | duration: 0.3, 82 | options: .transitionFlipFromLeft, 83 | animations: { 84 | self.isFaceUp = !self.isFaceUp 85 | }) { completed in 86 | if let completion = completion { 87 | completion(self) 88 | } 89 | } 90 | } else { 91 | self.isFaceUp = !self.isFaceUp 92 | } 93 | } 94 | 95 | /// Flips the card to face up only. 96 | /// 97 | /// - Parameter animated: flips with a transition from left to right. 98 | func turnFaceUp(animated: Bool = true) { 99 | if animated { 100 | UIView.transition(with: self, 101 | duration: 0.3, 102 | options: .transitionFlipFromLeft, 103 | animations: { 104 | self.isFaceUp = true 105 | }) 106 | } else { 107 | self.isFaceUp = true 108 | } 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /PersistentImageGallery/PersistentImageGallery/Controllers/ImageGalleryDocumentBrowserViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageGalleryDocumentBrowserViewController.swift 3 | // PersistentImageGallery 4 | // 5 | // Created by Tiago Maia Lopes on 03/04/18. 6 | // Copyright © 2018 Tiago Maia Lopes. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | 12 | class ImageGalleryDocumentBrowserViewController: UIDocumentBrowserViewController, UIDocumentBrowserViewControllerDelegate { 13 | 14 | // MARK: - Properties 15 | 16 | /// The template file's url. 17 | var templateURL: URL? 18 | 19 | // MARK: - Life cycle 20 | 21 | override func viewDidLoad() { 22 | super.viewDidLoad() 23 | 24 | delegate = self 25 | allowsPickingMultipleItems = false 26 | browserUserInterfaceStyle = .dark 27 | 28 | allowsDocumentCreation = false 29 | 30 | // Only allows the creation of documents when running on ipad devices. 31 | if UIDevice.current.userInterfaceIdiom == .pad { 32 | 33 | // Creates the template file: 34 | let fileManager = FileManager.default 35 | 36 | templateURL = try? fileManager.url( 37 | for: .applicationSupportDirectory, 38 | in: .userDomainMask, 39 | appropriateFor: nil, 40 | create: true 41 | ).appendingPathComponent("untitled.imagegallery") 42 | 43 | if let templateURL = templateURL { 44 | allowsDocumentCreation = fileManager.createFile(atPath: templateURL.path, contents: Data()) 45 | 46 | // Writes an empty image gallery into the template file: 47 | let emptyGallery = ImageGallery(images: [], title: "untitled") 48 | _ = try? JSONEncoder().encode(emptyGallery).write(to: templateURL) 49 | } 50 | } 51 | } 52 | 53 | // MARK: UIDocumentBrowserViewControllerDelegate 54 | 55 | func documentBrowser(_ controller: UIDocumentBrowserViewController, didRequestDocumentCreationWithHandler importHandler: @escaping (URL?, UIDocumentBrowserViewController.ImportMode) -> Void) { 56 | importHandler(templateURL, .copy) 57 | } 58 | 59 | func documentBrowser(_ controller: UIDocumentBrowserViewController, didPickDocumentURLs documentURLs: [URL]) { 60 | guard let sourceURL = documentURLs.first else { return } 61 | presentDocument(at: sourceURL) 62 | } 63 | 64 | func documentBrowser(_ controller: UIDocumentBrowserViewController, didImportDocumentAt sourceURL: URL, toDestinationURL destinationURL: URL) { 65 | presentDocument(at: destinationURL) 66 | } 67 | 68 | func documentBrowser(_ controller: UIDocumentBrowserViewController, failedToImportDocumentAt documentURL: URL, error: Error?) { 69 | presentWarningWith(title: "Error", message: "The document can't be opened") 70 | } 71 | 72 | // MARK: Document Presentation 73 | 74 | /// Presents the document stored at the provided url. 75 | func presentDocument(at documentURL: URL) { 76 | let storyBoard = UIStoryboard(name: "Main", bundle: nil) 77 | let navigationViewController = storyBoard.instantiateViewController(withIdentifier: "GalleryViewerNavigationController") 78 | let documentViewController = navigationViewController.contents as! GalleryDisplayCollectionViewController 79 | documentViewController.galleryDocument = ImageGalleryDocument(fileURL: documentURL) 80 | documentViewController.imageRequestManager = ImageRequestManager() 81 | 82 | present(navigationViewController, animated: true, completion: nil) 83 | } 84 | } 85 | 86 | -------------------------------------------------------------------------------- /AnimatedSetGame/AnimatedSetGame/Controllers/ConcentrationThemeChooserViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConcentrationThemeChooserViewController.swift 3 | // AnimatedSetGamee 4 | // 5 | // Created by Tiago Maia Lopes on 23/02/18. 6 | // Copyright © 2018 Tiago Maia Lopes. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ConcentrationThemeChooserViewController: UIViewController, UISplitViewControllerDelegate { 12 | 13 | // MARK: Properties 14 | 15 | /// Segue to show the concentration game view controller. 16 | private let concentrationSegueID = "show concentration" 17 | 18 | /// The array of all theme choosing buttons. 19 | @IBOutlet var themeButtons: [UIButton]! 20 | 21 | /// The SplitController's detail controller. 22 | /// - Note: If the user's device supports the split controller, 23 | /// it's possible to get it's detail controller and avoid the segue, 24 | /// preserving the game's current state. 25 | var splitDetailConcentrationController: ConcentrationViewController? { 26 | return (splitViewController?.viewControllers.last as? UINavigationController)?.visibleViewController as? ConcentrationViewController 27 | } 28 | 29 | /// The last segued view controller. 30 | /// - Note: Since the segue mechanism always creates a brand new controller, 31 | /// the controller is stored to preserve the concentration game's state. 32 | var lastSeguedToConcentrationController: ConcentrationViewController? 33 | 34 | // MARK: Life Cycle 35 | 36 | override func awakeFromNib() { 37 | splitViewController?.delegate = self 38 | } 39 | 40 | // MARK: Navigation 41 | 42 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 43 | if let concentrationVC = segue.destination as? ConcentrationViewController { 44 | if let tappedButton = sender as? UIButton { 45 | concentrationVC.pickedTheme = getPickedTheme(fromButton: tappedButton)! 46 | lastSeguedToConcentrationController = concentrationVC 47 | } 48 | } 49 | } 50 | 51 | // MARK: Imperatives 52 | 53 | /// Gets the theme associated with the passed button. 54 | func getPickedTheme(fromButton button: UIButton) -> ConcentrationViewController.Theme? { 55 | if let index = themeButtons.index(of: button) { 56 | return ConcentrationViewController.Theme(rawValue: index) 57 | } else { 58 | return nil 59 | } 60 | } 61 | 62 | // MARK: Actions 63 | 64 | /// Action method called when the user chooses a theme from one of the buttons. 65 | @IBAction func didTapThemeButton(_ sender: UIButton) { 66 | guard let theme = getPickedTheme(fromButton: sender) else { 67 | return 68 | } 69 | 70 | if let concentrationController = splitDetailConcentrationController { 71 | concentrationController.pickedTheme = theme 72 | } else if let storedConcentrationController = lastSeguedToConcentrationController { 73 | storedConcentrationController.pickedTheme = theme 74 | navigationController?.pushViewController(storedConcentrationController, animated: true) 75 | } else { 76 | performSegue(withIdentifier: concentrationSegueID, sender: sender) 77 | } 78 | } 79 | 80 | // MARK: UISplitViewController Delegate Methods 81 | 82 | func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool { 83 | if let concentrationController = secondaryViewController as? ConcentrationViewController { 84 | 85 | if concentrationController.pickedTheme == nil { 86 | return true 87 | } 88 | } 89 | 90 | return false 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /SetGame/SetGame/Models/SetCard.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SetCard.swift 3 | // SetGame 4 | // 5 | // Created by Tiago Maia Lopes on 1/23/18. 6 | // Copyright © 2018 Tiago Maia Lopes. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A card of a Set game. 12 | struct SetCard { 13 | 14 | // MARK: Properties 15 | 16 | /// The combined features that makes this card unique. 17 | private(set) var combination: FeatureCombination 18 | 19 | // MARK: Initializers 20 | 21 | init(combination: FeatureCombination) { 22 | self.combination = combination 23 | } 24 | } 25 | 26 | extension SetCard: Hashable { 27 | 28 | /// An Int based on each feature from the combination. 29 | var hashValue: Int { 30 | return Int("\(combination.number.rawValue)\(combination.color.rawValue)\(combination.symbol.rawValue)\(combination.shading.rawValue)")! 31 | } 32 | 33 | /// A card is equals to another if they have the same combination. 34 | static func ==(lhs: SetCard, rhs: SetCard) -> Bool { 35 | return lhs.combination == rhs.combination 36 | } 37 | } 38 | 39 | /// A possible feature combination. 40 | struct FeatureCombination { 41 | 42 | /// The number feature of the card. 43 | var number: Number = .none 44 | 45 | /// The color feature of the card. 46 | var color: Color = .none 47 | 48 | /// The symbol feature of the card. 49 | var symbol: Symbol = .none 50 | 51 | /// The shading feature of the card. 52 | var shading: Shading = .none 53 | 54 | /// Add a feature to the current combination instance. 55 | mutating func add(feature: Feature) { 56 | if feature is Number { 57 | 58 | number = feature as! Number 59 | 60 | } else if feature is Color { 61 | 62 | color = feature as! Color 63 | 64 | } else if feature is Symbol { 65 | 66 | symbol = feature as! Symbol 67 | 68 | } else if feature is Shading { 69 | 70 | shading = feature as! Shading 71 | } 72 | } 73 | } 74 | 75 | extension FeatureCombination: Equatable { 76 | 77 | /// A combination is equals to another if it's features are identical. 78 | static func ==(lhs: FeatureCombination, rhs: FeatureCombination) -> Bool { 79 | return lhs.number == rhs.number && 80 | lhs.color == rhs.color && 81 | lhs.symbol == rhs.symbol && 82 | lhs.shading == rhs.shading 83 | } 84 | } 85 | 86 | /// A card's feature. 87 | protocol Feature { 88 | 89 | /// The possible values of the current feature. 90 | static var values: [Feature] { get } 91 | 92 | /// Gets the next feature, in order, for the card creation mechanism. 93 | static func getNextFeatures() -> [Feature]? 94 | } 95 | 96 | /// The enum representing the possible 97 | /// Number feature values of a card in a set game. 98 | enum Number: Int, Feature { 99 | case one 100 | case two 101 | case three 102 | case none 103 | 104 | static var values: [Feature] { 105 | return [Number.one, Number.two, Number.three] 106 | } 107 | 108 | static func getNextFeatures() -> [Feature]? { 109 | return Color.values 110 | } 111 | } 112 | 113 | /// The enum representing the possible 114 | /// Color feature values of a card in a set game. 115 | enum Color: Int, Feature { 116 | case red 117 | case green 118 | case purple 119 | case none 120 | 121 | static var values: [Feature] { 122 | return [Color.red, Color.green, Color.purple] 123 | } 124 | 125 | static func getNextFeatures() -> [Feature]? { 126 | return Symbol.values 127 | } 128 | } 129 | 130 | /// The enum representing the possible 131 | /// Symbol feature values of a card in a set game. 132 | enum Symbol: Int, Feature { 133 | case squiggle 134 | case diamond 135 | case oval 136 | case none 137 | 138 | static var values: [Feature] { 139 | return [Symbol.squiggle, Symbol.diamond, Symbol.oval] 140 | } 141 | 142 | static func getNextFeatures() -> [Feature]? { 143 | return Shading.values 144 | } 145 | } 146 | 147 | /// The enum representing the possible 148 | /// Shading feature values of a card in a set game. 149 | enum Shading: Int, Feature { 150 | case solid 151 | case striped 152 | case outlined 153 | case none 154 | 155 | static var values: [Feature] { 156 | return [Shading.solid, Shading.striped, Shading.outlined] 157 | } 158 | 159 | static func getNextFeatures() -> [Feature]? { 160 | return nil 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /GraphicalSetGame/GraphicalSetGame/Models/SetCard.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SetCard.swift 3 | // SetGame 4 | // 5 | // Created by Tiago Maia Lopes on 1/23/18. 6 | // Copyright © 2018 Tiago Maia Lopes. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A card of a Set game. 12 | struct SetCard { 13 | 14 | // MARK: Properties 15 | 16 | /// The combined features that makes this card unique. 17 | private(set) var combination: FeatureCombination 18 | 19 | // MARK: Initializers 20 | 21 | init(combination: FeatureCombination) { 22 | self.combination = combination 23 | } 24 | } 25 | 26 | extension SetCard: Hashable { 27 | 28 | /// An Int based on each feature from the combination. 29 | var hashValue: Int { 30 | return Int("\(combination.number.rawValue)\(combination.color.rawValue)\(combination.symbol.rawValue)\(combination.shading.rawValue)")! 31 | } 32 | 33 | /// A card is equals to another if they have the same combination. 34 | static func ==(lhs: SetCard, rhs: SetCard) -> Bool { 35 | return lhs.combination == rhs.combination 36 | } 37 | } 38 | 39 | /// A possible feature combination. 40 | struct FeatureCombination { 41 | 42 | /// The number feature of the card. 43 | var number: Number = .none 44 | 45 | /// The color feature of the card. 46 | var color: Color = .none 47 | 48 | /// The symbol feature of the card. 49 | var symbol: Symbol = .none 50 | 51 | /// The shading feature of the card. 52 | var shading: Shading = .none 53 | 54 | /// Add a feature to the current combination instance. 55 | mutating func add(feature: Feature) { 56 | if feature is Number { 57 | 58 | number = feature as! Number 59 | 60 | } else if feature is Color { 61 | 62 | color = feature as! Color 63 | 64 | } else if feature is Symbol { 65 | 66 | symbol = feature as! Symbol 67 | 68 | } else if feature is Shading { 69 | 70 | shading = feature as! Shading 71 | } 72 | } 73 | } 74 | 75 | extension FeatureCombination: Equatable { 76 | 77 | /// A combination is equals to another if it's features are identical. 78 | static func ==(lhs: FeatureCombination, rhs: FeatureCombination) -> Bool { 79 | return lhs.number == rhs.number && 80 | lhs.color == rhs.color && 81 | lhs.symbol == rhs.symbol && 82 | lhs.shading == rhs.shading 83 | } 84 | } 85 | 86 | /// A card's feature. 87 | protocol Feature { 88 | 89 | /// The possible values of the current feature. 90 | static var values: [Feature] { get } 91 | 92 | /// Gets the next feature, in order, for the card creation mechanism. 93 | static func getNextFeatures() -> [Feature]? 94 | } 95 | 96 | /// The enum representing the possible 97 | /// Number feature values of a card in a set game. 98 | enum Number: Int, Feature { 99 | case one 100 | case two 101 | case three 102 | case none 103 | 104 | static var values: [Feature] { 105 | return [Number.one, Number.two, Number.three] 106 | } 107 | 108 | static func getNextFeatures() -> [Feature]? { 109 | return Color.values 110 | } 111 | } 112 | 113 | /// The enum representing the possible 114 | /// Color feature values of a card in a set game. 115 | enum Color: Int, Feature { 116 | case red 117 | case green 118 | case purple 119 | case none 120 | 121 | static var values: [Feature] { 122 | return [Color.red, Color.green, Color.purple] 123 | } 124 | 125 | static func getNextFeatures() -> [Feature]? { 126 | return Symbol.values 127 | } 128 | } 129 | 130 | /// The enum representing the possible 131 | /// Symbol feature values of a card in a set game. 132 | enum Symbol: Int, Feature { 133 | case squiggle 134 | case diamond 135 | case oval 136 | case none 137 | 138 | static var values: [Feature] { 139 | return [Symbol.squiggle, Symbol.diamond, Symbol.oval] 140 | } 141 | 142 | static func getNextFeatures() -> [Feature]? { 143 | return Shading.values 144 | } 145 | } 146 | 147 | /// The enum representing the possible 148 | /// Shading feature values of a card in a set game. 149 | enum Shading: Int, Feature { 150 | case solid 151 | case striped 152 | case outlined 153 | case none 154 | 155 | static var values: [Feature] { 156 | return [Shading.solid, Shading.striped, Shading.outlined] 157 | } 158 | 159 | static func getNextFeatures() -> [Feature]? { 160 | return nil 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /ImageGallery/ImageGallery/Stores/ImageGalleryStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageGalleryStore.swift 3 | // ImageGallery 4 | // 5 | // Created by Tiago Maia Lopes on 24/03/18. 6 | // Copyright © 2018 Tiago Maia Lopes. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// The class responsible for managing each gallery model. 12 | class ImageGalleryStore { 13 | 14 | /// The keys used to store the models within UserDefaults. 15 | private struct StorageKeys { 16 | static let galleries = "gallery" 17 | static let deletedGalleries = "deleted_galleries" 18 | } 19 | 20 | // MARK: - Properties 21 | 22 | /// The available image galleries. 23 | private(set) var galleries: [ImageGallery] { 24 | didSet { 25 | storeGalleries(galleries, at: StorageKeys.galleries) 26 | } 27 | } 28 | 29 | /// The deleted galleries. 30 | private(set) var deletedGalleries: [ImageGallery] { 31 | didSet { 32 | storeGalleries(deletedGalleries, at: StorageKeys.deletedGalleries) 33 | } 34 | } 35 | 36 | /// The store's user defaults instance. 37 | private let userDefaults = UserDefaults.standard 38 | 39 | // MARK: - Initializer 40 | 41 | init() { 42 | galleries = [] 43 | deletedGalleries = [] 44 | 45 | if let storedGalleries = getGalleriesBy(key: StorageKeys.galleries) { 46 | galleries = storedGalleries 47 | } 48 | 49 | if let storedDeletedGalleries = getGalleriesBy(key: StorageKeys.deletedGalleries) { 50 | deletedGalleries = storedDeletedGalleries 51 | } 52 | 53 | if galleries.isEmpty { 54 | addNewGallery() 55 | } 56 | } 57 | 58 | // MARK: - Imperatives 59 | 60 | /// Tries to access and retrieve the galleries stored at 61 | /// the specified key in the user defaults. 62 | private func getGalleriesBy(key: String) -> [ImageGallery]? { 63 | if let galleriesData = userDefaults.value(forKey: key) as? Data { 64 | if let galleries = try? JSONDecoder().decode([ImageGallery].self, from: galleriesData) { 65 | return galleries 66 | } 67 | } 68 | 69 | return nil 70 | } 71 | 72 | /// Tries to store the passed galleries at the key. 73 | private func storeGalleries(_ gallery: [ImageGallery], at key: String) { 74 | do { 75 | try? userDefaults.setValue( 76 | JSONEncoder().encode(gallery), 77 | forKey: key 78 | ) 79 | userDefaults.synchronize() 80 | } 81 | } 82 | 83 | /// Adds a new gallery into the store. 84 | func addNewGallery() { 85 | galleries.insert(makeGallery(), at: 0) 86 | } 87 | 88 | private func makeGallery() -> ImageGallery { 89 | let galleryNames = (galleries + deletedGalleries).map { gallery in 90 | return gallery.title 91 | } 92 | return ImageGallery( 93 | images: [], 94 | title: "Empty".madeUnique(withRespectTo: galleryNames) 95 | ) 96 | } 97 | 98 | /// Updates the passed gallery into the store. 99 | func updateGallery(_ gallery: ImageGallery) { 100 | if let galleryIndex = galleries.index(of: gallery) { 101 | galleries[galleryIndex] = gallery 102 | storeGalleries(galleries, at: StorageKeys.galleries) 103 | 104 | NotificationCenter.default.post( 105 | name: Notification.Name.galleryUpdated, 106 | object: self, 107 | userInfo: [Notification.Name.galleryUpdated : gallery] 108 | ) 109 | } 110 | } 111 | 112 | /// Removes the gallery from the stored ones. 113 | /// If the passed gallery is already deleted, 114 | /// it's permanently removed from the store. 115 | func removeGallery(_ gallery: ImageGallery) { 116 | if let galleryIndex = galleries.index(of: gallery) { 117 | deletedGalleries.append(galleries.remove(at: galleryIndex)) 118 | if galleries.isEmpty { 119 | addNewGallery() 120 | } 121 | 122 | NotificationCenter.default.post( 123 | name: Notification.Name.galleryDeleted, 124 | object: self, 125 | userInfo: [Notification.Name.galleryDeleted : gallery] 126 | ) 127 | } else if let deletedGalleryIndex = deletedGalleries.index(of: gallery) { 128 | deletedGalleries.remove(at: deletedGalleryIndex) 129 | } 130 | } 131 | 132 | /// Recovers the passed gallery, if it's a deleted one. 133 | func recoverGallery(_ gallery: ImageGallery) { 134 | if let deletedIndex = deletedGalleries.index(of: gallery) { 135 | galleries.append(deletedGalleries.remove(at: deletedIndex)) 136 | } 137 | } 138 | 139 | } 140 | 141 | extension Notification.Name { 142 | static let galleryUpdated = Notification.Name(rawValue: "galleryUpdated") 143 | static let galleryDeleted = Notification.Name(rawValue: "galleryDeleted") 144 | } 145 | -------------------------------------------------------------------------------- /AnimatedSetGame/AnimatedSetGame/Views/Concentration/ConcentrationCardsContainerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConcentrationCardsContainerView.swift 3 | // AnimatedSetGamee 4 | // 5 | // Created by Tiago Maia Lopes on 08/03/18. 6 | // Copyright © 2018 Tiago Maia Lopes. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @IBDesignable 12 | class ConcentrationCardsContainerView: CardsContainerView { 13 | 14 | // MARK: Properties 15 | 16 | override var buttonsToPosition: [CardButton] { 17 | return buttons.filter({ $0.isActive }) 18 | } 19 | 20 | // MARK: Initializer 21 | 22 | override func awakeFromNib() { 23 | super.awakeFromNib() 24 | 25 | let discardToOrigin = convert(CGPoint(x: UIScreen.main.bounds.width, 26 | y: UIScreen.main.bounds.height / 2), 27 | to: self) 28 | discardToFrame = CGRect(origin: discardToOrigin, 29 | size: CGSize(width: 80, 30 | height: 120)) 31 | 32 | let dealFromOrigin = convert(CGPoint(x: 0, 33 | y: UIScreen.main.bounds.height), 34 | to: self) 35 | dealingFromFrame = CGRect(origin: dealFromOrigin, 36 | size: CGSize(width: 80, 37 | height: 120)) 38 | } 39 | 40 | override func prepareForInterfaceBuilder() { 41 | super.prepareForInterfaceBuilder() 42 | 43 | addButtons(byAmount: numberOfButtonsForDisplay) 44 | 45 | respositionViews() 46 | 47 | for button in buttons { 48 | button.isActive = true 49 | button.isFaceUp = false 50 | button.setNeedsDisplay() 51 | } 52 | } 53 | 54 | // MARK: Imperatives 55 | 56 | /// Instantiates an array with the right amount of 57 | /// concentration cards buttons. 58 | override func makeButtons(byAmount numberOfButtons: Int) -> [CardButton] { 59 | return (0.. ()>) { 138 | let inactiveButtons = buttons.filter { !$0.isActive } 139 | guard inactiveButtons.count > 0 else { return } 140 | 141 | grid.cellCount = buttons.filter({ $0.isActive }).count 142 | updateViewsFrames(withAnimation: true, andCompletion: completion) 143 | } 144 | 145 | } 146 | -------------------------------------------------------------------------------- /AnimatedSetGame/AnimatedSetGame/Views/Set/SetCardsContainerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardContainerView.swift 3 | // GraphicalSetGamee 4 | // 5 | // Created by Tiago Maia Lopes on 09/02/18. 6 | // Copyright © 2018 Tiago Maia Lopes. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// The view responsible for holding and displaying a grid of cardButtons. 12 | @IBDesignable 13 | class SetCardsContainerView: CardsContainerView { 14 | 15 | // MARK: View life cycle 16 | 17 | override func layoutSubviews() { 18 | super.layoutSubviews() 19 | 20 | // Only updates the buttons frames if the centered rect has changed, 21 | // This will occur when orientation changes. 22 | // This check will prevent frame changes while 23 | // the animator is doing it's job. 24 | if grid.frame != gridRect { 25 | updateViewsFrames() 26 | } 27 | } 28 | 29 | /// Draws the card buttons for storyboard display. 30 | /// - Note: The button's properties are randomly generated. 31 | override func prepareForInterfaceBuilder() { 32 | super.prepareForInterfaceBuilder() 33 | 34 | if numberOfButtonsForDisplay > 0 { 35 | addButtons(byAmount: numberOfButtonsForDisplay) 36 | 37 | respositionViews() 38 | 39 | for button: SetCardButton in buttons as! [SetCardButton] { 40 | button.alpha = 1 41 | button.isFaceUp = true 42 | 43 | button.symbolShape = SetCardButton.CardSymbolShape.randomized() 44 | button.color = SetCardButton.CardColor.randomized() 45 | button.symbolShading = SetCardButton.CardSymbolShading.randomized() 46 | button.numberOfSymbols = 4.arc4random 47 | 48 | if (button.numberOfSymbols == 0 || button.numberOfSymbols > 3) { 49 | button.numberOfSymbols = 1 50 | } 51 | 52 | button.setNeedsDisplay() 53 | } 54 | } 55 | } 56 | 57 | // MARK: Imperatives 58 | 59 | override func makeButtons(byAmount numberOfButtons: Int) -> [CardButton] { 60 | return (0.. setGame.tableCards.count { 56 | cardsContainerView.removeCardButtons(byAmount: cardsContainerView.buttons.count - setGame.tableCards.count) 57 | } 58 | 59 | for (index, cardButton) in cardsContainerView.buttons.enumerated() { 60 | let currentCard = setGame.tableCards[index] 61 | 62 | // Color feature: 63 | switch currentCard.combination.color { 64 | case .green: 65 | cardButton.color = .green 66 | case .purple: 67 | cardButton.color = .purple 68 | case .red: 69 | cardButton.color = .red 70 | default: 71 | break 72 | } 73 | 74 | // Number feature: 75 | switch currentCard.combination.number { 76 | case .one: 77 | cardButton.numberOfSymbols = 1 78 | case .two: 79 | cardButton.numberOfSymbols = 2 80 | case .three: 81 | cardButton.numberOfSymbols = 3 82 | default: 83 | break 84 | } 85 | 86 | // Symbol feature: 87 | switch currentCard.combination.symbol { 88 | case .diamond: 89 | cardButton.symbolShape = .diamond 90 | case .squiggle: 91 | cardButton.symbolShape = .squiggle 92 | case .oval: 93 | cardButton.symbolShape = .oval 94 | default: 95 | break 96 | } 97 | 98 | // Shading feature: 99 | switch currentCard.combination.shading { 100 | case .outlined: 101 | cardButton.symbolShading = .outlined 102 | case .solid: 103 | cardButton.symbolShading = .solid 104 | case .striped: 105 | cardButton.symbolShading = .striped 106 | default: 107 | break 108 | } 109 | 110 | // Selection: 111 | if setGame.selectedCards.contains(currentCard) || 112 | setGame.matchedCards.contains(currentCard) { 113 | cardButton.layer.backgroundColor = #colorLiteral(red: 0.9764705896, green: 0.850980401, blue: 0.5490196347, alpha: 1) 114 | } else { 115 | cardButton.layer.backgroundColor = #colorLiteral(red: 0.9999960065, green: 1, blue: 1, alpha: 0.849352542) 116 | } 117 | 118 | } 119 | 120 | scoreLabel.text = "Score: \(setGame.score)" 121 | matchedTriosLabel.text = "Matches: \(setGame.matchedDeck.count)" 122 | 123 | handleDealMoreButton() 124 | } 125 | 126 | /// Checks if it's possible to deal more cards and 127 | /// enables or disables the deal more button accordingly. 128 | private func handleDealMoreButton() { 129 | dealMoreButton.isEnabled = setGame.deck.count > 3 130 | } 131 | 132 | // MARK: Actions 133 | 134 | /// Selects the chosen card. 135 | @objc func didTapCard(_ sender: UIButton) { 136 | let index = cardsContainerView.buttons.index(of: sender as! SetCardButton)! 137 | setGame.selectCard(at: index) 138 | 139 | displayCards() 140 | } 141 | 142 | // Adds more cards to the UI. 143 | @IBAction func didTapDealMore(_ sender: UIButton) { 144 | if setGame.matchedCards.count > 0 { 145 | setGame.replaceMatchedCards() 146 | } 147 | 148 | setGame.dealCards() 149 | cardsContainerView.addCardButtons() 150 | assignTargetAction() 151 | 152 | displayCards() 153 | } 154 | 155 | /// Restarts the current game. 156 | @IBAction func didTapNewGame(_ sender: UIButton) { 157 | setGame.reset() 158 | 159 | setGame.dealCards(forAmount: 12) 160 | cardsContainerView.clearCardContainer() 161 | cardsContainerView.addCardButtons(byAmount: 12) 162 | assignTargetAction() 163 | 164 | displayCards() 165 | } 166 | 167 | /// Deals more cards. 168 | @IBAction func didSwipeDown(_ sender: UISwipeGestureRecognizer) { 169 | didTapDealMore(dealMoreButton) 170 | } 171 | 172 | /// Reorder the table 173 | @IBAction func didRotate(_ sender: UIRotationGestureRecognizer) { 174 | if sender.state == .began { 175 | setGame.shuffleTableCards() 176 | displayCards() 177 | } 178 | } 179 | } 180 | 181 | -------------------------------------------------------------------------------- /AnimatedSetGame/AnimatedSetGame/Models/Set/SetCard.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SetCard.swift 3 | // SetGame 4 | // 5 | // Created by Tiago Maia Lopes on 1/23/18. 6 | // Copyright © 2018 Tiago Maia Lopes. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A card of a Set game. 12 | struct SetCard: CustomStringConvertible { 13 | 14 | // MARK: Properties 15 | 16 | /// The combined features that makes this card unique. 17 | private(set) var combination: FeatureCombination 18 | 19 | var description: String { 20 | var representation = "" 21 | 22 | for _ in 0...combination.number.rawValue { 23 | representation += combination.symbol.description 24 | } 25 | 26 | representation += " \(combination.color.description), \(combination.shading.description)" 27 | 28 | return representation 29 | } 30 | 31 | // MARK: Initializers 32 | 33 | init(combination: FeatureCombination) { 34 | self.combination = combination 35 | } 36 | } 37 | 38 | extension SetCard: Hashable { 39 | 40 | /// An Int based on each feature from the combination. 41 | var hashValue: Int { 42 | return Int("\(combination.number.rawValue)\(combination.color.rawValue)\(combination.symbol.rawValue)\(combination.shading.rawValue)")! 43 | } 44 | 45 | /// A card is equals to another if they have the same combination. 46 | static func ==(lhs: SetCard, rhs: SetCard) -> Bool { 47 | return lhs.combination == rhs.combination 48 | } 49 | } 50 | 51 | /// A possible feature combination. 52 | struct FeatureCombination { 53 | 54 | /// The number feature of the card. 55 | var number: Number = .none 56 | 57 | /// The color feature of the card. 58 | var color: Color = .none 59 | 60 | /// The symbol feature of the card. 61 | var symbol: Symbol = .none 62 | 63 | /// The shading feature of the card. 64 | var shading: Shading = .none 65 | 66 | /// Add a feature to the current combination instance. 67 | mutating func add(feature: Feature) { 68 | if feature is Number { 69 | 70 | number = feature as! Number 71 | 72 | } else if feature is Color { 73 | 74 | color = feature as! Color 75 | 76 | } else if feature is Symbol { 77 | 78 | symbol = feature as! Symbol 79 | 80 | } else if feature is Shading { 81 | 82 | shading = feature as! Shading 83 | } 84 | } 85 | } 86 | 87 | extension FeatureCombination: Equatable { 88 | 89 | /// A combination is equals to another if it's features are identical. 90 | static func ==(lhs: FeatureCombination, rhs: FeatureCombination) -> Bool { 91 | return lhs.number == rhs.number && 92 | lhs.color == rhs.color && 93 | lhs.symbol == rhs.symbol && 94 | lhs.shading == rhs.shading 95 | } 96 | } 97 | 98 | /// A card's feature. 99 | protocol Feature { 100 | 101 | /// The possible values of the current feature. 102 | static var values: [Feature] { get } 103 | 104 | /// Gets the next feature, in order, for the card creation mechanism. 105 | static func getNextFeatures() -> [Feature]? 106 | } 107 | 108 | /// The enum representing the possible 109 | /// Number feature values of a card in a set game. 110 | enum Number: Int, Feature { 111 | case one 112 | case two 113 | case three 114 | case none 115 | 116 | static var values: [Feature] { 117 | return [Number.one, Number.two, Number.three] 118 | } 119 | 120 | static func getNextFeatures() -> [Feature]? { 121 | return Color.values 122 | } 123 | } 124 | 125 | /// The enum representing the possible 126 | /// Color feature values of a card in a set game. 127 | enum Color: Int, Feature, CustomStringConvertible { 128 | case red 129 | case green 130 | case purple 131 | case none 132 | 133 | var description: String { 134 | var colorText = "" 135 | 136 | switch self { 137 | case .red: 138 | colorText = "red" 139 | case .purple: 140 | colorText = "purple" 141 | case .green: 142 | colorText = "green" 143 | default: 144 | break 145 | } 146 | 147 | return colorText 148 | } 149 | 150 | static var values: [Feature] { 151 | return [Color.red, Color.green, Color.purple] 152 | } 153 | 154 | static func getNextFeatures() -> [Feature]? { 155 | return Symbol.values 156 | } 157 | } 158 | 159 | /// The enum representing the possible 160 | /// Symbol feature values of a card in a set game. 161 | enum Symbol: Int, Feature, CustomStringConvertible { 162 | case squiggle 163 | case diamond 164 | case oval 165 | case none 166 | 167 | var description: String { 168 | var symbolText = "" 169 | 170 | switch self { 171 | case .diamond: 172 | symbolText = "▲" 173 | case .oval: 174 | symbolText = "●" 175 | case .squiggle: 176 | symbolText = "■" 177 | default: 178 | break 179 | } 180 | 181 | return symbolText 182 | } 183 | 184 | static var values: [Feature] { 185 | return [Symbol.squiggle, Symbol.diamond, Symbol.oval] 186 | } 187 | 188 | static func getNextFeatures() -> [Feature]? { 189 | return Shading.values 190 | } 191 | } 192 | 193 | /// The enum representing the possible 194 | /// Shading feature values of a card in a set game. 195 | enum Shading: Int, Feature, CustomStringConvertible { 196 | case solid 197 | case striped 198 | case outlined 199 | case none 200 | 201 | var description: String { 202 | var shadingText = "" 203 | 204 | switch self { 205 | case .solid: 206 | shadingText = "solid" 207 | case .striped: 208 | shadingText = "striped" 209 | case .outlined: 210 | shadingText = "outlined" 211 | default: 212 | break 213 | } 214 | 215 | return shadingText 216 | } 217 | 218 | static var values: [Feature] { 219 | return [Shading.solid, Shading.striped, Shading.outlined] 220 | } 221 | 222 | static func getNextFeatures() -> [Feature]? { 223 | return nil 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /SetGame/SetGame/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // SetGame 4 | // 5 | // Created by Tiago Maia Lopes on 1/23/18. 6 | // Copyright © 2018 Tiago Maia Lopes. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | // MARK: Properties 14 | 15 | /// The main set game. 16 | private var setGame = SetGame() 17 | 18 | /// The card buttons being displayed in the UI. 19 | @IBOutlet var cardButtons: [UIButton]! { 20 | didSet { 21 | _ = setGame.dealCards(forAmount: 12) 22 | } 23 | } 24 | 25 | /// The UI score label. 26 | @IBOutlet weak var scoreLabel: UILabel! 27 | 28 | /// The label containing the number of metched trios. 29 | @IBOutlet weak var matchedTriosLabel: UILabel! 30 | 31 | /// The deal more button in the UI. 32 | @IBOutlet weak var dealMoreButton: UIButton! 33 | 34 | /// The mapping between a symbol card feature and it's 35 | /// corresponding displayable char. 36 | private let symbolToText: [Symbol : String] = [ 37 | .squiggle : "■", 38 | .diamond : "▲", 39 | .oval : "●" 40 | ] 41 | 42 | /// The mapping between a color card feature and it's 43 | /// corresponding literal displayable UIColor. 44 | private let colorFeatureToColor: [Color : UIColor] = [ 45 | .red : #colorLiteral(red: 0.9254902005, green: 0.2352941185, blue: 0.1019607857, alpha: 1), 46 | .green : #colorLiteral(red: 0.4666666687, green: 0.7647058964, blue: 0.2666666806, alpha: 1), 47 | .purple : #colorLiteral(red: 0.3647058904, green: 0.06666667014, blue: 0.9686274529, alpha: 1) 48 | ] 49 | 50 | // MARK: Life cycle 51 | 52 | override func viewWillAppear(_ animated: Bool) { 53 | super.viewWillAppear(animated) 54 | displayCards() 55 | } 56 | 57 | // MARK: Imperatives 58 | 59 | /// Displays each card dealt by the setGame. 60 | /// Method in chard of keeping the UI in sync with the model. 61 | private func displayCards() { 62 | 63 | // Resets all cards to its original state. 64 | for cardButton in cardButtons { 65 | cardButton.alpha = 0 66 | cardButton.setAttributedTitle(nil, for: .normal) 67 | cardButton.setTitle(nil, for: .normal) 68 | } 69 | 70 | // Begins displaying each card. 71 | setGame.tableCards.enumerated().forEach { [unowned self] (index, card) in 72 | let cardButton = self.cardButtons[index] 73 | 74 | if let card = card { 75 | cardButton.alpha = 1 76 | cardButton.setAttributedTitle(self.getAttributedText(forCard: card)!, for: .normal) 77 | 78 | // If the card is selected, display borders to it. 79 | if self.setGame.selectedCards.contains(card) { 80 | cardButton.layer.borderWidth = 3 81 | cardButton.layer.borderColor = UIColor.blue.cgColor 82 | cardButton.layer.cornerRadius = 8 83 | } else { 84 | cardButton.layer.borderWidth = 0 85 | cardButton.layer.cornerRadius = 0 86 | } 87 | 88 | // Highlights the matched cards 89 | if self.setGame.matchedCards.contains(card) { 90 | cardButton.backgroundColor = #colorLiteral(red: 0.9764705896, green: 0.850980401, blue: 0.5490196347, alpha: 1) 91 | } else { 92 | cardButton.backgroundColor = #colorLiteral(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0) 93 | } 94 | 95 | } else { 96 | // Card was matched, hide the associated button for now. 97 | cardButton.alpha = 0 98 | } 99 | } 100 | 101 | scoreLabel.text = "Score: \(setGame.score)" 102 | matchedTriosLabel.text = "Matches: \(setGame.matchedDeck.count)" 103 | handleDealMoreButton() 104 | } 105 | 106 | /// Returns the configured attributed text for the given card, 107 | /// configured based on the card features. 108 | private func getAttributedText(forCard card: SetCard) -> NSAttributedString? { 109 | guard card.combination.number != .none else { return nil } 110 | guard card.combination.symbol != .none else { return nil } 111 | guard card.combination.color != .none else { return nil } 112 | guard card.combination.shading != .none else { return nil } 113 | 114 | let number = card.combination.number 115 | let symbol = card.combination.symbol 116 | let color = card.combination.color 117 | let shading = card.combination.shading 118 | 119 | // Checks if a symbol has an associated char. 120 | if let symbolChar = symbolToText[symbol] { 121 | // Creates the symbol text according to the number feature. 122 | let cardText = String(repeating: symbolChar, count: number.rawValue + 1) 123 | var attributes = [NSAttributedStringKey : Any]() 124 | // Gets the associated color from the card color feature. 125 | let cardColor = colorFeatureToColor[color]! 126 | 127 | // Adds the given attribute for one of the shading values. 128 | switch shading { 129 | case .outlined: 130 | attributes[NSAttributedStringKey.strokeWidth] = 10 131 | fallthrough 132 | case .solid: 133 | attributes[NSAttributedStringKey.foregroundColor] = cardColor 134 | case .striped: 135 | attributes[NSAttributedStringKey.foregroundColor] = cardColor.withAlphaComponent(0.3) 136 | default: 137 | break 138 | } 139 | 140 | let attributedText = NSAttributedString(string: cardText, 141 | attributes: attributes) 142 | return attributedText 143 | } else { 144 | return nil 145 | } 146 | } 147 | 148 | /// Checks if it's possible to deal more cards and 149 | /// enables or disables the deal more button accordingly. 150 | private func handleDealMoreButton() { 151 | if setGame.deck.count > 3, 152 | setGame.tableCards.count < cardButtons.count || setGame.matchedCards.count > 0 { 153 | dealMoreButton.isEnabled = true 154 | } else { 155 | dealMoreButton.isEnabled = false 156 | } 157 | } 158 | 159 | // MARK: Actions 160 | 161 | /// Selects the chosen card. 162 | @IBAction func didTapCard(_ sender: UIButton) { 163 | guard let index = cardButtons.index(of: sender) else { return } 164 | guard let _ = setGame.tableCards[index] else { return } 165 | 166 | setGame.selectCard(at: index) 167 | 168 | displayCards() 169 | } 170 | 171 | // Adds more cards to the UI. 172 | @IBAction func didTapDealMore(_ sender: UIButton) { 173 | if setGame.matchedCards.count > 0 { 174 | setGame.removeMatchedCardsFromTable() 175 | } 176 | _ = setGame.dealCards() 177 | displayCards() 178 | } 179 | 180 | /// Restarts the current game. 181 | @IBAction func didTapNewGame(_ sender: UIButton) { 182 | setGame.reset() 183 | _ = setGame.dealCards(forAmount: 12) 184 | displayCards() 185 | } 186 | } 187 | 188 | -------------------------------------------------------------------------------- /Concentration/Concentration/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Concentration 4 | // 5 | // Created by Tiago Maia Lopes on 1/16/18. 6 | // Copyright © 2018 Tiago Maia Lopes. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | // The following Theme code is part of one of the extra credit tasks. 12 | // MARK: Extra credit 1 13 | // ------------------------- 14 | 15 | typealias Emojis = [String] 16 | 17 | /// Enum representing all the possible card themes. 18 | enum Theme: Int { 19 | 20 | case Flags, Faces, Sports, Animals, Fruits, Appliances 21 | 22 | /// The color of the back of the card 23 | var cardColor: UIColor { 24 | switch self { 25 | case .Flags: 26 | return #colorLiteral(red: 0.8039215803, green: 0.8039215803, blue: 0.8039215803, alpha: 1) 27 | 28 | case .Faces: 29 | return #colorLiteral(red: 1, green: 0.5763723254, blue: 0, alpha: 1) 30 | 31 | case .Sports: 32 | return #colorLiteral(red: 0.4745098054, green: 0.8392156959, blue: 0.9764705896, alpha: 1) 33 | 34 | case .Animals: 35 | return #colorLiteral(red: 0.9254902005, green: 0.2352941185, blue: 0.1019607857, alpha: 1) 36 | 37 | case .Fruits: 38 | return #colorLiteral(red: 0.4666666687, green: 0.7647058964, blue: 0.2666666806, alpha: 1) 39 | 40 | case .Appliances: 41 | return #colorLiteral(red: 0.3647058904, green: 0.06666667014, blue: 0.9686274529, alpha: 1) 42 | } 43 | } 44 | 45 | /// The color of the background view 46 | var backgroundColor: UIColor { 47 | switch self { 48 | case .Flags: 49 | return #colorLiteral(red: 0.2549019754, green: 0.2745098174, blue: 0.3019607961, alpha: 1) 50 | 51 | case .Faces: 52 | return #colorLiteral(red: 0.9764705896, green: 0.850980401, blue: 0.5490196347, alpha: 1) 53 | 54 | case .Sports: 55 | return #colorLiteral(red: 0.05882352963, green: 0.180392161, blue: 0.2470588237, alpha: 1) 56 | 57 | case .Animals: 58 | return #colorLiteral(red: 0.9568627477, green: 0.6588235497, blue: 0.5450980663, alpha: 1) 59 | 60 | case .Fruits: 61 | return #colorLiteral(red: 0.721568644, green: 0.8862745166, blue: 0.5921568871, alpha: 1) 62 | 63 | case .Appliances: 64 | return #colorLiteral(red: 0.09019608051, green: 0, blue: 0.3019607961, alpha: 1) 65 | } 66 | } 67 | 68 | /// The emojis used by this theme 69 | var emojis: Emojis { 70 | switch self { 71 | case .Flags: 72 | return ["🇧🇷", "🇧🇪", "🇯🇵", "🇨🇦", "🇺🇸", "🇵🇪", "🇮🇪", "🇦🇷"] 73 | 74 | case .Faces: 75 | return ["😀", "🙄", "😡", "🤢", "🤡", "😱", "😍", "🤠"] 76 | 77 | case .Sports: 78 | return ["🏌️", "🤼‍♂️", "🥋", "🏹", "🥊", "🏊", "🤾🏿‍♂️", "🏇🏿"] 79 | 80 | case .Animals: 81 | return ["🦊", "🐼", "🦁", "🐘", "🐓", "🦀", "🐷", "🦉"] 82 | 83 | case .Fruits: 84 | return ["🥑", "🍍", "🍆", "🍠", "🍉", "🍇", "🥝", "🍒"] 85 | 86 | case .Appliances: 87 | return ["💻", "🖥", "⌚️", "☎️", "🖨", "🖱", "📱", "⌨️"] 88 | } 89 | } 90 | 91 | /// The count of possible themes. 92 | static var count: Int { 93 | return Theme.Appliances.rawValue + 1 94 | } 95 | 96 | static func getRandom() -> Theme { 97 | return Theme(rawValue: Theme.count.arc4random)! 98 | } 99 | 100 | } 101 | // ------------------------- 102 | // ------------------------- 103 | 104 | class ViewController: UIViewController { 105 | 106 | // MARK: Properties 107 | 108 | /// The cards presented in the UI. 109 | @IBOutlet var cardButtons: [UIButton]! 110 | 111 | /// The UI label indicating the amount of flips. 112 | @IBOutlet weak var flipsLabel: UILabel! 113 | 114 | /// The UI label indicating the player's score. 115 | @IBOutlet weak var scoreLabel: UILabel! 116 | 117 | /// The model encapsulating the concentration game's logic. 118 | private lazy var concentration = Concentration(numberOfPairs: (cardButtons.count / 2)) 119 | 120 | /// The randomly picked theme. 121 | /// The theme is chosen every time a new game starts. 122 | private var pickedTheme: Theme! 123 | 124 | // MARK: Life cycle 125 | 126 | override func viewDidLoad() { 127 | super.viewDidLoad() 128 | chooseRandomTheme() 129 | } 130 | 131 | // MARK: Actions 132 | 133 | /// Action fired when a card button is tapped. 134 | /// It flips a card checks if there's a match or not. 135 | @IBAction func didTapCard(_ sender: UIButton) { 136 | guard let index = cardButtons.index(of: sender) else { return } 137 | 138 | concentration.flipCard(at: index) 139 | 140 | displayCards() 141 | displayLabels() 142 | } 143 | 144 | /// Action fired when the new game button is tapped. 145 | /// It resets the current game and refreshes the UI. 146 | @IBAction func didTapNewGame(_ sender: UIButton) { 147 | chooseRandomTheme() 148 | concentration.resetGame() 149 | displayCards() 150 | displayLabels() 151 | } 152 | 153 | // MARK: Imperatives 154 | 155 | /// The map between a card and the emoji used with it. 156 | /// This is the dictionary responsible for mapping 157 | /// which emoji is going to be displayed by which card. 158 | private var cardsAndEmojisMap = [Card : String]() 159 | 160 | /// Method used to randomly choose the game's theme. 161 | private func chooseRandomTheme() { 162 | pickedTheme = Theme.getRandom() 163 | view.backgroundColor = pickedTheme.backgroundColor 164 | 165 | cardsAndEmojisMap = [:] 166 | var emojis = pickedTheme.emojis 167 | 168 | for card in concentration.cards { 169 | if cardsAndEmojisMap[card] == nil { 170 | cardsAndEmojisMap[card] = emojis.remove(at: emojis.count.arc4random) 171 | } 172 | } 173 | 174 | displayCards() 175 | } 176 | 177 | /// Method used to refresh the scores and flips UI labels. 178 | private func displayLabels() { 179 | flipsLabel.text = "Flips: \(concentration.flipsCount)" 180 | scoreLabel.text = "Score: \(concentration.score)" 181 | } 182 | 183 | /// Method in charge of displaying each card's state 184 | /// with the assciated card button. 185 | private func displayCards() { 186 | for (index, cardButton) in cardButtons.enumerated() { 187 | guard concentration.cards.indices.contains(index) else { continue } 188 | 189 | let card = concentration.cards[index] 190 | 191 | if card.isFaceUp { 192 | cardButton.setTitle(cardsAndEmojisMap[card], for: .normal) 193 | cardButton.backgroundColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1) 194 | } else { 195 | cardButton.setTitle("", for: .normal) 196 | cardButton.backgroundColor = card.isMatched ? #colorLiteral(red: 1, green: 1, blue: 1, alpha: 0) : pickedTheme.cardColor 197 | } 198 | } 199 | } 200 | 201 | } 202 | 203 | -------------------------------------------------------------------------------- /Concentration/Concentration/Concentration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Concentration.swift 3 | // Concentration 4 | // 5 | // Created by Tiago Maia Lopes on 1/17/18. 6 | // Copyright © 2018 Tiago Maia Lopes. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import GameplayKit 11 | 12 | /// The concentration's delegate 13 | protocol ConcentrationDelegate { 14 | 15 | /// Called after a match happens. 16 | func didMatchCards(withIndices indices: [Int]) 17 | } 18 | 19 | class Concentration { 20 | 21 | // MARK: Properties 22 | 23 | /// The cards used in the game. 24 | private(set) var cards = [Card]() 25 | 26 | /// The only flipped card index. Used to track 27 | /// the first chosen card of a pair. 28 | /// 29 | /// This variable is used to verify if 30 | /// it's time to check for a match or not. 31 | private var oneAndOnlyFlippedCardIndex: Int? { 32 | get { 33 | return cards.indices.filter { cards[$0].isFaceUp }.oneAndOnly 34 | } 35 | set { 36 | // Turns down any faced up pair. 37 | setCurrentPairToFaceDown() 38 | 39 | // Starts to compute the time the player is 40 | // taking to flip the next card. 41 | scoringDate = Date() 42 | } 43 | } 44 | 45 | /// The number of times the player flipped a card. 46 | private(set) var flipsCount = 0 47 | 48 | /// The date used to give a higher score to the player. 49 | /// It's set when the first card is flipped, and depending on the 50 | /// time taken for a match, we give a higher score or not. 51 | private var scoringDate: Date? 52 | 53 | /// The player's score. 54 | private(set) var score = 0 55 | 56 | /// The index of each card in the currently faced up pair. 57 | private var currentPairIndices: [Int]? 58 | 59 | /// The game's delegate 60 | var delegate: ConcentrationDelegate? 61 | 62 | // MARK: Initialization 63 | 64 | /// Prepares all the needed cards based 65 | /// on the passed amount of pairs. 66 | init(numberOfPairs: Int) { 67 | setPairs(withCount: numberOfPairs) 68 | } 69 | 70 | // MARK: Imperatives 71 | 72 | /// Resets the current concentration game. 73 | func resetGame() { 74 | flipsCount = 0 75 | score = 0 76 | 77 | Card.resetIdentifiersCount() 78 | let pairsCount = cards.count / 2 79 | cards = [] 80 | currentPairIndices = nil 81 | setPairs(withCount: pairsCount) 82 | } 83 | 84 | /// Populates the cards used in the game. 85 | /// The amount of cards is determined by the number of pairs. 86 | private func setPairs(withCount numberOfPairs: Int) { 87 | for _ in 0.. Void) { 52 | self.handler = handler 53 | } 54 | 55 | init(fetch url: URL, handler: @escaping (URL, UIImage) -> Void) { 56 | self.handler = handler 57 | fetch(url) 58 | } 59 | 60 | // Private Implementation 61 | 62 | private let handler: (URL, UIImage) -> Void 63 | private var fetchFailed = false { didSet { callHandlerIfNeeded() } } 64 | private func callHandlerIfNeeded() { 65 | if fetchFailed, let image = backup, let url = image.storeLocallyAsJPEG(named: String(Date().timeIntervalSinceReferenceDate)) { 66 | handler(url, image) 67 | } 68 | } 69 | } 70 | 71 | extension URL { 72 | var imageURL: URL { 73 | if let url = UIImage.urlToStoreLocallyAsJPEG(named: self.path) { 74 | // this was created using UIImage.storeLocallyAsJPEG 75 | return url 76 | } else { 77 | // check to see if there is an embedded imgurl reference 78 | for query in query?.components(separatedBy: "&") ?? [] { 79 | let queryComponents = query.components(separatedBy: "=") 80 | if queryComponents.count == 2 { 81 | if queryComponents[0] == "imgurl", let url = URL(string: queryComponents[1].removingPercentEncoding ?? "") { 82 | return url 83 | } 84 | } 85 | } 86 | return self.baseURL ?? self 87 | } 88 | } 89 | } 90 | 91 | extension UIImage 92 | { 93 | private static let localImagesDirectory = "UIImage.storeLocallyAsJPEG" 94 | 95 | static func urlToStoreLocallyAsJPEG(named: String) -> URL? { 96 | var name = named 97 | let pathComponents = named.components(separatedBy: "/") 98 | if pathComponents.count > 1 { 99 | if pathComponents[pathComponents.count-2] == localImagesDirectory { 100 | name = pathComponents.last! 101 | } else { 102 | return nil 103 | } 104 | } 105 | if var url = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first { 106 | url = url.appendingPathComponent(localImagesDirectory) 107 | do { 108 | try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) 109 | url = url.appendingPathComponent(name) 110 | if url.pathExtension != "jpg" { 111 | url = url.appendingPathExtension("jpg") 112 | } 113 | return url 114 | } catch let error { 115 | print("UIImage.urlToStoreLocallyAsJPEG \(error)") 116 | } 117 | } 118 | return nil 119 | } 120 | 121 | func storeLocallyAsJPEG(named name: String) -> URL? { 122 | if let imageData = UIImageJPEGRepresentation(self, 1.0) { 123 | if let url = UIImage.urlToStoreLocallyAsJPEG(named: name) { 124 | do { 125 | try imageData.write(to: url) 126 | return url 127 | } catch let error { 128 | print("UIImage.storeLocallyAsJPEG \(error)") 129 | } 130 | } 131 | } 132 | return nil 133 | } 134 | 135 | /// Returns the aspect ratio of the passed UIImage. 136 | var aspectRatio: Double { 137 | if let cgImage = cgImage { 138 | let imageHeight = Double(cgImage.height) 139 | let imageWidth = Double(cgImage.width) 140 | 141 | return Double(imageWidth / imageHeight) 142 | } else { 143 | return 1 144 | } 145 | } 146 | } 147 | 148 | extension String { 149 | func madeUnique(withRespectTo otherStrings: [String]) -> String { 150 | var possiblyUnique = self 151 | var uniqueNumber = 1 152 | while otherStrings.contains(possiblyUnique) { 153 | possiblyUnique = self + " \(uniqueNumber)" 154 | uniqueNumber += 1 155 | } 156 | return possiblyUnique 157 | } 158 | } 159 | 160 | extension Array where Element: Equatable { 161 | var uniquified: [Element] { 162 | var elements = [Element]() 163 | forEach { if !elements.contains($0) { elements.append($0) } } 164 | return elements 165 | } 166 | } 167 | 168 | extension NSAttributedString { 169 | func withFontScaled(by factor: CGFloat) -> NSAttributedString { 170 | let mutable = NSMutableAttributedString(attributedString: self) 171 | mutable.setFont(mutable.font?.scaled(by: factor)) 172 | return mutable 173 | } 174 | var font: UIFont? { 175 | get { return attribute(.font, at: 0, effectiveRange: nil) as? UIFont } 176 | } 177 | } 178 | 179 | extension String { 180 | func attributedString(withTextStyle style: UIFontTextStyle, ofSize size: CGFloat) -> NSAttributedString { 181 | let font = UIFontMetrics(forTextStyle: .body).scaledFont(for: UIFont.preferredFont(forTextStyle: .body).withSize(size)) 182 | return NSAttributedString(string: self, attributes: [.font:font]) 183 | } 184 | } 185 | 186 | extension NSMutableAttributedString { 187 | func setFont(_ newValue: UIFont?) { 188 | if newValue != nil { addAttributes([.font:newValue!], range: NSMakeRange(0, length)) } 189 | } 190 | } 191 | 192 | extension UIFont { 193 | func scaled(by factor: CGFloat) -> UIFont { return withSize(pointSize * factor) } 194 | } 195 | 196 | extension UILabel { 197 | func stretchToFit() { 198 | let oldCenter = center 199 | sizeToFit() 200 | center = oldCenter 201 | } 202 | } 203 | 204 | extension CGPoint { 205 | func offset(by delta: CGPoint) -> CGPoint { 206 | return CGPoint(x: x + delta.x, y: y + delta.y) 207 | } 208 | } 209 | 210 | extension UIViewController { 211 | var contents: UIViewController { 212 | if let navcon = self as? UINavigationController { 213 | return navcon.visibleViewController ?? navcon 214 | } else { 215 | return self 216 | } 217 | } 218 | } 219 | 220 | extension UIView { 221 | var snapshot: UIImage? { 222 | UIGraphicsBeginImageContext(bounds.size) 223 | drawHierarchy(in: bounds, afterScreenUpdates: true) 224 | let image = UIGraphicsGetImageFromCurrentImageContext() 225 | UIGraphicsEndImageContext() 226 | return image 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /PersistentImageGallery/PersistentImageGallery/Supporting Files/Utilities.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Utilities.swift 3 | // 4 | // Created by CS193p Instructor. 5 | // Copyright © 2017 Stanford University. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | class ImageFetcher 11 | { 12 | // Public API 13 | 14 | // To use, create with the closure you want called when the image is ready. 15 | // Example: let fetcher = ImageFetcher() { // code to execute when fetch is done } 16 | // Your closure is invoked OFF THE MAIN THREAD. 17 | // Then call fetch(url:) with the url you want to fetch. 18 | // And set a backup image in case the fetch fails. 19 | // 20 | // The handler will be called immediately if the fetch succeeds. 21 | // If the fetch fails, the handler will be called if and when the backup image is set. 22 | // The backup can be set at any time (i.e. before, during or after the fetch). 23 | // If the fetch fails and a backup image is never set, the handler will never be called. 24 | // Thus it would sort of be a strange use of this class to not set a backup image 25 | // (because you'd never find out when the fetch failed). 26 | // Note that you must keep a strong pointer to this object until the fetch finishes 27 | // otherwise the result of the fetch will be discarded and the handler never called. 28 | // In other words, keeping a strong pointer to your instance says "I'm still interested in its result." 29 | 30 | var backup: UIImage? { didSet { callHandlerIfNeeded() } } 31 | 32 | func fetch(_ url: URL) { 33 | DispatchQueue.global(qos: .userInitiated).async { [weak self] in 34 | if let data = try? Data(contentsOf: url.imageURL) { 35 | if self != nil { 36 | // yes, it's ok to create a UIImage off the main thread 37 | if let image = UIImage(data: data) { 38 | self?.handler(url, image) 39 | } else { 40 | self?.fetchFailed = true 41 | } 42 | } else { 43 | print("ImageFetcher: fetch returned but I've left the heap -- ignoring result.") 44 | } 45 | } else { 46 | self?.fetchFailed = true 47 | } 48 | } 49 | } 50 | 51 | init(handler: @escaping (URL, UIImage) -> Void) { 52 | self.handler = handler 53 | } 54 | 55 | init(fetch url: URL, handler: @escaping (URL, UIImage) -> Void) { 56 | self.handler = handler 57 | fetch(url) 58 | } 59 | 60 | // Private Implementation 61 | 62 | private let handler: (URL, UIImage) -> Void 63 | private var fetchFailed = false { didSet { callHandlerIfNeeded() } } 64 | private func callHandlerIfNeeded() { 65 | if fetchFailed, let image = backup, let url = image.storeLocallyAsJPEG(named: String(Date().timeIntervalSinceReferenceDate)) { 66 | handler(url, image) 67 | } 68 | } 69 | } 70 | 71 | extension URL { 72 | var imageURL: URL { 73 | if let url = UIImage.urlToStoreLocallyAsJPEG(named: self.path) { 74 | // this was created using UIImage.storeLocallyAsJPEG 75 | return url 76 | } else { 77 | // check to see if there is an embedded imgurl reference 78 | for query in query?.components(separatedBy: "&") ?? [] { 79 | let queryComponents = query.components(separatedBy: "=") 80 | if queryComponents.count == 2 { 81 | if queryComponents[0] == "imgurl", let url = URL(string: queryComponents[1].removingPercentEncoding ?? "") { 82 | return url 83 | } 84 | } 85 | } 86 | return self.baseURL ?? self 87 | } 88 | } 89 | } 90 | 91 | extension UIImage 92 | { 93 | private static let localImagesDirectory = "UIImage.storeLocallyAsJPEG" 94 | 95 | static func urlToStoreLocallyAsJPEG(named: String) -> URL? { 96 | var name = named 97 | let pathComponents = named.components(separatedBy: "/") 98 | if pathComponents.count > 1 { 99 | if pathComponents[pathComponents.count-2] == localImagesDirectory { 100 | name = pathComponents.last! 101 | } else { 102 | return nil 103 | } 104 | } 105 | if var url = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first { 106 | url = url.appendingPathComponent(localImagesDirectory) 107 | do { 108 | try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) 109 | url = url.appendingPathComponent(name) 110 | if url.pathExtension != "jpg" { 111 | url = url.appendingPathExtension("jpg") 112 | } 113 | return url 114 | } catch let error { 115 | print("UIImage.urlToStoreLocallyAsJPEG \(error)") 116 | } 117 | } 118 | return nil 119 | } 120 | 121 | func storeLocallyAsJPEG(named name: String) -> URL? { 122 | if let imageData = UIImageJPEGRepresentation(self, 1.0) { 123 | if let url = UIImage.urlToStoreLocallyAsJPEG(named: name) { 124 | do { 125 | try imageData.write(to: url) 126 | return url 127 | } catch let error { 128 | print("UIImage.storeLocallyAsJPEG \(error)") 129 | } 130 | } 131 | } 132 | return nil 133 | } 134 | 135 | /// Returns the aspect ratio of the passed UIImage. 136 | var aspectRatio: Double { 137 | if let cgImage = cgImage { 138 | let imageHeight = Double(cgImage.height) 139 | let imageWidth = Double(cgImage.width) 140 | 141 | return Double(imageWidth / imageHeight) 142 | } else { 143 | return 1 144 | } 145 | } 146 | } 147 | 148 | extension String { 149 | func madeUnique(withRespectTo otherStrings: [String]) -> String { 150 | var possiblyUnique = self 151 | var uniqueNumber = 1 152 | while otherStrings.contains(possiblyUnique) { 153 | possiblyUnique = self + " \(uniqueNumber)" 154 | uniqueNumber += 1 155 | } 156 | return possiblyUnique 157 | } 158 | } 159 | 160 | extension Array where Element: Equatable { 161 | var uniquified: [Element] { 162 | var elements = [Element]() 163 | forEach { if !elements.contains($0) { elements.append($0) } } 164 | return elements 165 | } 166 | } 167 | 168 | extension NSAttributedString { 169 | func withFontScaled(by factor: CGFloat) -> NSAttributedString { 170 | let mutable = NSMutableAttributedString(attributedString: self) 171 | mutable.setFont(mutable.font?.scaled(by: factor)) 172 | return mutable 173 | } 174 | var font: UIFont? { 175 | get { return attribute(.font, at: 0, effectiveRange: nil) as? UIFont } 176 | } 177 | } 178 | 179 | extension String { 180 | func attributedString(withTextStyle style: UIFontTextStyle, ofSize size: CGFloat) -> NSAttributedString { 181 | let font = UIFontMetrics(forTextStyle: .body).scaledFont(for: UIFont.preferredFont(forTextStyle: .body).withSize(size)) 182 | return NSAttributedString(string: self, attributes: [.font:font]) 183 | } 184 | } 185 | 186 | extension NSMutableAttributedString { 187 | func setFont(_ newValue: UIFont?) { 188 | if newValue != nil { addAttributes([.font:newValue!], range: NSMakeRange(0, length)) } 189 | } 190 | } 191 | 192 | extension UIFont { 193 | func scaled(by factor: CGFloat) -> UIFont { return withSize(pointSize * factor) } 194 | } 195 | 196 | extension UILabel { 197 | func stretchToFit() { 198 | let oldCenter = center 199 | sizeToFit() 200 | center = oldCenter 201 | } 202 | } 203 | 204 | extension CGPoint { 205 | func offset(by delta: CGPoint) -> CGPoint { 206 | return CGPoint(x: x + delta.x, y: y + delta.y) 207 | } 208 | } 209 | 210 | extension UIViewController { 211 | var contents: UIViewController { 212 | if let navcon = self as? UINavigationController { 213 | return navcon.visibleViewController ?? navcon 214 | } else { 215 | return self 216 | } 217 | } 218 | } 219 | 220 | extension UIView { 221 | var snapshot: UIImage? { 222 | UIGraphicsBeginImageContext(bounds.size) 223 | drawHierarchy(in: bounds, afterScreenUpdates: true) 224 | let image = UIGraphicsGetImageFromCurrentImageContext() 225 | UIGraphicsEndImageContext() 226 | return image 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /ImageGallery/ImageGallery/Controllers/GallerySelectionTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GallerySelectionTableViewController.swift 3 | // ImageGallery 4 | // 5 | // Created by Tiago Maia Lopes on 21/03/18. 6 | // Copyright © 2018 Tiago Maia Lopes. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// The controller responsible for the selection of galleries. 12 | class GallerySelectionTableViewController: UITableViewController, GallerySelectionTableViewCellDelegate { 13 | 14 | enum Section: Int { 15 | case available = 0 16 | case deleted 17 | } 18 | 19 | // MARK: Properties 20 | 21 | /// The store containing the user's galleries. 22 | var galleriesStore: ImageGalleryStore? { 23 | didSet { 24 | tableView?.reloadData() 25 | } 26 | } 27 | 28 | /// The table view's data. 29 | private var galleriesSource: [[ImageGallery]] { 30 | get { 31 | if let store = galleriesStore { 32 | return [store.galleries, store.deletedGalleries] 33 | } else { 34 | return [] 35 | } 36 | } 37 | } 38 | 39 | /// The split's detail controller, if set. 40 | private var detailController: GalleryDisplayCollectionViewController? { 41 | return splitViewController?.viewControllers.last?.contents as? GalleryDisplayCollectionViewController 42 | } 43 | 44 | // MARK: - Life cycle 45 | 46 | deinit { 47 | NotificationCenter.default.removeObserver(self) 48 | } 49 | 50 | override func viewDidLoad() { 51 | super.viewDidLoad() 52 | 53 | NotificationCenter.default.addObserver( 54 | self, 55 | selector: #selector(didReceiveDeleteNotification(_:)), 56 | name: Notification.Name.galleryDeleted, 57 | object: nil 58 | ) 59 | } 60 | 61 | override func viewWillAppear(_ animated: Bool) { 62 | super.viewWillAppear(animated) 63 | 64 | if let selectedGallery = detailController?.gallery { 65 | if let index = galleriesStore?.galleries.index(of: selectedGallery) { 66 | let selectionIndexPath = IndexPath(row: index, section: Section.available.rawValue) 67 | tableView.selectRow( 68 | at: selectionIndexPath, 69 | animated: true, 70 | scrollPosition: .none 71 | ) 72 | } 73 | } 74 | } 75 | 76 | // MARK: - Navigation 77 | 78 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 79 | if let selectedCell = sender as? UITableViewCell { 80 | if let indexPath = tableView.indexPath(for: selectedCell) { 81 | 82 | let section = Section(rawValue: indexPath.section) 83 | guard section == .available else { return } 84 | 85 | let selectedGallery = galleriesSource[indexPath.section][indexPath.row] 86 | 87 | if let navigationController = segue.destination as? UINavigationController { 88 | if let displayController = navigationController.visibleViewController as? GalleryDisplayCollectionViewController { 89 | displayController.gallery = selectedGallery 90 | displayController.galleriesStore = galleriesStore 91 | } 92 | } 93 | } 94 | } 95 | } 96 | 97 | // MARK: - Actions 98 | 99 | @IBAction func didTapAddMore(_ sender: UIBarButtonItem) { 100 | galleriesStore?.addNewGallery() 101 | tableView.reloadData() 102 | } 103 | 104 | @IBAction func didDoubleTap(_ sender: UITapGestureRecognizer) { 105 | if let indexPath = tableView.indexPathForRow(at: sender.location(in: tableView)) { 106 | if let cell = tableView.cellForRow(at: indexPath) as? GallerySelectionTableViewCell { 107 | cell.isEditing = true 108 | } 109 | } 110 | } 111 | 112 | // MARK: - Notification 113 | 114 | @objc func didReceiveDeleteNotification(_ notification: Notification) { 115 | if let deletedGallery = notification.userInfo?[Notification.Name.galleryDeleted] as? ImageGallery { 116 | if detailController?.gallery == deletedGallery { 117 | Timer.scheduledTimer(withTimeInterval: 0.2, repeats: false) { _ in 118 | self.selectFirstGallery() 119 | } 120 | } 121 | } 122 | } 123 | 124 | // MARK: - Imperatives 125 | 126 | /// Selects the first gallery. 127 | private func selectFirstGallery() { 128 | let availableSection = Section.available.rawValue 129 | 130 | guard !galleriesSource[availableSection].isEmpty else { return } 131 | 132 | let selectionIndexPath = IndexPath(row: 0, section: availableSection) 133 | 134 | tableView.selectRow( 135 | at: selectionIndexPath, 136 | animated: true, 137 | scrollPosition: UITableViewScrollPosition.top 138 | ) 139 | 140 | let selectedCell = tableView.cellForRow(at: selectionIndexPath) 141 | 142 | performSegue(withIdentifier: "selectionSegue", sender: selectedCell) 143 | } 144 | 145 | private func getGallery(at indexPath: IndexPath) -> ImageGallery? { 146 | return galleriesSource[indexPath.section][indexPath.row] 147 | } 148 | 149 | // MARK: - Table view data source 150 | 151 | override func numberOfSections(in tableView: UITableView) -> Int { 152 | return galleriesSource.count 153 | } 154 | 155 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 156 | return galleriesSource[section].count 157 | } 158 | 159 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 160 | let cell = tableView.dequeueReusableCell(withIdentifier: "galleryCell", 161 | for: indexPath) 162 | 163 | let gallery = galleriesSource[indexPath.section][indexPath.row] 164 | if let galleryCell = cell as? GallerySelectionTableViewCell { 165 | galleryCell.delegate = self 166 | galleryCell.title = gallery.title 167 | } 168 | 169 | return cell 170 | } 171 | 172 | override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) { 173 | switch editingStyle { 174 | case .delete: 175 | if let deletedGallery = getGallery(at: indexPath) { 176 | self.galleriesStore?.removeGallery(deletedGallery) 177 | tableView.reloadData() 178 | } 179 | break 180 | 181 | default: 182 | break 183 | } 184 | } 185 | 186 | // MARK: - Table view delegate 187 | 188 | override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool { 189 | let section = Section(rawValue: indexPath.section) 190 | return section == .available 191 | } 192 | 193 | override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 194 | if section == 1 { 195 | return "Recently Deleted" 196 | } else { 197 | return nil 198 | } 199 | } 200 | 201 | override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { 202 | let section = Section(rawValue: indexPath.section) 203 | 204 | if section == .deleted { 205 | var actions = [UIContextualAction]() 206 | 207 | let recoverAction = UIContextualAction(style: .normal, title: "Recover") { (action, view, _) in 208 | if let deletedGallery = self.getGallery(at: indexPath) { 209 | self.galleriesStore?.recoverGallery(deletedGallery) 210 | self.tableView.reloadData() 211 | } 212 | } 213 | 214 | actions.append(recoverAction) 215 | return UISwipeActionsConfiguration(actions: actions) 216 | 217 | } else { 218 | return nil 219 | } 220 | } 221 | 222 | // MARK: - Gallery selection cell delegate 223 | 224 | func titleDidChange(_ title: String, in cell: UITableViewCell) { 225 | if let indexPath = tableView.indexPath(for: cell) { 226 | if var gallery = getGallery(at: indexPath) { 227 | gallery.title = title 228 | galleriesStore?.updateGallery(gallery) 229 | tableView.reloadData() 230 | } 231 | } 232 | } 233 | 234 | } 235 | -------------------------------------------------------------------------------- /GraphicalSetGame/GraphicalSetGame/Views/SetCardButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SetCardView.swift 3 | // GraphicalSetGamee 4 | // 5 | // Created by Tiago Maia Lopes on 09/02/18. 6 | // Copyright © 2018 Tiago Maia Lopes. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// The view responsible for displaying a single card. 12 | class SetCardButton: UIButton { 13 | 14 | // MARK: Internal types 15 | 16 | enum CardSymbolShape { 17 | case squiggle 18 | case diamond 19 | case oval 20 | } 21 | 22 | enum CardColor { 23 | case red 24 | case green 25 | case purple 26 | 27 | /// Returns the associated color. 28 | func get() -> UIColor { 29 | switch self { 30 | case .red: 31 | return #colorLiteral(red: 0.521568656, green: 0.1098039225, blue: 0.05098039284, alpha: 1) 32 | case .green: 33 | return #colorLiteral(red: 0.2745098174, green: 0.4862745106, blue: 0.1411764771, alpha: 1) 34 | case .purple: 35 | return #colorLiteral(red: 0.5568627715, green: 0.3529411852, blue: 0.9686274529, alpha: 1) 36 | } 37 | } 38 | 39 | } 40 | 41 | enum CardSymbolShading { 42 | case solid 43 | case striped 44 | case outlined 45 | } 46 | 47 | // MARK: Properties 48 | 49 | /// The symbol shape (diamong, squiggle or oval) for this card view. 50 | var symbolShape: CardSymbolShape? { 51 | didSet { 52 | setNeedsDisplay() 53 | } 54 | } 55 | 56 | /// The number of symbols (one, two or three) for this card view. 57 | var numberOfSymbols = 0 { 58 | didSet { 59 | setNeedsDisplay() 60 | } 61 | } 62 | 63 | /// The symbol color (red, green or purple) for this card view. 64 | var color: CardColor? { 65 | didSet { 66 | setNeedsDisplay() 67 | } 68 | } 69 | 70 | /// The symbol shading (solid, striped or open) for this card view. 71 | var symbolShading: CardSymbolShading? { 72 | didSet { 73 | setNeedsDisplay() 74 | } 75 | } 76 | 77 | /// The path containing all shapes of this view. 78 | var path: UIBezierPath? 79 | 80 | /// The rect in which each path is drawn. 81 | private var drawableRect: CGRect { 82 | let drawableWidth = frame.size.width * 0.80 83 | let drawableHeight = frame.size.height * 0.90 84 | 85 | return CGRect(x: frame.size.width * 0.1, 86 | y: frame.size.height * 0.05, 87 | width: drawableWidth, 88 | height: drawableHeight) 89 | } 90 | 91 | private var shapeHorizontalMargin: CGFloat { 92 | return drawableRect.width * 0.05 93 | } 94 | 95 | private var shapeVerticalMargin: CGFloat { 96 | return drawableRect.height * 0.05 + drawableRect.origin.y 97 | } 98 | 99 | private var shapeWidth: CGFloat { 100 | return (drawableRect.width - (2 * shapeHorizontalMargin)) / 3 101 | } 102 | 103 | private var shapeHeight: CGFloat { 104 | return drawableRect.size.height * 0.9 105 | } 106 | 107 | private var drawableCenter: CGPoint { 108 | return CGPoint(x: bounds.width / 2, y: bounds.height / 2) 109 | } 110 | 111 | // MARK: Life cycle 112 | 113 | override func draw(_ rect: CGRect) { 114 | guard let shape = symbolShape else { return } 115 | guard let color = color?.get() else { return } 116 | guard let shading = symbolShading else { return } 117 | guard numberOfSymbols <= 3 || numberOfSymbols > 0 else { return } 118 | 119 | switch shape { 120 | case .squiggle: 121 | drawSquiggles(byAmount: numberOfSymbols) 122 | 123 | case .diamond: 124 | drawDiamonds(byAmount: numberOfSymbols) 125 | 126 | case .oval: 127 | drawOvals(byAmount: numberOfSymbols) 128 | } 129 | 130 | path!.lineCapStyle = .round 131 | 132 | switch shading { 133 | case .solid: 134 | color.setFill() 135 | path!.fill() 136 | 137 | case .outlined: 138 | color.setStroke() 139 | path!.lineWidth = 1 // TODO: Calculate the line width 140 | path!.stroke() 141 | 142 | case .striped: 143 | path!.lineWidth = 0.01 * frame.size.width 144 | color.setStroke() 145 | path!.stroke() 146 | path!.addClip() 147 | 148 | var currentX: CGFloat = 0 149 | 150 | let stripedPath = UIBezierPath() 151 | stripedPath.lineWidth = 0.005 * frame.size.width 152 | 153 | while currentX < frame.size.width { 154 | stripedPath.move(to: CGPoint(x: currentX, y: 0)) 155 | stripedPath.addLine(to: CGPoint(x: currentX, y: frame.size.height)) 156 | currentX += 0.03 * frame.size.width 157 | } 158 | 159 | color.setStroke() 160 | stripedPath.stroke() 161 | 162 | break 163 | } 164 | } 165 | 166 | // MARK: Imperatives 167 | 168 | /// Draws the squiggles to the drawable rect. 169 | private func drawSquiggles(byAmount amount: Int) { 170 | let path = UIBezierPath() 171 | let allSquigglesWidth = CGFloat(numberOfSymbols) * shapeWidth + CGFloat(numberOfSymbols - 1) * shapeHorizontalMargin 172 | let beginX = (frame.size.width - allSquigglesWidth) / 2 173 | 174 | for i in 0.. 0 { 75 | replaceMatchedCards() 76 | } 77 | 78 | // If the trio selected before wasn't a match, 79 | // Deselect it and penalize the player. 80 | if selectedCards.count == 3 { 81 | // The player shouldn't be able to deselect when three cards are selected. 82 | guard !selectedCards.contains(card) else { return } 83 | 84 | score -= 2 85 | selectedCards = [] 86 | } 87 | 88 | // The selected card is added or removed. 89 | if let index = selectedCards.index(of: card) { 90 | selectedCards.remove(at: index) 91 | } else { 92 | selectedCards.append(card) 93 | } 94 | 95 | // If the new selected card makes a match, 96 | // increase the player's score, mark the current selection as matched 97 | // and deselect the current selection. 98 | if selectedCards.count == 3, currentSelectionMatches() { 99 | matchedCards = selectedCards 100 | selectedCards = [] 101 | score += 4 102 | } 103 | } 104 | 105 | /// Replaces the matched cards from the table. 106 | func replaceMatchedCards() { 107 | guard matchedCards.count == 3 else { return } 108 | dealCards() 109 | matchedCards = [] 110 | } 111 | 112 | /// Returns if the current selection is a match or not. 113 | private func currentSelectionMatches() -> Bool { 114 | guard selectedCards.count == 3 else { return false } 115 | return matches(selectedCards) 116 | } 117 | 118 | /// Checks if the given trio of cards performs a match. 119 | /// 120 | /// - Parameter cards: the cards to be checked for a match, as per the rules of Set. 121 | private func matches(_ cards: SetTrio) -> Bool { 122 | let first = cards[0] 123 | let second = cards[1] 124 | let third = cards[2] 125 | 126 | // A Set is used because of it's unique value constraint. 127 | // Since we have to compare each feature for equality or inequality, 128 | // using a Set and checking it's count can give the result. 129 | let numbersFeatures = Set([first.combination.number, second.combination.number, third.combination.number]) 130 | let colorsFeatures = Set([first.combination.color, second.combination.color, third.combination.color]) 131 | let symbolsFeatures = Set([first.combination.symbol, second.combination.symbol, third.combination.symbol]) 132 | let shadingsFeatures = Set([first.combination.shading, second.combination.shading, third.combination.shading]) 133 | 134 | // All features must be either equal (with the set count of 1) 135 | // or all different (with the count of 3) 136 | return (numbersFeatures.count == 1 || numbersFeatures.count == 3) && 137 | (colorsFeatures.count == 1 || colorsFeatures.count == 3) && 138 | (symbolsFeatures.count == 1 || symbolsFeatures.count == 3) && 139 | (shadingsFeatures.count == 1 || shadingsFeatures.count == 3) 140 | } 141 | 142 | /// Method in charge of dealing the game's cards. 143 | /// 144 | /// - Parameter forAmount: The number of cards to be dealt. 145 | func dealCards(forAmount amount: Int = 3) { 146 | guard amount > 0 else { return } 147 | guard deck.count >= amount else { 148 | 149 | for card in matchedCards { 150 | let index = tableCards.index(of: card)! 151 | tableCards.remove(at: index) 152 | } 153 | 154 | return 155 | } 156 | 157 | var cardsToDeal = [SetCard]() 158 | 159 | for _ in 0.. SetDeck { 200 | var deck = deck 201 | var currentCombination = currentCombination 202 | // Gets the next features that should be added to the combination. 203 | let nextFeatures = type(of: features[0]).getNextFeatures() 204 | 205 | for feature in features { 206 | currentCombination.add(feature: feature) 207 | 208 | // Does it have more features to be added? 209 | if let nextFeatures = nextFeatures { 210 | // Add the next features to the combinations. 211 | deck = makeDeck(features: nextFeatures, currentCombination: currentCombination, deck: deck) 212 | } else { 213 | // The current features are the last ones. 214 | // The combination is now complete, so a new card is created and added to the deck. 215 | deck.append(SetCard(combination: currentCombination)) 216 | } 217 | } 218 | 219 | return deck.shuffled() 220 | } 221 | } 222 | 223 | import GameKit 224 | 225 | extension Array { 226 | /// Returns the current instance with all 227 | /// of it's elements in a random order. 228 | func shuffled() -> [Element] { 229 | return GKRandomSource.sharedRandom().arrayByShufflingObjects(in: self) as! [Element] 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /SetGame/SetGame/Models/SetGame.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SetGame.swift 3 | // SetGame 4 | // 5 | // Created by Tiago Maia Lopes on 1/23/18. 6 | // Copyright © 2018 Tiago Maia Lopes. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | typealias SetDeck = [SetCard] 12 | typealias SetTrio = [SetCard] 13 | 14 | /// The main class responsible for the set game's logic. 15 | class SetGame { 16 | 17 | // MARK: Properties 18 | 19 | /// The game's deck. 20 | /// It represents all cards still available for dealing. 21 | private(set) var deck = SetDeck() 22 | 23 | /// The matched trios of cards. 24 | /// Every time the player makes a match, 25 | /// the matched trio is added to this deck. 26 | private(set) var matchedDeck = [SetTrio]() 27 | 28 | /// The current displaying cards. 29 | /// 30 | /// - Note: Since a card can be matched and removed from the table, 31 | /// the type of each card is optional. 32 | private(set) var tableCards = [SetCard?]() 33 | 34 | /// The currently selected cards. 35 | private(set) var selectedCards = [SetCard]() 36 | 37 | /// The currently matched cards. 38 | private(set) var matchedCards = [SetCard]() { 39 | didSet { 40 | if matchedCards.count == 3 { 41 | matchedDeck.append(matchedCards) 42 | } 43 | } 44 | } 45 | 46 | /// The player's score. 47 | /// If the player has just made a match, increase it's score by 4, 48 | /// if the player has made a mismatch, decrease it by 2. 49 | /// The score can't be lower than zero. 50 | private(set) var score = 0 { 51 | didSet { 52 | if score < 0 { 53 | score = 0 54 | } 55 | } 56 | } 57 | 58 | // MARK: Initializers 59 | 60 | init() { 61 | deck = makeDeck() 62 | } 63 | 64 | // MARK: Imperatives 65 | 66 | /// The method responsible for selecting the chosen card. 67 | /// If three cards are selected, it should check for a match. 68 | func selectCard(at index: Int) { 69 | guard let card = tableCards[index] else { return } 70 | guard !matchedCards.contains(card) else { return } 71 | 72 | // Removes any matched cards from the table. 73 | if matchedCards.count > 0 { 74 | removeMatchedCardsFromTable() 75 | _ = dealCards() 76 | } 77 | 78 | // If the trio selected before wasn't a match, 79 | // Deselect it and penalize the player. 80 | if selectedCards.count == 3 { 81 | // The player shouldn't be able to deselect when three cards are selected. 82 | guard !selectedCards.contains(card) else { return } 83 | 84 | if !currentSelectionMatches() { 85 | score -= 2 86 | } 87 | 88 | selectedCards = [] 89 | } 90 | 91 | // The selected card is added or removed. 92 | if let index = selectedCards.index(of: card) { 93 | selectedCards.remove(at: index) 94 | } else { 95 | selectedCards.append(card) 96 | } 97 | 98 | // If the new selected card makes a match, 99 | // increase the player's score, mark the current selection as matched 100 | // and deselect the current selection. 101 | if selectedCards.count == 3, currentSelectionMatches() { 102 | matchedCards = selectedCards 103 | selectedCards = [] 104 | score += 4 105 | } 106 | } 107 | 108 | /// Removes any matched cards from the table cards. 109 | func removeMatchedCardsFromTable() { 110 | guard matchedCards.count == 3 else { return } 111 | 112 | for index in tableCards.indices { 113 | if let card = tableCards[index], matchedCards.contains(card) { 114 | tableCards[index] = nil 115 | } 116 | } 117 | 118 | matchedCards = [] 119 | } 120 | 121 | /// Returns if the current selection is a match or not. 122 | private func currentSelectionMatches() -> Bool { 123 | guard selectedCards.count == 3 else { return false } 124 | return matches(selectedCards) 125 | } 126 | 127 | /// Checks if the given trio of cards performs a match. 128 | /// 129 | /// - Parameter cards: the cards to be checked for a match, as per the rules of Set. 130 | private func matches(_ cards: SetTrio) -> Bool { 131 | let first = cards[0] 132 | let second = cards[1] 133 | let third = cards[2] 134 | 135 | // A Set is used because of it's unique value constraint. 136 | // Since we have to compare each feature for equality or inequality, 137 | // using a Set and checking it's count can give the result. 138 | let numbersFeatures = Set([first.combination.number, second.combination.number, third.combination.number]) 139 | let colorsFeatures = Set([first.combination.color, second.combination.color, third.combination.color]) 140 | let symbolsFeatures = Set([first.combination.symbol, second.combination.symbol, third.combination.symbol]) 141 | let shadingsFeatures = Set([first.combination.shading, second.combination.shading, third.combination.shading]) 142 | 143 | // All features must be either equal (with the set count of 1) 144 | // or all different (with the count of 3) 145 | return (numbersFeatures.count == 1 || numbersFeatures.count == 3) && 146 | (colorsFeatures.count == 1 || colorsFeatures.count == 3) && 147 | (symbolsFeatures.count == 1 || symbolsFeatures.count == 3) && 148 | (shadingsFeatures.count == 1 || shadingsFeatures.count == 3) 149 | } 150 | 151 | /// Method in charge of dealing the game's cards. 152 | /// 153 | /// - Parameter forAmount: The number of cards to be dealt. 154 | func dealCards(forAmount amount: Int = 3) -> [SetCard] { 155 | guard amount > 0 else { return [] } 156 | guard deck.count >= amount else { return [] } 157 | 158 | var cardsToDeal = [SetCard]() 159 | 160 | for _ in 0.. SetDeck { 198 | var deck = deck 199 | var currentCombination = currentCombination 200 | // Gets the next features that should be added to the combination. 201 | let nextFeatures = type(of: features[0]).getNextFeatures() 202 | 203 | for feature in features { 204 | currentCombination.add(feature: feature) 205 | 206 | // Does it have more features to be added? 207 | if let nextFeatures = nextFeatures { 208 | // Add the next features to the combinations. 209 | deck = makeDeck(features: nextFeatures, currentCombination: currentCombination, deck: deck) 210 | } else { 211 | // The current features are the last ones. 212 | // The combination is now complete, so a new card is created and added to the deck. 213 | deck.append(SetCard(combination: currentCombination)) 214 | } 215 | } 216 | 217 | return deck.shuffled() 218 | } 219 | } 220 | 221 | import GameKit 222 | 223 | extension Array { 224 | /// Returns the current instance with all 225 | /// of it's elements in a random order. 226 | func shuffled() -> [Element] { 227 | return GKRandomSource.sharedRandom().arrayByShufflingObjects(in: self) as! [Element] 228 | } 229 | } 230 | --------------------------------------------------------------------------------