├── 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 |
7 |
8 | ## Set:
9 |
10 |
11 |
12 | ## Graphical set:
13 |
14 |
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 |
21 |
22 |
23 |
24 |
25 |
26 |
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 |
33 |
34 |
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 |
42 |
43 |
44 |
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 |
--------------------------------------------------------------------------------