├── .gitignore
├── Color-Swatches.md
├── Contacts-App-Challenge.md
├── CovidLookup.md
├── LICENSE
├── README.md
├── URLSession-Cheatsheet.md
├── ViewControllers.md
├── Views-and-Controls.md
├── assets
└── swiftui-logo.png
├── bottom-sheet.md
├── iOS-Assessment.md
├── preview.md
├── size-classes-and-traits.md
├── uikit-and-swiftui-app-setup.md
└── update-swiftui-view.md
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## User settings
6 | xcuserdata/
7 |
8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
9 | *.xcscmblueprint
10 | *.xccheckout
11 |
12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
13 | build/
14 | DerivedData/
15 | *.moved-aside
16 | *.pbxuser
17 | !default.pbxuser
18 | *.mode1v3
19 | !default.mode1v3
20 | *.mode2v3
21 | !default.mode2v3
22 | *.perspectivev3
23 | !default.perspectivev3
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 |
28 | ## App packaging
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 | # Package.resolved
43 | # *.xcodeproj
44 | #
45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
46 | # hence it is not needed unless you have added a package configuration file to your project
47 | # .swiftpm
48 |
49 | .build/
50 |
51 | # CocoaPods
52 | #
53 | # We recommend against adding the Pods directory to your .gitignore. However
54 | # you should judge for yourself, the pros and cons are mentioned at:
55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
56 | #
57 | # Pods/
58 | #
59 | # Add this line if you want to avoid checking in source code from the Xcode workspace
60 | # *.xcworkspace
61 |
62 | # Carthage
63 | #
64 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
65 | # Carthage/Checkouts
66 |
67 | Carthage/Build/
68 |
69 | # Accio dependency management
70 | Dependencies/
71 | .accio/
72 |
73 | # fastlane
74 | #
75 | # It is recommended to not store the screenshots in the git repo.
76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
77 | # For more information about the recommended setup visit:
78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
79 |
80 | fastlane/report.xml
81 | fastlane/Preview.html
82 | fastlane/screenshots/**/*.png
83 | fastlane/test_output
84 |
85 | # Code Injection
86 | #
87 | # After new code Injection tools there's a generated folder /iOSInjectionProject
88 | # https://github.com/johnno1962/injectionforxcode
89 |
90 | iOSInjectionProject/
91 |
92 | .DS_Store
93 |
--------------------------------------------------------------------------------
/Color-Swatches.md:
--------------------------------------------------------------------------------
1 | # Color Swatches
2 |
3 | ## Challenge
4 |
5 | #### Topics covered
6 |
7 | * UICollectionView
8 | * UICollectionViewDataSource or UICollectionViewDiffableDataSource
9 | * UICollectionViewDelegate
10 | * Auto Layout
11 | * UITextField, UITextFeildDelegate
12 | * UILabel
13 | * UIButton
14 | * Segue
15 | * Data persistence
16 | * UICollectionViewCell
17 |
18 | Build a color swatches app that utilizes [UIColorWell](https://developer.apple.com/documentation/uikit/uicolorwell) or the [UIColorPickerViewController](https://developer.apple.com/documentation/uikit/uicolorpickerviewcontroller) to create color swatches. Those color swatches will be persisted to the iOS device.
19 |
20 | - [ ] User should be able to create a color swatch.
21 | - [ ] User should be able to create a color swatch with the minimum properties: swatch name and color.
22 | - [ ] User should be able to view a list of all their created color swatches in a collection view.
23 | - [ ] User should be able to segue to a detail view to view a larger color swatch.
24 | - [ ] All user generated swatches should persist to the device.
25 |
26 | 
27 |
--------------------------------------------------------------------------------
/Contacts-App-Challenge.md:
--------------------------------------------------------------------------------
1 | # Contacts app
2 |
3 | ## Level 1
4 |
5 | #### Review topics
6 |
7 | * Arrays
8 | * Dictionaries
9 | * Structs and Classes
10 | * Properties: computed properties
11 | * UIViewController
12 | * Storyboard and Interface Builder
13 | * UITableView
14 | * UITableViewDataSource
15 | * UITableViewCell
16 |
17 | Contacts Dictionary
18 |
19 | ```swift
20 | let contactsDict = [03364152046: ("Christin", "Böttger"),
21 | 927525456: ("Joaquin", "Bravo"),
22 | 6868840334: ("David", "Edwards"),
23 | 07905753: ("Roope", "Mattila"),
24 | 27991860: ("Lærke", "Wist"),
25 | 957021797: ("Jonathan", "Diez"),
26 | 01768757320: ("Emily", "Long"),
27 | 0501439641: ("Noe", "Roussel"),
28 | 375351453: ("Justin", "Harris"),
29 | 3028950023: ("Ezra", "Lee"),
30 | 0478121870: ("Ninon", "Bernard"),
31 | 60749217: ("Helene", "Strange"),
32 | 7638623154: ("Estefânia", "Barros"),
33 | 2945132492: ("Gül", "Sinanoğlu"),
34 | 1963139555: ("George", "Miller"),
35 | 64513463: ("Cecilie", "Peterson"),
36 | 01539627648: ("Jared", "Mitchelle"),
37 | 0157693915: ("Valdelaine", "de Souza"),
38 | 07798852536: ("Kristin", "Tausch"),
39 | 00499228235: ("Marissa", "Rode"),
40 | ]
41 | ```
42 |
43 | 1. Use the contacts dictionary provided to create an array of `Contact` objects.
44 | 2. Show the list of contacts in a table view.
45 | 2. Use the built-in table view cell's subtitle option:
46 | 1. Show the contact's first and last name on the cell's text label. Create a computed property in your Contact struct to return `fullname`.
47 | 2. Show the contact's phone number in the detail text label
48 |
49 | Extra:
50 |
51 | 1. Add a unit test to verify you have 20 contacts.
52 |
53 | ## Level 2
54 |
55 | #### Review topics
56 |
57 | * Arrays
58 | * Dictionaries
59 | * Structs and Classes
60 | * Properties: computed properties
61 | * UIViewController
62 | * Storyboard and Interface Builder
63 | * UITableView
64 | * UITableViewDataSource or UITableViewDiffableDataSource
65 | * UITableViewCell
66 | * UIImageView
67 | * UITableViewDelegate
68 | * Segueing in iOS
69 | * Custom Protocol / Delegation, Unwind Segue, Completion Handler
70 | * UIImagePickerController, UIImagePickerControllerDelegate, UINavigationControllerDelegate
71 | * InputAccessoryView
72 |
73 | #### Build a Contacts app that does the following:
74 |
75 | 1. User should be able to add new contact.
76 | 2. User should be able to add at minimum the following: first name, last name, photo, a phone number and email.
77 | 3. User should be able to click on a contact and segue to a detail screen.
78 | 4. User should be able to click on a phone number in the detail screen to place a phone call.
79 | 5. User should be able to delete a contact.
80 | 6. Contacts data should persist through app launches.
81 |
82 |
--------------------------------------------------------------------------------
/CovidLookup.md:
--------------------------------------------------------------------------------
1 | # CovidLookup
2 |
3 | ## Objective
4 |
5 | * Creating a Swift model(s) from Web API JSON data.
6 | * Using `URLSession` natively.
7 | * Populating a Table View with parsed JSON data.
8 |
9 | ## COVID API
10 |
11 | 1. Given the following endpoint: `https://api.covid19api.com/summary` verify the JSON payload via Postman or other.
12 | 1. Create a model(s) and an API client using the endpoint above.
13 | 1. Convert JSON property names to Swifty names using `CodingKeys` as needed.
14 | 1. Populate a Table View with the `countries` array from the JSON data.
15 |
16 | 
17 |
18 | Bonus:
19 | 1. Add a search bar to the view controller.
20 | 2. The user should be able to search by country name e.g `Saint Lucia`
21 | 3. Add a Table View Header to show Global Statistics of Covid cases.
22 | 4. Segue to a detail view and show a chart representation of covid data for country.
23 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Alex Paul
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # iOS using UIKit
2 |
3 | iOS development using UIKit.
4 |
5 |
6 |
7 | ## Prerequisties
8 |
9 | * Familiarity with [Swift Fundamentals](https://github.com/alexpaul/Swift-Fundamentals).
10 | * Computer running macOS.
11 | * [Xcode](https://developer.apple.com/xcode/).
12 | * iOS device if testing camera and ARKit features.
13 | * $99 annual Apple developer account if planning on shipping your own applications to the Apple App Store.
14 |
15 | ## Code structure of a UIKit app using MVC architecture.
16 |
17 | 
18 |
19 | # Table of Contents
20 |
21 | 1. [Views and Controls](https://github.com/alexpaul/iOS-UIKit/blob/main/Views-and-Controls.md)
22 | 1. View Layout
23 | 1. [View Controllers](https://github.com/alexpaul/iOS-UIKit/blob/main/ViewControllers.md)
24 | 1. Animation and Haptics
25 | 1. Windows and Screens
26 | 1. Appearance Customization
27 |
28 | # Frameworks and APIs
29 |
30 | * UIKit
31 | * Core Location
32 | * URLSession
33 | * JSONDecoder / JSONEncoder
34 | * MapKit
35 | * UNUserNotificationCenter
36 | * FileManager
37 | * Core Data
38 | * Combine
39 | * AVFoundation
40 |
41 | # Challenges
42 |
43 | To practice your iOS skills you can choose to work on any of the recommended projects below.
44 |
45 | 1. [Contacts app](https://github.com/alexpaul/iOS-UIKit/blob/main/Contacts-App-Challenge.md)
46 | 1. [Color Swatches](https://github.com/alexpaul/iOS-UIKit/blob/main/Color-Swatches.md)
47 | 1. [Covid Lookup](https://github.com/alexpaul/iOS-UIKit/blob/main/CovidLookup.md)
48 | 1. Tip Calculator
49 | 1. Tic Tac Toe
50 | 1. Hangman
51 | 1. Trivia Game
52 | 1. Three Card Monty
53 | 1. Black Jack
54 | 1. Timer
55 | 1. BestSellers
56 | 1. Photo Journal
57 | 1. Budget app
58 | 1. Podcast Player
59 | 1. Weather app
60 | 1. Drawing app
61 | 1. TV Shows
62 | 1. Currency Converter
63 | 1. To do list
64 | 1. Venues
65 | 1. Blog
66 | 1. Flash Cards
67 | 1. Kids Activity App
68 | 1. Fitness app
69 | 1. Stock app
70 | 1. [iOS Assessment](https://github.com/alexpaul/iOS-UIKit/blob/main/iOS-Assessment.md)
71 |
72 |
73 | # Completed Projects
74 |
75 | Feel free to review those or re-create for practice.
76 |
77 | 1. [Astronomy Photos](https://github.com/alexpaul/AstronomyPhotos)
78 | 1. [Animations](https://github.com/alexpaul/UIKit-Animations)
79 | 1. [Recipe Search](https://github.com/alexpaul/RecipeSearch-Using-Basic-Auth)
80 | 1. [Marketplace](https://github.com/alexpaul/Firebase-Demo)
81 | 1. [Top Stories](https://github.com/alexpaul/TopStories)
82 | 1. [Shopping List](https://github.com/alexpaul/Diffable-Data-Source/tree/master/ShoppingList)
83 | 1. [Scheduler](https://github.com/alexpaul/Scheduler-Custom-Delegation-Tab-Controller)
84 | 1. [MediaFeed](https://github.com/alexpaul/AVFoundation-MediaFeed)
85 | 1. [National Dish](https://github.com/alexpaul/NationalDish)
86 | 1. [Online Photo Search](https://github.com/alexpaul/Compositional-Layout/tree/master/Compositional-Layout-Combine)
87 |
--------------------------------------------------------------------------------
/URLSession-Cheatsheet.md:
--------------------------------------------------------------------------------
1 | # URLSession Cheatsheet
2 |
3 | ## Vocabulary
4 |
5 | * JSON
6 | * endpoint
7 | * RESTFul API
8 | * URLSession
9 | * URLSession.shared
10 | * JSONDecoder, JSONEncoder
11 | * URL
12 | * URLRequest
13 | * URLResponse
14 | * HTTPURLResponse
15 | * Status Code
16 | * Data
17 | * Codable
18 | * Encodable
19 | * Decodable
20 | * HTTP methods: GET, POST, PUT, DELETE, PATCH, UPDATE
21 | * Asynchronous
22 | * Result
23 | * @escaping closures
24 | * capture list e.g [weak self], [unowned self]
25 | * weak vs unowned
26 |
27 | ## Using a closure to capture the `Result` of the asynchronous network request
28 |
29 | `Result` type is an `enum` type that has two arguments, a `success` state and an `failure` state.
30 |
31 | ```swift
32 | func fetchWebData(completion: @escaping (Result<[ModelObject], Error>) -> ()) {
33 | // netowrking code here
34 | }
35 | ```
36 |
37 | ## Perform a GET request using `URLSession`
38 |
39 | `URLSession.shared` is a singleton instance on `URLSession` with basic networking configurations.
40 |
41 | ```swift
42 | let dataTask = URLSession.shared.dataTask(with: url) { (data, response, error) in
43 | // networking code here
44 | }
45 | dataTask.resume()
46 | ```
47 |
48 | ## Check that the `HTTPURLResponse` status code is within the valid range of `200...299` indicating a successful response
49 |
50 | ```swift
51 | guard let httpResponse = response as? HTTPURLResponse,
52 | (200...299).contains(httpResponse.statusCode) else {
53 | print("bad status code")
54 | return
55 | }
56 | ```
57 |
58 | ## Converting `JSON` data to Swift objects
59 |
60 | ```swift
61 | do {
62 | let topLevelModel = try JSONDecoder().decode(TopLevelModel.self, from: jsonData)
63 | let modelObjects = topLevelModel.modelObjects
64 | completion(.success(modelObjects)
65 | } catch {
66 | // decoding error
67 | completion(.failure(error))
68 | }
69 | ```
70 |
71 | ## Using the `Codable` protocol to parse JSON to Swift model(s)
72 |
73 | ```swift
74 | struct CovidCountriesWrapper: Codable {
75 | let countries: [CountrySummary]
76 |
77 | // CodingKeys allows us to rename properties
78 | enum CodingKeys: String, CodingKey {
79 | case countries = "Countries"
80 | }
81 | }
82 |
83 | struct CountrySummary: Codable {
84 | let country: String
85 | let totalConfirmed: Int
86 | let totalRecovered: Int
87 |
88 | enum CodingKeys: String, CodingKey {
89 | case country = "Country"
90 | case totalConfirmed = "TotalConfirmed"
91 | case totalRecovered = "TotalRecovered"
92 | }
93 | }
94 | ```
95 |
96 | ## The `CodingKeys` built-in `enum` type allows us to change JSON property names to our own custom names
97 |
98 | In this example we change `Countries` to a more Swift naming conventional name `countries`.
99 |
100 | ```swift
101 | struct CovidCountriesWrapper: Codable {
102 | let countries: [CountrySummary]
103 |
104 | // CodingKeys allows us to rename properties
105 | enum CodingKeys: String, CodingKey {
106 | case countries = "Countries"
107 | }
108 | }
109 | ```
110 |
111 | ## Completed API Client to fetch web data
112 |
113 | ```swift
114 | func fetchWebData(completion: @escaping (Result<[ModelObject], Error>) -> ()) {
115 | // 1. - endpoint URL string
116 | let endpointURLString = "https://........"
117 |
118 | // 2. - convert the string to an URL
119 | guard let url = URL(string: endpointURLString) else {
120 | print("bad url")
121 | return
122 | }
123 |
124 | // URL vs URLRequest
125 |
126 | // 3. - make the request using URLSession
127 | // .shared is an singleton instance on URLSession comes with basic configuration needed for most requests
128 | let dataTask = URLSession.shared.dataTask(with: url) { (data, response, error) in
129 | if let error = error {
130 | return completion(.failure(error))
131 | }
132 |
133 | // first we have to type cast URLResponse to HTTPURLRepsonse to get access to the status code
134 | // we verify the that status code is in the 200 range which signals all went well with the GET request
135 | guard let httpResponse = response as? HTTPURLResponse,
136 | (200...299).contains(httpResponse.statusCode) else {
137 | print("bad status code")
138 | return
139 | }
140 |
141 | if let jsonData = data {
142 | // convert data to our swift model
143 | do {
144 | let topLevelModel = try JSONDecoder().decode(TopLevelModel.self, from: jsonData)
145 | let modelObjects = topLevelModel.modelObjects
146 | completion(.success(modelObjects))
147 | } catch {
148 | // decoding error
149 | completion(.failure(error))
150 | }
151 | }
152 | }
153 | dataTask.resume()
154 | }
155 | ```
156 |
--------------------------------------------------------------------------------
/ViewControllers.md:
--------------------------------------------------------------------------------
1 | # View Controllers
2 |
3 | * Content View Controllers
4 | * UIViewController
5 | * UITableViewController
6 | * UICollectionViewController
7 | * Container View Controllers
8 | * UISplitViewController
9 | * UINavigationController
10 | * UINavigationBar
11 | * UINavigationItem
12 | * UITabBarController
13 | * UITabBar
14 | * UITabBarItem
15 | * UIPageViewController
16 | * Presnetation Management
17 | * UIPresentationController
18 | * Search Interface
19 | * UISearchContainerViewController
20 | * UISearchController
21 | * UISearchBar
22 | * Images and Video
23 | * UIImagePickerController
24 | * UIVideoEditorController
25 | * Documents and Directories
26 | * UIDocumentBrowserViewController
27 | * UIDocumentPickerController
28 | * UIDocumentInteractionController
29 | * iCloud Sharing
30 | * UICloudSharingController
31 | * Activities Interface
32 | * UIActivityViewController
33 | * UIActivityItemProvider
34 | * UIActivity
35 | * Font Picker
36 | * UIFontPickerViewController
37 | * Color Picker
38 | * UIColorPickerViewController
39 | * Word Lookup
40 | * UIReferenceLibraryViewController
41 | * Printer Picker
42 | * UIPrinterPickerController
43 |
44 |
--------------------------------------------------------------------------------
/Views-and-Controls.md:
--------------------------------------------------------------------------------
1 | # Views and Controls
2 |
3 | * Views and Controls
4 | * UIView
5 | * Container Views
6 | * Collection Views
7 | * Table Views
8 | * UIStackView
9 | * UIScrollView
10 | * Content Views
11 | * UIActivityIndicatorView
12 | * UIImageView
13 | * UIPickerView
14 | * UIProgressView
15 | * Controls
16 | * UIControl
17 | * UIButton
18 | * UIColorWell - introduced in iOS 14
19 | * UIDatePicker
20 | * UIPageControl
21 | * UISegmentedControl
22 | * UISlider
23 | * UIStepper
24 | * UISwitch
25 | * Text Views
26 | * UILabel
27 | * UITextField
28 | * Search Field
29 | * UISearchTextField - introduced in iOS 13
30 | * UISearchToken - introduced in iOS 13
31 | * Visual Effects
32 | * UIVisualEffect
33 | * UIVisualEffectView
34 | * UIVibrancyEffect
35 | * UIBlurEffect
36 | * Bars
37 | * UIBarItem
38 | * UIBarButtonItem
39 | * UIBarButtonItemGroup
40 | * UINavigationBar
41 | * UISearchBar
42 | * UIToolbar
43 | * UITabBar
44 | * UITabBarItem
45 |
--------------------------------------------------------------------------------
/assets/swiftui-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexpaul/UIKit/ff635a45fe690de1705e2ac5a384955d8753d75d/assets/swiftui-logo.png
--------------------------------------------------------------------------------
/bottom-sheet.md:
--------------------------------------------------------------------------------
1 | # Presenting a bottom sheet in UIKit using `UISheetPresentationController`
2 |
3 | > Available in iOS 15+
4 |
5 | ## Files
6 |
7 | * `BottomSheetViewController`: the main view controller responsible for presenting the sheet
8 | * `BottomSheetView`: Presented bottom sheet written in SwiftUI
9 | * `BottomSheetDetailController`: hosts the SwiftUI View
10 |
11 | 
12 |
13 | 
14 |
15 | 
16 |
17 | 
18 |
19 | 
20 |
21 |
22 | try? it out
23 |
24 | ## `BottomSheetViewController.swift`
25 |
26 | ```swift
27 | import UIKit
28 |
29 | final class BottomSheetViewController: UIViewController {
30 | private lazy var presentationButton: UIButton = {
31 | let button = UIButton()
32 | button.addTarget(self, action: #selector(presentSheet), for: .touchUpInside)
33 | button.setTitle("Preset Bottom Sheet using UIKit", for: .normal)
34 | button.setTitleColor(.white, for: .normal)
35 | return button
36 | }()
37 |
38 | override func viewDidLoad() {
39 | super.viewDidLoad()
40 | view.backgroundColor = .orange
41 | view.addSubview(presentationButton)
42 | }
43 |
44 | override func viewWillLayoutSubviews() {
45 | super.viewWillLayoutSubviews()
46 |
47 | presentationButton.translatesAutoresizingMaskIntoConstraints = false
48 | NSLayoutConstraint.activate([
49 | presentationButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
50 | presentationButton.centerYAnchor.constraint(equalTo: view.centerYAnchor)
51 | ])
52 | }
53 |
54 | override func viewDidAppear(_ animated: Bool) {
55 | super.viewDidAppear(animated)
56 | }
57 |
58 | @objc func presentSheet() {
59 | // 1
60 | // Create an instance of the view controller that will be presented
61 | // in this case it host a SwiftUI View
62 | let detailVC = BottomSheetDetailController(rootView: BottomSheetView())
63 |
64 | // 2
65 | // Support `popover` for iPad
66 | detailVC.modalPresentationStyle = .popover
67 |
68 | // 3
69 | // Get the `popoverPresentationController` from the view controller
70 | guard let popover = detailVC.popoverPresentationController else {
71 | return
72 | }
73 |
74 | // 4
75 | // Configure the `popover` instance
76 | popover.permittedArrowDirections = [.up]
77 | popover.sourceView = presentationButton
78 |
79 | // 5
80 | // `adaptiveSheetPresentationController` is available as of iOS 15
81 | let sheet = popover.adaptiveSheetPresentationController
82 |
83 | // 6
84 | // Configure the `UISheetPresentationController` here
85 | sheet.prefersEdgeAttachedInCompactHeight = true
86 | sheet.widthFollowsPreferredContentSizeWhenEdgeAttached = true
87 | sheet.preferredCornerRadius = 24.0
88 | sheet.prefersGrabberVisible = true
89 |
90 | // 7
91 | // [UISheetPresentationController.Detents], e.g .medium(), .large()
92 | // if `.detents` is not set a full sheet will be presented
93 | sheet.detents = [.medium(), .large()]
94 |
95 | // 8
96 | // Present the sheet
97 | present(detailVC, animated: true)
98 | }
99 | }
100 | ```
101 |
102 | ***
103 |
104 |
105 | ## `BottomSheetDetailController.swift`
106 |
107 | ```swift
108 | import SwiftUI
109 |
110 | final class BottomSheetDetailController: UIHostingController {}
111 | ```
112 |
113 | ***
114 |
115 | ## `BottomSheetView.swift`
116 |
117 | ```swift
118 | import SwiftUI
119 |
120 | struct BottomSheetView: View {
121 | @Environment(\.dismiss) var dismiss
122 | @Environment(\.verticalSizeClass) var verticalSizeClass
123 |
124 | private let names = [
125 | "Combine", "SwiftUI", "Testing", "UIKit",
126 | "Swift Package", "Swift Concurrency", "Accessibility", "Xcode"
127 | ]
128 |
129 | var body: some View {
130 | VStack {
131 | ScrollView(showsIndicators: false) {
132 | VStack(alignment: .center, spacing: 20) {
133 | Text("SwiftUI View hosted by UIKit **`UISheetPresentationController`**")
134 | .font(verticalSizeClass == .regular ? .headline : .title3)
135 | .multilineTextAlignment(.center)
136 | Image("swiftui-logo")
137 | .resizable()
138 | .frame(width: 140, height: 140)
139 | .aspectRatio(contentMode: .fit)
140 | ForEach(names, id: \.self) { name in
141 | Text(name)
142 | }
143 | }
144 | .padding(.top, 40)
145 | }
146 | Divider()
147 | .frame(height: 0.5)
148 | .background(.gray)
149 | .shadow(color: .gray, radius: 12)
150 | .padding(.bottom, 2)
151 | Button(action: dismissView) {
152 | Text("Dismiss")
153 | .font(.title3)
154 | .foregroundColor(.white)
155 | .frame(maxWidth: .infinity)
156 | .padding(10)
157 | }
158 | .background(.blue)
159 | .cornerRadius(10)
160 | .padding(.horizontal, 20)
161 | .padding(.bottom, 10)
162 | }
163 | }
164 |
165 | private func dismissView() { dismiss() }
166 | }
167 |
168 | struct BottomSheetView_Previews: PreviewProvider {
169 | static var previews: some View {
170 | BottomSheetView()
171 | }
172 | }
173 | ```
174 |
175 | ***
176 |
177 | ## Resources
178 |
179 | * [Apple docs: UISheetPresentationController](https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller)
180 | * [Customize and Resize Sheets in UIKit - WWDC21](https://developer.apple.com/wwdc21/10063)
181 |
--------------------------------------------------------------------------------
/iOS-Assessment.md:
--------------------------------------------------------------------------------
1 | # iOS Assessment
2 |
3 | This assessment covers the following topics in iOS:
4 |
5 | * UIKit
6 | * View Layout - Auto Layout via Storyboard or Programmatically
7 | * Animation
8 | * Windows and Screens
9 | * Persistence e.g UserDefaults, FileManager or Core Data
10 | * Networking with URLSession, Codable
11 | * MapKit
12 |
13 | ## Rubric
14 |
15 | | Evaluation | Description |
16 | |:------:|:------:|
17 | | MVC Architecture | Clean MVC architecture, avoid M(assive) V(iew) C(controllers) by separating application / business logic |
18 | | DRY | Use of function, do not repeat yourself |
19 | | Unit Test | Unit test your functions |
20 | | Animation | Subtle animations where it will deliver great user feedback |
21 | | Naming Conventions | Adhere to good naming conventions e.g s`truct person` should be `struct Person` |
22 | | Memory Managment | Break retain cycles where needed e.g `[weak self]` if capturing `self` by an unowned object |
23 | | Encapsulation | Use custom initializers and dependency injection to pass data and marking your properties private to avoid mutating from outside objects |
24 | | URLSession | Please write native netowrking code and don't use a third party library e.g `Alamofire` |
25 |
26 |
27 | ## Assessment
28 |
29 | During COVID we all look to enjoy the outdoors when we can and want to help our local restaurants while doing so. In this application you will be building an outdoor dining app.
30 |
31 | Yelp API `https://api.yelp.com/v3/businesses/search?term=coffee&location=10023`
32 |
33 | The app will perform the following:
34 |
35 | - [ ] As a user I can search for restaurants around me, or by provided location.
36 | - [ ] As a user I am able to see restaurants on a map.
37 | - [ ] As a user I can tag a restaurant as providing outdoor dining options.
38 | - [ ] As a user I can view all my tagged restaurants in a Profile page.
39 |
--------------------------------------------------------------------------------
/preview.md:
--------------------------------------------------------------------------------
1 | # Previewing UIKit Views
2 |
3 |
4 |
5 | try it out?
6 |
7 | ```swift
8 | import UIKit
9 | import SwiftUI
10 |
11 | struct Shoe: Hashable {
12 | var imageName: String
13 |
14 | static var data: [Shoe] {
15 | [
16 | Shoe(imageName: "bondi-8-black"),
17 | Shoe(imageName: "bondi-8-orange"),
18 | Shoe(imageName: "rocket-x-2-black"),
19 | Shoe(imageName: "rocket-x-2-green")
20 | ].shuffled()
21 | }
22 | }
23 |
24 | struct ShoeViewer: View {
25 | var body: some View {
26 | TabView {
27 | ForEach(Shoe.data, id: \.self) { shoe in
28 | Image(shoe.imageName)
29 | .resizable()
30 | .aspectRatio(contentMode: .fit)
31 | }
32 | }
33 | .tabViewStyle(.page(indexDisplayMode: .always))
34 | }
35 | }
36 |
37 | final class UIKitView: UIView {
38 | let title: UILabel = {
39 | let label = UILabel()
40 | label.text = "Shoe Viewer"
41 | return label
42 | }()
43 |
44 | let shoeViewer: UIView = {
45 | let hostingVC = UIHostingController(rootView: ShoeViewer())
46 | hostingVC.view.backgroundColor = .black
47 | return hostingVC.view
48 | }()
49 |
50 | override init(frame: CGRect) {
51 | super.init(frame: frame)
52 | backgroundColor = .white
53 | setupConstraints()
54 | }
55 |
56 | required init?(coder: NSCoder) {
57 | fatalError("init(coder:) has not been implemented")
58 | }
59 |
60 | private func setupConstraints() {
61 | addSubview(title)
62 | title.translatesAutoresizingMaskIntoConstraints = false
63 | NSLayoutConstraint.activate([
64 | title.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 40),
65 | title.centerXAnchor.constraint(equalTo: centerXAnchor)
66 | ])
67 |
68 | addSubview(shoeViewer)
69 | shoeViewer.translatesAutoresizingMaskIntoConstraints = false
70 | NSLayoutConstraint.activate([
71 | shoeViewer.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 20),
72 | shoeViewer.centerXAnchor.constraint(equalTo: centerXAnchor),
73 | shoeViewer.heightAnchor.constraint(equalToConstant: 400),
74 | shoeViewer.widthAnchor.constraint(equalTo: safeAreaLayoutGuide.widthAnchor)
75 | ])
76 | }
77 | }
78 |
79 | final class ViewController: UIViewController {
80 | override func loadView() {
81 | view = UIKitView()
82 | }
83 | }
84 |
85 | #Preview {
86 | ViewController()
87 | }
88 | ```
89 |
--------------------------------------------------------------------------------
/size-classes-and-traits.md:
--------------------------------------------------------------------------------
1 | # Size Classes, UIKit and SwiftUI
2 |
3 | > Note: Built against iOS 15.
4 |
5 | This demo uses two layouts based on the current size class `UIUserInterfaceSizeClass`.
6 | * If the `.compact` size class is detected the layout renders one item per row (or one column).
7 | * If the `.regular` size class is detected the layout renders two items per row (or two columns).
8 |
9 | ### iPad
10 |
11 | Shows the following:
12 | * `.regular` size class
13 | * `.compact` size class when in split view
14 |
15 | https://github.com/alexpaul/UIKit/assets/1819208/69ad52d0-2fe8-4a19-9170-174ee6533cfe
16 |
17 | ### iPhone 15 Pro Max
18 |
19 | Shows the following:
20 | * `.compact` size in portrait
21 | * `.regular` size in landscape
22 |
23 | https://github.com/alexpaul/UIKit/assets/1819208/6c4a711a-8266-49b4-bd9f-366800a1c69b
24 |
25 | try? it out
26 |
27 | ```swift
28 | // MARK: - Model
29 |
30 | struct Item: Hashable {
31 | var title: String
32 | var image: String
33 |
34 | static var data: [Item] {
35 | [
36 | Item(
37 | title: "Share",
38 | image: "square.and.arrow.up"
39 | ),
40 | Item(
41 | title: "Bookmark",
42 | image: "bookmark"
43 | ),
44 | Item(
45 | title: "Cart",
46 | image: "cart"
47 | ),
48 | Item(
49 | title: "Credit Card",
50 | image: "creditcard"
51 | ),
52 | Item(
53 | title: "Settings",
54 | image: "gear"
55 | ),
56 | Item(
57 | title: "Favorite",
58 | image: "star"
59 | )
60 | ]
61 | }
62 | }
63 |
64 | // MARK: - SwiftUI Views
65 |
66 | struct ItemView: View {
67 | let item: Item
68 |
69 | var body: some View {
70 | Button(action: {}) {
71 | HStack {
72 | Image(systemName: item.image)
73 | Text(item.title)
74 | }
75 | .frame(maxWidth: .infinity)
76 | .foregroundColor(.primary)
77 | .padding(8)
78 | }
79 | .overlay {
80 | RoundedRectangle(cornerRadius: 16)
81 | .stroke(lineWidth: 1)
82 | }
83 | }
84 | }
85 |
86 | struct RegularLayoutView: View {
87 | let columns = [GridItem(.flexible()), GridItem(.flexible())]
88 |
89 | var body: some View {
90 | VStack {
91 | LazyVGrid(columns: columns, spacing: 20) {
92 | ForEach(Item.data, id: \.self) { item in
93 | ItemView(item: item)
94 | }
95 | }
96 | }
97 | }
98 | }
99 |
100 | struct CompactLayoutView: View {
101 | var body: some View {
102 | VStack(spacing: 20) {
103 | ForEach(Item.data, id: \.self) { item in
104 | ItemView(item: item)
105 | }
106 | }
107 | }
108 | }
109 |
110 | // MARK: - UIKit Views
111 |
112 | final class ContentView: UIView {
113 | private var isCurrentLayoutApplied = false
114 |
115 | private let stackView: UIStackView = {
116 | let stack = UIStackView()
117 | return stack
118 | }()
119 |
120 | private let compactView: UIView = {
121 | let hostingVC = UIHostingController(rootView: CompactLayoutView())
122 | return hostingVC.view
123 | }()
124 |
125 | private let regularView: UIView = {
126 | let hostingVC = UIHostingController(rootView: RegularLayoutView())
127 | return hostingVC.view
128 | }()
129 |
130 | override init(frame: CGRect) {
131 | super.init(frame: frame)
132 | backgroundColor = .systemBackground
133 | addSubViews()
134 | }
135 |
136 | required init?(coder: NSCoder) {
137 | fatalError("init(coder:) has not been implemented")
138 | }
139 |
140 | override func layoutSubviews() {
141 | super.layoutSubviews()
142 | activateConstraintsForStackView()
143 | configureView(for: traitCollection.horizontalSizeClass)
144 | }
145 |
146 | override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
147 | super.traitCollectionDidChange(previousTraitCollection)
148 | if traitCollection.horizontalSizeClass != previousTraitCollection?.horizontalSizeClass {
149 | isCurrentLayoutApplied = false
150 | stackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
151 | }
152 | }
153 |
154 | private func addSubViews() {
155 | addSubview(stackView)
156 | stackView.translatesAutoresizingMaskIntoConstraints = false
157 | }
158 |
159 | private func configureView(for horizontalSizeClass: UIUserInterfaceSizeClass) {
160 | if !isCurrentLayoutApplied {
161 | if horizontalSizeClass == .compact {
162 | applyCompactStackView()
163 | } else {
164 | applyRegularStackView()
165 | }
166 | }
167 | isCurrentLayoutApplied = true
168 | }
169 |
170 | private func activateConstraintsForStackView() {
171 | NSLayoutConstraint.activate([
172 | stackView.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor),
173 | stackView.centerYAnchor.constraint(equalTo: safeAreaLayoutGuide.centerYAnchor),
174 | stackView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.8),
175 | stackView.heightAnchor.constraint(equalTo: safeAreaLayoutGuide.heightAnchor, multiplier: 0.6)
176 | ])
177 | }
178 |
179 | private func applyCompactStackView() {
180 | stackView.addArrangedSubview(compactView)
181 | }
182 |
183 | private func applyRegularStackView() {
184 | stackView.addArrangedSubview(regularView)
185 | }
186 | }
187 |
188 | // MARK: - View Controller
189 |
190 | final class ViewController: UIViewController {
191 | override func loadView() {
192 | view = ContentView()
193 | }
194 | }
195 | ```
196 |
--------------------------------------------------------------------------------
/uikit-and-swiftui-app-setup.md:
--------------------------------------------------------------------------------
1 | # Setting up a UIKit app with SwiftUI
2 |
3 | ## `SceneDelegate`
4 |
5 | ```swift
6 | import UIKit
7 |
8 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
9 | var window: UIWindow?
10 |
11 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
12 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
13 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
14 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
15 | guard let windowScene = (scene as? UIWindowScene) else { return }
16 | window = UIWindow(windowScene: windowScene)
17 | window?.rootViewController = HomeViewController(rootView: HomeView())
18 | window?.makeKeyAndVisible()
19 | }
20 | }
21 | ```
22 |
23 | ***
24 |
25 | ## Root View Controller
26 |
27 | ```swift
28 | import SwiftUI
29 |
30 | class HomeViewController: UIHostingController {}
31 | ```
32 |
--------------------------------------------------------------------------------
/update-swiftui-view.md:
--------------------------------------------------------------------------------
1 | # Updating a SwiftUI view from a UIKit app
2 |
3 | ## 1. Using `ObservableObject` and `@Published` Property Wrapper
4 |
5 | 
6 |
7 | ```swift
8 | import UIKit
9 | import SwiftUI
10 |
11 | // MARK: - Model
12 | final class Content: ObservableObject {
13 | @Published var imageName: String
14 |
15 | init(imageName: String) {
16 | self.imageName = imageName
17 | }
18 | }
19 |
20 | // MARK: - SwiftUI Views
21 | struct ImageCard: View {
22 | @ObservedObject var content: Content
23 |
24 | var body: some View {
25 | VStack(alignment: .leading) {
26 | Image(content.imageName)
27 | .resizable()
28 | .aspectRatio(contentMode: .fit)
29 | .cornerRadius(8)
30 | }
31 | }
32 | }
33 |
34 | // MARK: - UIKit Views
35 |
36 | final class UIKitView: UIView {
37 | private var content = Content(imageName: "ocean")
38 |
39 | private lazy var hostingVC = UIHostingController(
40 | rootView: ImageCard(
41 | content: content
42 | )
43 | )
44 |
45 | private lazy var swiftUIView: UIView = {
46 | hostingVC.view
47 | }()
48 |
49 | override init(frame: CGRect) {
50 | super.init(frame: frame)
51 | backgroundColor = .systemBackground
52 |
53 | DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
54 | self?.content.imageName = "room"
55 | }
56 | }
57 |
58 | required init?(coder: NSCoder) {
59 | fatalError("init(coder:) has not been implemented")
60 | }
61 |
62 | override func layoutSubviews() {
63 | super.layoutSubviews()
64 | addSubview(swiftUIView)
65 | swiftUIView.translatesAutoresizingMaskIntoConstraints = false
66 | NSLayoutConstraint.activate([
67 | swiftUIView.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor),
68 | swiftUIView.centerYAnchor.constraint(equalTo: safeAreaLayoutGuide.centerYAnchor),
69 | swiftUIView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 20),
70 | swiftUIView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -20),
71 | ])
72 | }
73 | }
74 |
75 | // MARK: - View Controller
76 |
77 | final class ViewController: UIViewController {
78 | override func loadView() {
79 | view = UIKitView()
80 | }
81 | }
82 |
83 | #Preview {
84 | ViewController()
85 | }
86 | ```
87 |
88 | ***
89 |
90 | ## 2. Using `rootView`
91 |
92 | 
93 |
94 | try? it out
95 |
96 | ```swift
97 | import UIKit
98 | import SwiftUI
99 |
100 | // MARK: - SwiftUI Views
101 | struct SwiftUIView: View {
102 | var content: String
103 |
104 | var body: some View {
105 | Text(content)
106 | }
107 | }
108 |
109 | // MARK: - UIKit Views
110 |
111 | final class UIKitView: UIView {
112 | private let hostingVC = UIHostingController(
113 | rootView: SwiftUIView(
114 | content: "Initial SwiftUI Content"
115 | )
116 | )
117 |
118 | private lazy var swiftUIView: UIView = {
119 | hostingVC.view
120 | }()
121 |
122 | override init(frame: CGRect) {
123 | super.init(frame: frame)
124 | backgroundColor = .systemBackground
125 | }
126 |
127 | required init?(coder: NSCoder) {
128 | fatalError("init(coder:) has not been implemented")
129 | }
130 |
131 | override func layoutSubviews() {
132 | super.layoutSubviews()
133 | addSubview(swiftUIView)
134 | swiftUIView.translatesAutoresizingMaskIntoConstraints = false
135 | NSLayoutConstraint.activate([
136 | swiftUIView.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor),
137 | swiftUIView.centerYAnchor.constraint(equalTo: safeAreaLayoutGuide.centerYAnchor),
138 | swiftUIView.widthAnchor.constraint(equalTo: safeAreaLayoutGuide.widthAnchor)
139 | ])
140 | }
141 |
142 | func updateSwiftUIView(content: String) {
143 | hostingVC.rootView.content = content
144 | }
145 | }
146 |
147 | // MARK: - View Controller
148 |
149 | final class ViewController: UIViewController {
150 | private let uikitView = UIKitView()
151 |
152 | override func loadView() {
153 | view = uikitView
154 | }
155 |
156 | override func viewDidLoad() {
157 | super.viewDidLoad()
158 |
159 | DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
160 | self?.uikitView.updateSwiftUIView(content: "Updated SwiftUI Content")
161 | }
162 | }
163 | }
164 |
165 | #Preview {
166 | ViewController()
167 | }
168 | ```
169 |
--------------------------------------------------------------------------------