├── .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 | ![color swatches app](https://user-images.githubusercontent.com/1819208/98969732-2742e580-24dd-11eb-98e6-80bfd5e4128e.gif) 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 | ![covid lookup](https://user-images.githubusercontent.com/1819208/101109279-85c32700-35a4-11eb-9f58-864bbc5fdf5a.png) 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 | Buy Me A Coffee 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 | ![mvc architecture](https://docs-assets.developer.apple.com/published/4e7c26b6ad/ff7aa08f-4857-44ce-88d5-7dacbef84509.png) 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 | ![Screen Shot 2023-01-27 at 4 23 21 PM](https://user-images.githubusercontent.com/1819208/215203281-4c077ba2-e867-41f2-9568-279d335e6387.png) 12 | 13 | ![Screen Shot 2023-01-27 at 4 20 43 PM](https://user-images.githubusercontent.com/1819208/215203033-d8836e42-22a3-4b34-9eec-f6f27d5a23b0.png) 14 | 15 | ![Screen Shot 2023-01-27 at 4 20 57 PM](https://user-images.githubusercontent.com/1819208/215203053-98c66a43-25d4-40af-9eea-f4684f87bb09.png) 16 | 17 | ![Simulator Screen Shot - iPad Pro (11-inch) (4th generation) - 2023-01-27 at 16 16 14](https://user-images.githubusercontent.com/1819208/215202317-fdefde12-45de-42d5-b83f-cba4b2b3ef64.png) 18 | 19 | ![Simulator Screen Shot - iPad Pro (11-inch) (4th generation) - 2023-01-27 at 16 17 43](https://user-images.githubusercontent.com/1819208/215202528-e41067b9-eaaf-474a-9624-831bb3b9c269.png) 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 | Screenshot 2023-11-03 at 10 28 40 PM 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 | ![Screenshot 2023-11-09 at 7 22 31 PM](https://github.com/alexpaul/UIKit/assets/1819208/54e2aea1-c6a7-40e7-a239-3e1f12be87a7) 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 | ![Screenshot 2023-11-07 at 4 04 32 PM](https://github.com/alexpaul/UIKit/assets/1819208/001cd381-c115-4600-867e-b8482be45297) 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 | --------------------------------------------------------------------------------