├── LICENSE ├── README.md ├── documents ├── Assignments │ ├── a1.pdf │ ├── a2.pdf │ ├── a3.pdf │ ├── a4.pdf │ ├── a5.pdf │ └── a6.pdf ├── Readings │ ├── r1.pdf │ ├── r2.pdf │ └── r3.pdf └── Slides │ ├── l1.pdf │ ├── l10.pdf │ ├── l11.pdf │ ├── l12.pdf │ ├── l13.pdf │ ├── l14.pdf │ ├── l2.pdf │ ├── l3.pdf │ ├── l4.pdf │ ├── l5.pdf │ ├── l6.pdf │ ├── l7.pdf │ ├── l8.pdf │ └── l9.pdf ├── header.png └── projects ├── EmojiArt ├── EmojiArt.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcuserdata │ │ │ └── Tieda.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ └── xcuserdata │ │ └── Tieda.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist └── EmojiArt │ ├── AnimatableSystemFontModifier.swift │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── Base.lproj │ └── LaunchScreen.storyboard │ ├── EditableText.swift │ ├── EmojiArt.swift │ ├── EmojiArtDocument+Palette.swift │ ├── EmojiArtDocument.swift │ ├── EmojiArtDocumentChooser.swift │ ├── EmojiArtDocumentStore.swift │ ├── EmojiArtDocumentView.swift │ ├── EmojiArtExtensions.swift │ ├── Grid.swift │ ├── GridLayout.swift │ ├── Info.plist │ ├── OptionalImage.swift │ ├── PaletteChooser.swift │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ ├── SceneDelegate.swift │ └── Spinning.swift ├── Enroute L12 ├── Enroute.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcuserdata │ │ │ ├── Tieda.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ │ │ ├── cs193p.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ │ │ └── paul.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ └── xcuserdata │ │ ├── Tieda.xcuserdatad │ │ └── xcschemes │ │ │ └── xcschememanagement.plist │ │ ├── cs193p.xcuserdatad │ │ └── xcschemes │ │ │ └── xcschememanagement.plist │ │ └── paul.xcuserdatad │ │ ├── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ │ └── xcschemes │ │ └── xcschememanagement.plist └── Enroute │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── Base.lproj │ └── LaunchScreen.storyboard │ ├── Enroute.xcdatamodeld │ ├── .xccurrentversion │ └── Enroute.xcdatamodel │ │ └── contents │ ├── FilterFlights.swift │ ├── FlightAware │ ├── FAFlight.swift │ ├── FlightAware+AirlineInfo.swift │ ├── FlightAware+AirportInfo.swift │ ├── FlightAware+Enroute.swift │ ├── FlightAwareRequest.swift │ └── FlightSimulationData.swift │ ├── FlightsEnrouteView.swift │ ├── FoundationExtensions.swift │ ├── Info.plist │ ├── L12 │ ├── Airline.swift │ ├── Airport.swift │ └── Flight.swift │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ └── SceneDelegate.swift └── Memorize ├── Memorize.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcuserdata │ │ └── Tieda.xcuserdatad │ │ └── UserInterfaceState.xcuserstate └── xcuserdata │ └── Tieda.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist └── Memorize ├── AppDelegate.swift ├── Array+Identifiable.swift ├── Array+Only.swift ├── Assets.xcassets ├── AppIcon.appiconset │ └── Contents.json └── Contents.json ├── Base.lproj └── LaunchScreen.storyboard ├── Cardify.swift ├── EmojiMemoryGame.swift ├── EmojiMemoryGameView.swift ├── Grid.swift ├── GridLayout.swift ├── Info.plist ├── MemoryGame.swift ├── Pie.swift ├── Preview Content └── Preview Assets.xcassets │ └── Contents.json └── SceneDelegate.swift /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Tieda 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 | ![image](header.png) 2 | 3 | Lecturer: **[Paul Hegarty](https://www.quora.com/Who-is-Paul-Hegarty-from-Stanford-CS193)** 4 | 5 | ## Lecture 1: Course Logistics and Intro to SwiftUI 6 | 7 | [Video](https://youtu.be/jbtqIBpUG7g) 8 | 9 | [Slides](./documents/Slides/l1.pdf) 10 | 11 | ## Lecture 2: MVVM and the Swift Type System 12 | 13 | [Video](https://youtu.be/4GjXq2Sr55Q) 14 | 15 | - [MVVM](https://youtu.be/4GjXq2Sr55Q?t=40) 16 | - [struct vs class](https://youtu.be/4GjXq2Sr55Q?t=1205) 17 | - [Copy on write](https://youtu.be/4GjXq2Sr55Q?t=1248) 18 | - [Generics](https://youtu.be/4GjXq2Sr55Q?t=1730) 19 | - [Functions as Types](https://youtu.be/4GjXq2Sr55Q?t=1971) 20 | 21 | [Slides](./documents/Slides/l2.pdf) 22 | 23 | [Reading 1](./documents/Readings/r1.pdf) 24 | 25 | [Assignment 1](./documents/Assignments/a1.pdf) 26 | 27 | ## Lecture 3: Reactive UI + Protocols + Layout 28 | 29 | [Video](https://youtu.be/SIYdYpPXil4) 30 | 31 | - [Protocols](https://youtu.be/SIYdYpPXil4?t=2065) 32 | - [Protocols and Generics](https://youtu.be/SIYdYpPXil4?t=2694) 33 | - [Layout](https://youtu.be/SIYdYpPXil4?t=3176) 34 | - [GeometryReader](https://youtu.be/SIYdYpPXil4?t=4207) 35 | 36 | [Slides](./documents/Slides/l3.pdf) 37 | 38 | ## Lecture 4: Grid + enum + Optionals 39 | 40 | [Video](https://youtu.be/eHEeWzFP6O4) 41 | 42 | - [@esacping](https://www.youtube.com/watch?v=eHEeWzFP6O4&feature=youtu.be&t=515) 43 | - [enum](https://youtu.be/eHEeWzFP6O4?t=1973) 44 | - [Optional](https://youtu.be/eHEeWzFP6O4?t=2453) 45 | - [Equatable](https://youtu.be/eHEeWzFP6O4?t=3808) 46 | 47 | [Slides](./documents/Slides/l4.pdf) 48 | 49 | [Reading 2](./documents/Readings/r2.pdf) 50 | 51 | [Assignment 2](./documents/Assignments/a2.pdf) 52 | 53 | ## Lecture 5: ViewBuilder + Shape + ViewModifier 54 | 55 | [Video](https://www.youtube.com/watch?v=oDKDGCRdSHc) 56 | 57 | - [private(set)](https://youtu.be/oDKDGCRdSHc?t=282) 58 | - [@ViewBuilder](https://youtu.be/oDKDGCRdSHc?t=725) 59 | - [Shape](https://youtu.be/oDKDGCRdSHc?t=1226) 60 | - [ViewModifier](https://youtu.be/oDKDGCRdSHc?t=2555) 61 | 62 | [Slides](./documents/Slides/l5.pdf) 63 | 64 | ## Lecture 6: Animation 65 | 66 | [Video](https://www.youtube.com/watch?v=3krC2c56ceQ) 67 | 68 | - [Property Observer](https://youtu.be/3krC2c56ceQ?t=43) 69 | - [@State](https://youtu.be/3krC2c56ceQ?t=119) 70 | - [Implicit("automatic") Animation](https://youtu.be/3krC2c56ceQ?t=716) 71 | - [Explicit Animation](https://youtu.be/3krC2c56ceQ?t=1048) 72 | - [Explicit Animation Demo](https://youtu.be/3krC2c56ceQ?t=3055) 73 | - [Transitions](https://youtu.be/3krC2c56ceQ?t=1253) 74 | - [AnimatableModifier](https://youtu.be/3krC2c56ceQ?t=3856) 75 | 76 | [Slides](./documents/Slides/l6.pdf) 77 | 78 | [Reading 3](./documents/Readings/r3.pdf) 79 | 80 | [Assignment 3](./documents/Assignments/a3.pdf) 81 | 82 | ## Lecture 7: Multithreading EmojiArt 83 | 84 | [Video](https://youtu.be/tmx-OwkBWxA) 85 | 86 | - [Multithreading](https://youtu.be/tmx-OwkBWxA?t=378) 87 | - [Threads & Queues](https://youtu.be/tmx-OwkBWxA?t=474) 88 | - [GCD](https://youtu.be/tmx-OwkBWxA?t=716) 89 | - [fileprivate](https://youtu.be/tmx-OwkBWxA?t=2822) 90 | 91 | [Slides](./documents/Slides/l7.pdf) 92 | 93 | ## Lecture 8: Gestures JSON 94 | 95 | [Video](https://youtu.be/mz-rNLWJ0bk) 96 | 97 | - [UserDefaults](https://youtu.be/mz-rNLWJ0bk?t=175) 98 | - [Gestures](https://youtu.be/mz-rNLWJ0bk?t=526) 99 | - [Discrete Gestures](https://youtu.be/mz-rNLWJ0bk?t=694) 100 | - [Non-Discrete Gestures](https://youtu.be/mz-rNLWJ0bk?t=757) 101 | - [User Defaults won't write to disk right away](https://youtu.be/mz-rNLWJ0bk?t=2397) 102 | - [inout gesture state](https://youtu.be/mz-rNLWJ0bk?t=3906) 103 | 104 | [Slides](./documents/Slides/l8.pdf) 105 | 106 | [Assignment 4](./documents/Assignments/a4.pdf) 107 | 108 | [Assignment 5](./documents/Assignments/a5.pdf) 109 | 110 | ## Lecture 9: Data Flow 111 | 112 | [Video](https://youtu.be/0i152oA3T3s) 113 | 114 | - [Property Wrappers](https://youtu.be/0i152oA3T3s?t=60) 115 | - [@State](https://youtu.be/0i152oA3T3s?t=376) 116 | - [@ObservedObject](https://youtu.be/0i152oA3T3s?t=443) 117 | - [@Binding](https://youtu.be/0i152oA3T3s?t=492) 118 | - [@EnvironmentObject](https://youtu.be/0i152oA3T3s?t=746) 119 | - [@Environment](https://youtu.be/0i152oA3T3s?t=934) 120 | - [@Publisher](https://youtu.be/0i152oA3T3s?t=1108) 121 | - [.sink{}/AnyCancellable](https://youtu.be/0i152oA3T3s?t=2212) 122 | - [.onReceive{}](https://youtu.be/0i152oA3T3s?t=2570) 123 | - [Publisher + URLSession(dataTaskPublisher)](https://youtu.be/0i152oA3T3s?t=2698) 124 | 125 | [Slides](./documents/Slides/l9.pdf) 126 | 127 | ## Lecture 10: Modal Presentation and Navigation 128 | 129 | [Video](https://youtu.be/CKexGQuIO7E) 130 | 131 | - [Initialize @State var in initializer](https://youtu.be/CKexGQuIO7E?t=213) 132 | 133 | - [Share viewmodel via @EnviromentObject](https://youtu.be/CKexGQuIO7E?t=1011) 134 | 135 | - [Form](https://youtu.be/CKexGQuIO7E?t=1648) 136 | 137 | - [KeyPath](https://youtu.be/CKexGQuIO7E?t=2005) 138 | 139 | - [Hashable/Equatable/Identifiable](https://youtu.be/CKexGQuIO7E?t=3278) 140 | 141 | - [@EnvironmentObject](https://youtu.be/CKexGQuIO7E?t=3728) 142 | 143 | - [List](https://youtu.be/CKexGQuIO7E?t=3830) 144 | 145 | - [Inject EnvironmentObject](https://youtu.be/CKexGQuIO7E?t=3861) 146 | 147 | - [Navigation Link](https://youtu.be/CKexGQuIO7E?t=4033) 148 | 149 | - [Alert](https://youtu.be/CKexGQuIO7E?t=4785) 150 | 151 | - [Swipe to Delete](https://youtu.be/CKexGQuIO7E?t=5331) 152 | 153 | - [Edit Mode](https://youtu.be/CKexGQuIO7E?t=5467) 154 | 155 | - [Set environment](https://youtu.be/CKexGQuIO7E?t=5663) 156 | 157 | - [.zIndex()](https://youtu.be/CKexGQuIO7E?t=6102) 158 | 159 | [Slides](./documents/Slides/l10.pdf) 160 | 161 | [Assignment 6](./documents/Assignments/a6.pdf) 162 | 163 | ## Lecture 11: Enroute Picker 164 | 165 | [Video](https://youtu.be/fCfC6m7XUew) 166 | 167 | - [Init a @Binding var(using \_var)](https://youtu.be/fCfC6m7XUew?t=1558) 168 | 169 | - [Init @State with wrappedValue](https://youtu.be/fCfC6m7XUew?t=1770) 170 | 171 | - [Picker](https://youtu.be/fCfC6m7XUew?t=1861) 172 | 173 | - [Picker in Form](https://youtu.be/fCfC6m7XUew?t=2155) 174 | 175 | - [Picker and .tag()](https://youtu.be/fCfC6m7XUew?t=2680) 176 | 177 | - [Toggle in Form](https://youtu.be/fCfC6m7XUew?t=2904) 178 | 179 | [Slides](./documents/Slides/l11.pdf) 180 | 181 | ## Lecture 12: Core Data 182 | 183 | [Video](https://youtu.be/yOhyOpXvaec) 184 | 185 | - [Core Data Features](https://youtu.be/yOhyOpXvaec?t=265) 186 | 187 | - [SwiftUI Integration](https://youtu.be/yOhyOpXvaec?t=304) 188 | 189 | - [Read/Write data](https://youtu.be/yOhyOpXvaec?t=737) 190 | 191 | - [@FetchRequest](https://youtu.be/yOhyOpXvaec?t=1009) 192 | 193 | - [Build Objects Graph in Core Data](https://youtu.be/yOhyOpXvaec?t=1610) 194 | 195 | - [Fetch/Create Object](https://youtu.be/yOhyOpXvaec?t=2361) 196 | 197 | - [NSPredicate](https://youtu.be/yOhyOpXvaec?t=2442) 198 | 199 | - [NSSortDescriptor](https://youtu.be/yOhyOpXvaec?t=2521) 200 | 201 | - [Deal with NSSet of one to many relationship](https://youtu.be/yOhyOpXvaec?t=3255) 202 | 203 | - [Deal with non-optional value](https://youtu.be/yOhyOpXvaec?t=3499) 204 | 205 | - [@FetchRequest demo](https://youtu.be/yOhyOpXvaec?t=3762) 206 | 207 | - [Init @FetchRequest](https://youtu.be/yOhyOpXvaec?t=3926) 208 | 209 | - [TRUEPREDICATE](https://youtu.be/yOhyOpXvaec?t=4884) 210 | 211 | - [Crash Error: Context in environment is not connected to a persistent store coordinator](https://youtu.be/yOhyOpXvaec?t=5160) 212 | 213 | - [Build a programmatic NSPredicate](https://youtu.be/yOhyOpXvaec?t=5406) 214 | 215 | [Slides](./documents/Slides/l12.pdf) 216 | 217 | ## Lecture 13: Persistence 218 | 219 | [Video](https://youtu.be/fTNPRhGGP-0) 220 | 221 | - [Persistence Overview](https://youtu.be/fTNPRhGGP-0?t=112) 222 | - [Cloud Kit](https://youtu.be/fTNPRhGGP-0?t=332) 223 | - [Create a record in Cloud Kit](https://youtu.be/fTNPRhGGP-0?t=840) 224 | - [Query for records](https://youtu.be/fTNPRhGGP-0?t=1173) 225 | - [File System](https://youtu.be/fTNPRhGGP-0?t=1319) 226 | - [Sandbox](https://youtu.be/fTNPRhGGP-0?t=1452) 227 | - [File Manager](https://youtu.be/fTNPRhGGP-0?t=1610) 228 | 229 | [Slides](./documents/Slides/l13.pdf) 230 | 231 | ## Lecture 14: UIKit Integration 232 | 233 | [Video](https://youtu.be/GRX5Dha_Clw) 234 | 235 | [Slides](./documents/Slides/l14.pdf) 236 | -------------------------------------------------------------------------------- /documents/Assignments/a1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weitieda/cs193p-2020-swiftui/ba90cdc1d0b0ae59e386e9640c2f2d27bc2bcc15/documents/Assignments/a1.pdf -------------------------------------------------------------------------------- /documents/Assignments/a2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weitieda/cs193p-2020-swiftui/ba90cdc1d0b0ae59e386e9640c2f2d27bc2bcc15/documents/Assignments/a2.pdf -------------------------------------------------------------------------------- /documents/Assignments/a3.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weitieda/cs193p-2020-swiftui/ba90cdc1d0b0ae59e386e9640c2f2d27bc2bcc15/documents/Assignments/a3.pdf -------------------------------------------------------------------------------- /documents/Assignments/a4.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weitieda/cs193p-2020-swiftui/ba90cdc1d0b0ae59e386e9640c2f2d27bc2bcc15/documents/Assignments/a4.pdf -------------------------------------------------------------------------------- /documents/Assignments/a5.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weitieda/cs193p-2020-swiftui/ba90cdc1d0b0ae59e386e9640c2f2d27bc2bcc15/documents/Assignments/a5.pdf -------------------------------------------------------------------------------- /documents/Assignments/a6.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weitieda/cs193p-2020-swiftui/ba90cdc1d0b0ae59e386e9640c2f2d27bc2bcc15/documents/Assignments/a6.pdf -------------------------------------------------------------------------------- /documents/Readings/r1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weitieda/cs193p-2020-swiftui/ba90cdc1d0b0ae59e386e9640c2f2d27bc2bcc15/documents/Readings/r1.pdf -------------------------------------------------------------------------------- /documents/Readings/r2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weitieda/cs193p-2020-swiftui/ba90cdc1d0b0ae59e386e9640c2f2d27bc2bcc15/documents/Readings/r2.pdf -------------------------------------------------------------------------------- /documents/Readings/r3.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weitieda/cs193p-2020-swiftui/ba90cdc1d0b0ae59e386e9640c2f2d27bc2bcc15/documents/Readings/r3.pdf -------------------------------------------------------------------------------- /documents/Slides/l1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weitieda/cs193p-2020-swiftui/ba90cdc1d0b0ae59e386e9640c2f2d27bc2bcc15/documents/Slides/l1.pdf -------------------------------------------------------------------------------- /documents/Slides/l10.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weitieda/cs193p-2020-swiftui/ba90cdc1d0b0ae59e386e9640c2f2d27bc2bcc15/documents/Slides/l10.pdf -------------------------------------------------------------------------------- /documents/Slides/l11.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weitieda/cs193p-2020-swiftui/ba90cdc1d0b0ae59e386e9640c2f2d27bc2bcc15/documents/Slides/l11.pdf -------------------------------------------------------------------------------- /documents/Slides/l12.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weitieda/cs193p-2020-swiftui/ba90cdc1d0b0ae59e386e9640c2f2d27bc2bcc15/documents/Slides/l12.pdf -------------------------------------------------------------------------------- /documents/Slides/l13.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weitieda/cs193p-2020-swiftui/ba90cdc1d0b0ae59e386e9640c2f2d27bc2bcc15/documents/Slides/l13.pdf -------------------------------------------------------------------------------- /documents/Slides/l2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weitieda/cs193p-2020-swiftui/ba90cdc1d0b0ae59e386e9640c2f2d27bc2bcc15/documents/Slides/l2.pdf -------------------------------------------------------------------------------- /documents/Slides/l3.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weitieda/cs193p-2020-swiftui/ba90cdc1d0b0ae59e386e9640c2f2d27bc2bcc15/documents/Slides/l3.pdf -------------------------------------------------------------------------------- /documents/Slides/l4.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weitieda/cs193p-2020-swiftui/ba90cdc1d0b0ae59e386e9640c2f2d27bc2bcc15/documents/Slides/l4.pdf -------------------------------------------------------------------------------- /documents/Slides/l5.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weitieda/cs193p-2020-swiftui/ba90cdc1d0b0ae59e386e9640c2f2d27bc2bcc15/documents/Slides/l5.pdf -------------------------------------------------------------------------------- /documents/Slides/l6.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weitieda/cs193p-2020-swiftui/ba90cdc1d0b0ae59e386e9640c2f2d27bc2bcc15/documents/Slides/l6.pdf -------------------------------------------------------------------------------- /documents/Slides/l7.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weitieda/cs193p-2020-swiftui/ba90cdc1d0b0ae59e386e9640c2f2d27bc2bcc15/documents/Slides/l7.pdf -------------------------------------------------------------------------------- /documents/Slides/l8.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weitieda/cs193p-2020-swiftui/ba90cdc1d0b0ae59e386e9640c2f2d27bc2bcc15/documents/Slides/l8.pdf -------------------------------------------------------------------------------- /documents/Slides/l9.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weitieda/cs193p-2020-swiftui/ba90cdc1d0b0ae59e386e9640c2f2d27bc2bcc15/documents/Slides/l9.pdf -------------------------------------------------------------------------------- /header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weitieda/cs193p-2020-swiftui/ba90cdc1d0b0ae59e386e9640c2f2d27bc2bcc15/header.png -------------------------------------------------------------------------------- /projects/EmojiArt/EmojiArt.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 833C136B24A4140600DA6BAA /* OptionalImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833C136A24A4140500DA6BAA /* OptionalImage.swift */; }; 11 | 833C136D24A41FA400DA6BAA /* AnimatableSystemFontModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833C136C24A41FA400DA6BAA /* AnimatableSystemFontModifier.swift */; }; 12 | 833C137F24A95F6900DA6BAA /* Spinning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833C137E24A95F6900DA6BAA /* Spinning.swift */; }; 13 | 833C138124A98D2A00DA6BAA /* PaletteChooser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833C138024A98D2A00DA6BAA /* PaletteChooser.swift */; }; 14 | 833C138324A9933E00DA6BAA /* EmojiArtDocument+Palette.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833C138224A9933E00DA6BAA /* EmojiArtDocument+Palette.swift */; }; 15 | 833C138524AC453200DA6BAA /* Grid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833C138424AC453200DA6BAA /* Grid.swift */; }; 16 | 833C138724AC454600DA6BAA /* GridLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833C138624AC454600DA6BAA /* GridLayout.swift */; }; 17 | 833C138924AD1D2A00DA6BAA /* EmojiArtDocumentStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833C138824AD1D2A00DA6BAA /* EmojiArtDocumentStore.swift */; }; 18 | 833C138B24AD3A4100DA6BAA /* EmojiArtDocumentChooser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833C138A24AD3A4100DA6BAA /* EmojiArtDocumentChooser.swift */; }; 19 | 833C138D24AD615000DA6BAA /* EditableText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833C138C24AD615000DA6BAA /* EditableText.swift */; }; 20 | 83A97381249863320056E875 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83A97380249863320056E875 /* AppDelegate.swift */; }; 21 | 83A97383249863320056E875 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83A97382249863320056E875 /* SceneDelegate.swift */; }; 22 | 83A97387249863340056E875 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 83A97386249863340056E875 /* Assets.xcassets */; }; 23 | 83A9738A249863340056E875 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 83A97389249863340056E875 /* Preview Assets.xcassets */; }; 24 | 83A9738D249863340056E875 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 83A9738B249863340056E875 /* LaunchScreen.storyboard */; }; 25 | 83A97395249863560056E875 /* EmojiArtDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83A97394249863560056E875 /* EmojiArtDocument.swift */; }; 26 | 83A97397249863D60056E875 /* EmojiArtDocumentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83A97396249863D60056E875 /* EmojiArtDocumentView.swift */; }; 27 | 83A97399249866F80056E875 /* EmojiArt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83A97398249866F80056E875 /* EmojiArt.swift */; }; 28 | 83A9739B249869270056E875 /* EmojiArtExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83A9739A249869270056E875 /* EmojiArtExtensions.swift */; }; 29 | /* End PBXBuildFile section */ 30 | 31 | /* Begin PBXFileReference section */ 32 | 833C136A24A4140500DA6BAA /* OptionalImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionalImage.swift; sourceTree = ""; }; 33 | 833C136C24A41FA400DA6BAA /* AnimatableSystemFontModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatableSystemFontModifier.swift; sourceTree = ""; }; 34 | 833C137E24A95F6900DA6BAA /* Spinning.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Spinning.swift; sourceTree = ""; }; 35 | 833C138024A98D2A00DA6BAA /* PaletteChooser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaletteChooser.swift; sourceTree = ""; }; 36 | 833C138224A9933E00DA6BAA /* EmojiArtDocument+Palette.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EmojiArtDocument+Palette.swift"; sourceTree = ""; }; 37 | 833C138424AC453200DA6BAA /* Grid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Grid.swift; sourceTree = ""; }; 38 | 833C138624AC454600DA6BAA /* GridLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridLayout.swift; sourceTree = ""; }; 39 | 833C138824AD1D2A00DA6BAA /* EmojiArtDocumentStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiArtDocumentStore.swift; sourceTree = ""; }; 40 | 833C138A24AD3A4100DA6BAA /* EmojiArtDocumentChooser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiArtDocumentChooser.swift; sourceTree = ""; }; 41 | 833C138C24AD615000DA6BAA /* EditableText.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditableText.swift; sourceTree = ""; }; 42 | 83A9737D249863320056E875 /* Emoji Art.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Emoji Art.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 43 | 83A97380249863320056E875 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 44 | 83A97382249863320056E875 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 45 | 83A97386249863340056E875 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 46 | 83A97389249863340056E875 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 47 | 83A9738C249863340056E875 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 48 | 83A9738E249863340056E875 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 49 | 83A97394249863560056E875 /* EmojiArtDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiArtDocument.swift; sourceTree = ""; }; 50 | 83A97396249863D60056E875 /* EmojiArtDocumentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiArtDocumentView.swift; sourceTree = ""; }; 51 | 83A97398249866F80056E875 /* EmojiArt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiArt.swift; sourceTree = ""; }; 52 | 83A9739A249869270056E875 /* EmojiArtExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiArtExtensions.swift; sourceTree = ""; }; 53 | /* End PBXFileReference section */ 54 | 55 | /* Begin PBXFrameworksBuildPhase section */ 56 | 83A9737A249863310056E875 /* Frameworks */ = { 57 | isa = PBXFrameworksBuildPhase; 58 | buildActionMask = 2147483647; 59 | files = ( 60 | ); 61 | runOnlyForDeploymentPostprocessing = 0; 62 | }; 63 | /* End PBXFrameworksBuildPhase section */ 64 | 65 | /* Begin PBXGroup section */ 66 | 83A97374249863310056E875 = { 67 | isa = PBXGroup; 68 | children = ( 69 | 83A9737F249863320056E875 /* EmojiArt */, 70 | 83A9737E249863320056E875 /* Products */, 71 | ); 72 | sourceTree = ""; 73 | }; 74 | 83A9737E249863320056E875 /* Products */ = { 75 | isa = PBXGroup; 76 | children = ( 77 | 83A9737D249863320056E875 /* Emoji Art.app */, 78 | ); 79 | name = Products; 80 | sourceTree = ""; 81 | }; 82 | 83A9737F249863320056E875 /* EmojiArt */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | 83A97394249863560056E875 /* EmojiArtDocument.swift */, 86 | 833C138A24AD3A4100DA6BAA /* EmojiArtDocumentChooser.swift */, 87 | 833C138024A98D2A00DA6BAA /* PaletteChooser.swift */, 88 | 833C138224A9933E00DA6BAA /* EmojiArtDocument+Palette.swift */, 89 | 833C138C24AD615000DA6BAA /* EditableText.swift */, 90 | 833C138824AD1D2A00DA6BAA /* EmojiArtDocumentStore.swift */, 91 | 83A97380249863320056E875 /* AppDelegate.swift */, 92 | 83A97382249863320056E875 /* SceneDelegate.swift */, 93 | 83A97396249863D60056E875 /* EmojiArtDocumentView.swift */, 94 | 833C138424AC453200DA6BAA /* Grid.swift */, 95 | 833C138624AC454600DA6BAA /* GridLayout.swift */, 96 | 83A97398249866F80056E875 /* EmojiArt.swift */, 97 | 83A9739A249869270056E875 /* EmojiArtExtensions.swift */, 98 | 833C137E24A95F6900DA6BAA /* Spinning.swift */, 99 | 83A97386249863340056E875 /* Assets.xcassets */, 100 | 83A9738B249863340056E875 /* LaunchScreen.storyboard */, 101 | 83A9738E249863340056E875 /* Info.plist */, 102 | 833C136A24A4140500DA6BAA /* OptionalImage.swift */, 103 | 833C136C24A41FA400DA6BAA /* AnimatableSystemFontModifier.swift */, 104 | 83A97388249863340056E875 /* Preview Content */, 105 | ); 106 | path = EmojiArt; 107 | sourceTree = ""; 108 | }; 109 | 83A97388249863340056E875 /* Preview Content */ = { 110 | isa = PBXGroup; 111 | children = ( 112 | 83A97389249863340056E875 /* Preview Assets.xcassets */, 113 | ); 114 | path = "Preview Content"; 115 | sourceTree = ""; 116 | }; 117 | /* End PBXGroup section */ 118 | 119 | /* Begin PBXNativeTarget section */ 120 | 83A9737C249863310056E875 /* EmojiArt */ = { 121 | isa = PBXNativeTarget; 122 | buildConfigurationList = 83A97391249863340056E875 /* Build configuration list for PBXNativeTarget "EmojiArt" */; 123 | buildPhases = ( 124 | 83A97379249863310056E875 /* Sources */, 125 | 83A9737A249863310056E875 /* Frameworks */, 126 | 83A9737B249863310056E875 /* Resources */, 127 | ); 128 | buildRules = ( 129 | ); 130 | dependencies = ( 131 | ); 132 | name = EmojiArt; 133 | productName = EmojiArt; 134 | productReference = 83A9737D249863320056E875 /* Emoji Art.app */; 135 | productType = "com.apple.product-type.application"; 136 | }; 137 | /* End PBXNativeTarget section */ 138 | 139 | /* Begin PBXProject section */ 140 | 83A97375249863310056E875 /* Project object */ = { 141 | isa = PBXProject; 142 | attributes = { 143 | LastSwiftUpdateCheck = 1140; 144 | LastUpgradeCheck = 1140; 145 | ORGANIZATIONNAME = "Tieda Wei"; 146 | TargetAttributes = { 147 | 83A9737C249863310056E875 = { 148 | CreatedOnToolsVersion = 11.4.1; 149 | }; 150 | }; 151 | }; 152 | buildConfigurationList = 83A97378249863310056E875 /* Build configuration list for PBXProject "EmojiArt" */; 153 | compatibilityVersion = "Xcode 9.3"; 154 | developmentRegion = en; 155 | hasScannedForEncodings = 0; 156 | knownRegions = ( 157 | en, 158 | Base, 159 | ); 160 | mainGroup = 83A97374249863310056E875; 161 | productRefGroup = 83A9737E249863320056E875 /* Products */; 162 | projectDirPath = ""; 163 | projectRoot = ""; 164 | targets = ( 165 | 83A9737C249863310056E875 /* EmojiArt */, 166 | ); 167 | }; 168 | /* End PBXProject section */ 169 | 170 | /* Begin PBXResourcesBuildPhase section */ 171 | 83A9737B249863310056E875 /* Resources */ = { 172 | isa = PBXResourcesBuildPhase; 173 | buildActionMask = 2147483647; 174 | files = ( 175 | 83A9738D249863340056E875 /* LaunchScreen.storyboard in Resources */, 176 | 83A9738A249863340056E875 /* Preview Assets.xcassets in Resources */, 177 | 83A97387249863340056E875 /* Assets.xcassets in Resources */, 178 | ); 179 | runOnlyForDeploymentPostprocessing = 0; 180 | }; 181 | /* End PBXResourcesBuildPhase section */ 182 | 183 | /* Begin PBXSourcesBuildPhase section */ 184 | 83A97379249863310056E875 /* Sources */ = { 185 | isa = PBXSourcesBuildPhase; 186 | buildActionMask = 2147483647; 187 | files = ( 188 | 83A97397249863D60056E875 /* EmojiArtDocumentView.swift in Sources */, 189 | 83A97381249863320056E875 /* AppDelegate.swift in Sources */, 190 | 833C138324A9933E00DA6BAA /* EmojiArtDocument+Palette.swift in Sources */, 191 | 83A9739B249869270056E875 /* EmojiArtExtensions.swift in Sources */, 192 | 83A97399249866F80056E875 /* EmojiArt.swift in Sources */, 193 | 83A97383249863320056E875 /* SceneDelegate.swift in Sources */, 194 | 833C138724AC454600DA6BAA /* GridLayout.swift in Sources */, 195 | 833C136B24A4140600DA6BAA /* OptionalImage.swift in Sources */, 196 | 833C137F24A95F6900DA6BAA /* Spinning.swift in Sources */, 197 | 833C138924AD1D2A00DA6BAA /* EmojiArtDocumentStore.swift in Sources */, 198 | 833C136D24A41FA400DA6BAA /* AnimatableSystemFontModifier.swift in Sources */, 199 | 833C138B24AD3A4100DA6BAA /* EmojiArtDocumentChooser.swift in Sources */, 200 | 833C138D24AD615000DA6BAA /* EditableText.swift in Sources */, 201 | 83A97395249863560056E875 /* EmojiArtDocument.swift in Sources */, 202 | 833C138124A98D2A00DA6BAA /* PaletteChooser.swift in Sources */, 203 | 833C138524AC453200DA6BAA /* Grid.swift in Sources */, 204 | ); 205 | runOnlyForDeploymentPostprocessing = 0; 206 | }; 207 | /* End PBXSourcesBuildPhase section */ 208 | 209 | /* Begin PBXVariantGroup section */ 210 | 83A9738B249863340056E875 /* LaunchScreen.storyboard */ = { 211 | isa = PBXVariantGroup; 212 | children = ( 213 | 83A9738C249863340056E875 /* Base */, 214 | ); 215 | name = LaunchScreen.storyboard; 216 | sourceTree = ""; 217 | }; 218 | /* End PBXVariantGroup section */ 219 | 220 | /* Begin XCBuildConfiguration section */ 221 | 83A9738F249863340056E875 /* Debug */ = { 222 | isa = XCBuildConfiguration; 223 | buildSettings = { 224 | ALWAYS_SEARCH_USER_PATHS = NO; 225 | CLANG_ANALYZER_NONNULL = YES; 226 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 227 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 228 | CLANG_CXX_LIBRARY = "libc++"; 229 | CLANG_ENABLE_MODULES = YES; 230 | CLANG_ENABLE_OBJC_ARC = YES; 231 | CLANG_ENABLE_OBJC_WEAK = YES; 232 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 233 | CLANG_WARN_BOOL_CONVERSION = YES; 234 | CLANG_WARN_COMMA = YES; 235 | CLANG_WARN_CONSTANT_CONVERSION = YES; 236 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 237 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 238 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 239 | CLANG_WARN_EMPTY_BODY = YES; 240 | CLANG_WARN_ENUM_CONVERSION = YES; 241 | CLANG_WARN_INFINITE_RECURSION = YES; 242 | CLANG_WARN_INT_CONVERSION = YES; 243 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 244 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 245 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 246 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 247 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 248 | CLANG_WARN_STRICT_PROTOTYPES = YES; 249 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 250 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 251 | CLANG_WARN_UNREACHABLE_CODE = YES; 252 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 253 | COPY_PHASE_STRIP = NO; 254 | DEBUG_INFORMATION_FORMAT = dwarf; 255 | ENABLE_STRICT_OBJC_MSGSEND = YES; 256 | ENABLE_TESTABILITY = YES; 257 | GCC_C_LANGUAGE_STANDARD = gnu11; 258 | GCC_DYNAMIC_NO_PIC = NO; 259 | GCC_NO_COMMON_BLOCKS = YES; 260 | GCC_OPTIMIZATION_LEVEL = 0; 261 | GCC_PREPROCESSOR_DEFINITIONS = ( 262 | "DEBUG=1", 263 | "$(inherited)", 264 | ); 265 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 266 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 267 | GCC_WARN_UNDECLARED_SELECTOR = YES; 268 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 269 | GCC_WARN_UNUSED_FUNCTION = YES; 270 | GCC_WARN_UNUSED_VARIABLE = YES; 271 | IPHONEOS_DEPLOYMENT_TARGET = 13.4; 272 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 273 | MTL_FAST_MATH = YES; 274 | ONLY_ACTIVE_ARCH = YES; 275 | SDKROOT = iphoneos; 276 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 277 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 278 | }; 279 | name = Debug; 280 | }; 281 | 83A97390249863340056E875 /* Release */ = { 282 | isa = XCBuildConfiguration; 283 | buildSettings = { 284 | ALWAYS_SEARCH_USER_PATHS = NO; 285 | CLANG_ANALYZER_NONNULL = YES; 286 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 287 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 288 | CLANG_CXX_LIBRARY = "libc++"; 289 | CLANG_ENABLE_MODULES = YES; 290 | CLANG_ENABLE_OBJC_ARC = YES; 291 | CLANG_ENABLE_OBJC_WEAK = YES; 292 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 293 | CLANG_WARN_BOOL_CONVERSION = YES; 294 | CLANG_WARN_COMMA = YES; 295 | CLANG_WARN_CONSTANT_CONVERSION = YES; 296 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 297 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 298 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 299 | CLANG_WARN_EMPTY_BODY = YES; 300 | CLANG_WARN_ENUM_CONVERSION = YES; 301 | CLANG_WARN_INFINITE_RECURSION = YES; 302 | CLANG_WARN_INT_CONVERSION = YES; 303 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 304 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 305 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 306 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 307 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 308 | CLANG_WARN_STRICT_PROTOTYPES = YES; 309 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 310 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 311 | CLANG_WARN_UNREACHABLE_CODE = YES; 312 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 313 | COPY_PHASE_STRIP = NO; 314 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 315 | ENABLE_NS_ASSERTIONS = NO; 316 | ENABLE_STRICT_OBJC_MSGSEND = YES; 317 | GCC_C_LANGUAGE_STANDARD = gnu11; 318 | GCC_NO_COMMON_BLOCKS = YES; 319 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 320 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 321 | GCC_WARN_UNDECLARED_SELECTOR = YES; 322 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 323 | GCC_WARN_UNUSED_FUNCTION = YES; 324 | GCC_WARN_UNUSED_VARIABLE = YES; 325 | IPHONEOS_DEPLOYMENT_TARGET = 13.4; 326 | MTL_ENABLE_DEBUG_INFO = NO; 327 | MTL_FAST_MATH = YES; 328 | SDKROOT = iphoneos; 329 | SWIFT_COMPILATION_MODE = wholemodule; 330 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 331 | VALIDATE_PRODUCT = YES; 332 | }; 333 | name = Release; 334 | }; 335 | 83A97392249863340056E875 /* Debug */ = { 336 | isa = XCBuildConfiguration; 337 | buildSettings = { 338 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 339 | CODE_SIGN_STYLE = Automatic; 340 | DEVELOPMENT_ASSET_PATHS = "\"EmojiArt/Preview Content\""; 341 | DEVELOPMENT_TEAM = T64RAU2S7P; 342 | ENABLE_PREVIEWS = YES; 343 | INFOPLIST_FILE = EmojiArt/Info.plist; 344 | LD_RUNPATH_SEARCH_PATHS = ( 345 | "$(inherited)", 346 | "@executable_path/Frameworks", 347 | ); 348 | PRODUCT_BUNDLE_IDENTIFIER = com.tiedawei.EmojiArt; 349 | PRODUCT_NAME = "Emoji Art"; 350 | SWIFT_VERSION = 5.0; 351 | TARGETED_DEVICE_FAMILY = "1,2"; 352 | }; 353 | name = Debug; 354 | }; 355 | 83A97393249863340056E875 /* Release */ = { 356 | isa = XCBuildConfiguration; 357 | buildSettings = { 358 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 359 | CODE_SIGN_STYLE = Automatic; 360 | DEVELOPMENT_ASSET_PATHS = "\"EmojiArt/Preview Content\""; 361 | DEVELOPMENT_TEAM = T64RAU2S7P; 362 | ENABLE_PREVIEWS = YES; 363 | INFOPLIST_FILE = EmojiArt/Info.plist; 364 | LD_RUNPATH_SEARCH_PATHS = ( 365 | "$(inherited)", 366 | "@executable_path/Frameworks", 367 | ); 368 | PRODUCT_BUNDLE_IDENTIFIER = com.tiedawei.EmojiArt; 369 | PRODUCT_NAME = "Emoji Art"; 370 | SWIFT_VERSION = 5.0; 371 | TARGETED_DEVICE_FAMILY = "1,2"; 372 | }; 373 | name = Release; 374 | }; 375 | /* End XCBuildConfiguration section */ 376 | 377 | /* Begin XCConfigurationList section */ 378 | 83A97378249863310056E875 /* Build configuration list for PBXProject "EmojiArt" */ = { 379 | isa = XCConfigurationList; 380 | buildConfigurations = ( 381 | 83A9738F249863340056E875 /* Debug */, 382 | 83A97390249863340056E875 /* Release */, 383 | ); 384 | defaultConfigurationIsVisible = 0; 385 | defaultConfigurationName = Release; 386 | }; 387 | 83A97391249863340056E875 /* Build configuration list for PBXNativeTarget "EmojiArt" */ = { 388 | isa = XCConfigurationList; 389 | buildConfigurations = ( 390 | 83A97392249863340056E875 /* Debug */, 391 | 83A97393249863340056E875 /* Release */, 392 | ); 393 | defaultConfigurationIsVisible = 0; 394 | defaultConfigurationName = Release; 395 | }; 396 | /* End XCConfigurationList section */ 397 | }; 398 | rootObject = 83A97375249863310056E875 /* Project object */; 399 | } 400 | -------------------------------------------------------------------------------- /projects/EmojiArt/EmojiArt.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /projects/EmojiArt/EmojiArt.xcodeproj/project.xcworkspace/xcuserdata/Tieda.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weitieda/cs193p-2020-swiftui/ba90cdc1d0b0ae59e386e9640c2f2d27bc2bcc15/projects/EmojiArt/EmojiArt.xcodeproj/project.xcworkspace/xcuserdata/Tieda.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /projects/EmojiArt/EmojiArt.xcodeproj/xcuserdata/Tieda.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | EmojiArt.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /projects/EmojiArt/EmojiArt/AnimatableSystemFontModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimatableSystemFontModifier.swift 3 | // EmojiArt 4 | // 5 | // Created by Tieda Wei on 2020-06-24. 6 | // Copyright © 2020 Tieda Wei. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct AnimatableSystemFontModifier: AnimatableModifier { 12 | var size: CGFloat 13 | var weight: Font.Weight = .regular 14 | var design: Font.Design = .default 15 | 16 | func body(content: Content) -> some View { 17 | content.font(Font.system(size: size, weight: weight, design: design)) 18 | } 19 | 20 | var animatableData: CGFloat { 21 | get { size } 22 | set { size = newValue } 23 | } 24 | } 25 | 26 | extension View { 27 | func font(animatableWithSize size: CGFloat, weight: Font.Weight = .regular, design: Font.Design = .default) -> some View { 28 | self.modifier(AnimatableSystemFontModifier(size: size, weight: weight, design: design)) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /projects/EmojiArt/EmojiArt/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // EmojiArt 4 | // 5 | // Created by Tieda Wei on 2020-06-15. 6 | // Copyright © 2020 Tieda Wei. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | // MARK: UISceneSession Lifecycle 22 | 23 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 24 | // Called when a new scene session is being created. 25 | // Use this method to select a configuration to create the new scene with. 26 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 27 | } 28 | 29 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 30 | // Called when the user discards a scene session. 31 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 32 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 33 | } 34 | 35 | 36 | } 37 | 38 | -------------------------------------------------------------------------------- /projects/EmojiArt/EmojiArt/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /projects/EmojiArt/EmojiArt/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /projects/EmojiArt/EmojiArt/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 | -------------------------------------------------------------------------------- /projects/EmojiArt/EmojiArt/EditableText.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EditableText.swift 3 | // EmojiArt 4 | // 5 | // Created by CS193p Instructor on 5/6/20. 6 | // Copyright © 2020 Stanford University. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct EditableText: View { 12 | var text: String = "" 13 | var isEditing: Bool 14 | var onChanged: (String) -> Void 15 | 16 | init(_ text: String, isEditing: Bool, onChanged: @escaping (String) -> Void) { 17 | self.text = text 18 | self.isEditing = isEditing 19 | self.onChanged = onChanged 20 | } 21 | 22 | @State private var editableText: String = "" 23 | 24 | var body: some View { 25 | ZStack(alignment: .leading) { 26 | TextField(text, text: $editableText, onEditingChanged: { began in 27 | self.callOnChangedIfChanged() 28 | }) 29 | .opacity(isEditing ? 1 : 0) 30 | .disabled(!isEditing) 31 | if !isEditing { 32 | Text(text) 33 | .opacity(isEditing ? 0 : 1) 34 | .onAppear { 35 | // any time we move from editable to non-editable 36 | // we want to report any changes that happened to the text 37 | // while were editable 38 | // (i.e. we never "abandon" changes) 39 | self.callOnChangedIfChanged() 40 | } 41 | } 42 | } 43 | .onAppear { self.editableText = self.text } 44 | } 45 | 46 | func callOnChangedIfChanged() { 47 | if editableText != text { 48 | onChanged(editableText) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /projects/EmojiArt/EmojiArt/EmojiArt.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmojiArt.swift 3 | // EmojiArt 4 | // 5 | // Created by Tieda Wei on 2020-06-15. 6 | // Copyright © 2020 Tieda Wei. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct EmojiArt: Codable { 12 | var backgroundURL: URL? 13 | var emojis = [Emoji]() 14 | 15 | var json: Data? { 16 | try? JSONEncoder().encode(self) 17 | } 18 | 19 | private var uniqueEmojiId = 0 20 | 21 | init() { } 22 | 23 | init?(json: Data?) { 24 | if let json = json, let newEmojiArt = try? JSONDecoder().decode(EmojiArt.self, from: json) { 25 | self = newEmojiArt 26 | } else { 27 | return nil 28 | } 29 | } 30 | 31 | struct Emoji: Identifiable, Codable { 32 | let text: String 33 | var x, y: Int 34 | var size: Int 35 | let id: Int 36 | 37 | fileprivate init(text: String, x: Int, y: Int, size: Int, id: Int) { 38 | self.text = text 39 | self.x = x 40 | self.y = y 41 | self.size = size 42 | self.id = id 43 | } 44 | } 45 | 46 | mutating func addEmoji(_ text: String, x: Int, y: Int, size: Int) { 47 | uniqueEmojiId += 1 48 | emojis.append(Emoji(text: text, x: x, y: y, size: size, id: uniqueEmojiId)) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /projects/EmojiArt/EmojiArt/EmojiArtDocument+Palette.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmojiArtDocument+Palette.swift 3 | // EmojiArt 4 | // 5 | // Created by Tieda Wei on 2020-06-28. 6 | // Copyright © 2020 Tieda Wei. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // MARK: - Palette Extension 12 | 13 | extension EmojiArtDocument 14 | { 15 | private static let PalettesKey = "EmojiArtDocument.PalettesKey" 16 | 17 | // even though this is an instance var, it is shared across instances 18 | // and is also persistent across application launches 19 | private(set) var paletteNames: [String:String] { 20 | get { 21 | UserDefaults.standard.object(forKey: EmojiArtDocument.PalettesKey) as? [String:String] ?? [ 22 | "😀😅😂😇🥰😉🙃😎🥳😡🤯🥶🤥😴🙄👿😷🤧🤡":"Faces", 23 | "🍏🍎🥒🍞🥨🥓🍔🍟🍕🍰🍿☕️":"Food", 24 | "🐶🐼🐵🙈🙉🙊🦆🐝🕷🐟🦓🐪🦒🦨":"Animals", 25 | "⚽️🏈⚾️🎾🏐🏓⛳️🥌⛷🚴‍♂️🎳🎼🎭🪂":"Activities" 26 | ] 27 | } 28 | set { 29 | UserDefaults.standard.set(newValue, forKey: EmojiArtDocument.PalettesKey) 30 | objectWillChange.send() 31 | } 32 | } 33 | 34 | var sortedPalettes: [String] { 35 | paletteNames.keys.sorted(by: { paletteNames[$0]! < paletteNames[$1]! }) 36 | } 37 | 38 | var defaultPalette: String { 39 | sortedPalettes.first ?? "⚠️" 40 | } 41 | 42 | func renamePalette(_ palette: String, to name: String) { 43 | paletteNames[palette] = name 44 | } 45 | 46 | func addPalette(_ palette: String, named name: String) { 47 | paletteNames[name] = palette 48 | } 49 | 50 | func removePalette(named name: String) { 51 | paletteNames[name] = nil 52 | } 53 | 54 | @discardableResult 55 | func addEmoji(_ emoji: String, toPalette palette: String) -> String { 56 | return changePalette(palette, to: (emoji + palette).uniqued()) 57 | } 58 | 59 | @discardableResult 60 | func removeEmoji(_ emojisToRemove: String, fromPalette palette: String) -> String { 61 | return changePalette(palette, to: palette.filter { !emojisToRemove.contains($0) }) 62 | } 63 | 64 | private func changePalette(_ palette: String, to newPalette: String) -> String { 65 | let name = paletteNames[palette] ?? "" 66 | paletteNames[palette] = nil 67 | paletteNames[newPalette] = name 68 | return newPalette 69 | } 70 | 71 | func palette(after otherPalette: String) -> String { 72 | palette(offsetBy: +1, from: otherPalette) 73 | } 74 | 75 | func palette(before otherPalette: String) -> String { 76 | palette(offsetBy: -1, from: otherPalette) 77 | } 78 | 79 | private func palette(offsetBy offset: Int, from otherPalette: String) -> String { 80 | if let currentIndex = mostLikelyIndex(of: otherPalette) { 81 | let newIndex = (currentIndex + (offset >= 0 ? offset : sortedPalettes.count - abs(offset) % sortedPalettes.count)) % sortedPalettes.count 82 | return sortedPalettes[newIndex] 83 | } else { 84 | return defaultPalette 85 | } 86 | } 87 | 88 | // this is a trick to make the code in the demo a little bit simpler 89 | // in the real world, we'd want palettes to be Identifiable 90 | // here we're simply guessing at that 😀 91 | private func mostLikelyIndex(of palette: String) -> Int? { 92 | let paletteSet = Set(palette) 93 | var best: (index: Int, score: Int)? 94 | let palettes = sortedPalettes 95 | for index in palettes.indices { 96 | let score = paletteSet.intersection(Set(palettes[index])).count 97 | if score > (best?.score ?? 0) { 98 | best = (index, score) 99 | } 100 | } 101 | return best?.index 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /projects/EmojiArt/EmojiArt/EmojiArtDocument.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmojiArtDocument.swift 3 | // EmojiArt 4 | // 5 | // Created by Tieda Wei on 2020-06-15. 6 | // Copyright © 2020 Tieda Wei. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | 12 | class EmojiArtDocument: ObservableObject, Identifiable, Hashable, Equatable { 13 | 14 | static let palette = "🥳👍🏼💪🏼🦞" 15 | 16 | @Published private(set) var backgroundImage: UIImage? 17 | @Published private var emojiArt: EmojiArt 18 | 19 | @Published var steadyStateZoomScale: CGFloat = 1.0 20 | @Published var steadyStatePanOffset: CGSize = .zero 21 | 22 | private var autosaveCancellable: AnyCancellable? 23 | private var fetchImageCancellable: AnyCancellable? 24 | 25 | let id: UUID 26 | static func == (lhs: EmojiArtDocument, rhs: EmojiArtDocument) -> Bool { lhs.id == rhs.id } 27 | func hash(into hasher: inout Hasher) { 28 | hasher.combine(id) 29 | } 30 | 31 | var emojis: [EmojiArt.Emoji] { emojiArt.emojis } 32 | 33 | var backgroundUrl: URL? { 34 | get { emojiArt.backgroundURL } 35 | set { emojiArt.backgroundURL = newValue?.imageURL; fetchBackgroundImageData() } 36 | } 37 | 38 | init(id: UUID? = nil) { 39 | self.id = id ?? UUID() 40 | let defaultsKey = "EmojiArtDocument.\(self.id.uuidString)" 41 | emojiArt = EmojiArt(json: UserDefaults.standard.data(forKey: defaultsKey)) ?? EmojiArt() 42 | autosaveCancellable = $emojiArt.sink { emojiArt in 43 | UserDefaults.standard.set(emojiArt.json, forKey: defaultsKey) 44 | } 45 | fetchBackgroundImageData() 46 | } 47 | 48 | // MARK: - Intent(s) 49 | 50 | func addEmoji(_ emoji: String, at location: CGPoint, size: CGFloat) { 51 | emojiArt.addEmoji(emoji, x: Int(location.x), y: Int(location.y), size: Int(size)) 52 | } 53 | 54 | func moveEmoji(_ emoji: EmojiArt.Emoji, by offset: CGSize) { 55 | if let index = emojiArt.emojis.firstIndex(matching: emoji) { 56 | emojiArt.emojis[index].x += Int(offset.width) 57 | emojiArt.emojis[index].y += Int(offset.height) 58 | } 59 | } 60 | 61 | func scaleEmoji(_ emoji: EmojiArt.Emoji, by scale: CGFloat) { 62 | if let index = emojiArt.emojis.firstIndex(matching: emoji) { 63 | emojiArt.emojis[index].size = Int((CGFloat(emojiArt.emojis[index].size) * scale).rounded(.toNearestOrEven)) 64 | } 65 | } 66 | 67 | private func fetchBackgroundImageData() { 68 | backgroundImage = nil 69 | guard let url = self.emojiArt.backgroundURL else { return } 70 | fetchImageCancellable?.cancel() 71 | fetchImageCancellable = URLSession.shared 72 | .dataTaskPublisher(for: url) 73 | .map {data, response in UIImage(data: data)} 74 | .receive(on: DispatchQueue.main) 75 | .replaceError(with: nil) 76 | .assign(to: \.backgroundImage, on: self) 77 | } 78 | } 79 | 80 | extension EmojiArt.Emoji { 81 | var fontSize: CGFloat { CGFloat(self.size) } 82 | var location: CGPoint { CGPoint(x: CGFloat(x), y: CGFloat(y)) } 83 | } 84 | -------------------------------------------------------------------------------- /projects/EmojiArt/EmojiArt/EmojiArtDocumentChooser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmojiArtDocumentChooser.swift 3 | // EmojiArt 4 | // 5 | // Created by Tieda Wei on 2020-07-01. 6 | // Copyright © 2020 Tieda Wei. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct EmojiArtDocumentChooser: View { 12 | 13 | @EnvironmentObject var store: EmojiArtDocumentStore 14 | 15 | @State private var editMode: EditMode = .inactive 16 | 17 | var body: some View { 18 | NavigationView { 19 | List { 20 | ForEach(store.documents) { document in 21 | NavigationLink(destination: EmojiArtDocumentView(document: document).navigationBarTitle(self.store.name(for: document)) 22 | ) { 23 | EditableText(self.store.name(for: document), isEditing: self.editMode.isEditing) { name in 24 | self.store.setName(name, for: document) 25 | } 26 | } 27 | } 28 | .onDelete { indexSet in 29 | indexSet.map { self.store.documents[$0] }.forEach { document in 30 | self.store.removeDocument(document) 31 | } 32 | } 33 | } 34 | .navigationBarTitle(self.store.name) 35 | .navigationBarItems( 36 | leading: Button(action: { 37 | self.store.addDocument() 38 | }, label: { 39 | Image(systemName: "plus").imageScale(.large) 40 | }), 41 | trailing: EditButton() 42 | ) 43 | .environment(\.editMode, $editMode) 44 | } 45 | } 46 | } 47 | 48 | struct EmojiArtDocumentChooser_Previews: PreviewProvider { 49 | static var previews: some View { 50 | EmojiArtDocumentChooser() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /projects/EmojiArt/EmojiArt/EmojiArtDocumentStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmojiArtDocumentStore.swift 3 | // EmojiArt 4 | // 5 | // Created by CS193p Instructor on 5/6/20. 6 | // Copyright © 2020 Stanford University. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | 12 | class EmojiArtDocumentStore: ObservableObject { 13 | let name: String 14 | 15 | @Published private var documentNames = [EmojiArtDocument:String]() 16 | 17 | private var autosave: AnyCancellable? 18 | 19 | var documents: [EmojiArtDocument] { 20 | documentNames.keys.sorted { documentNames[$0]! < documentNames[$1]! } 21 | } 22 | 23 | init(named name: String = "Emoji Art") { 24 | self.name = name 25 | let defaultsKey = "EmojiArtDocumentStore.\(name)" 26 | documentNames = Dictionary(fromPropertyList: UserDefaults.standard.object(forKey: defaultsKey)) 27 | autosave = $documentNames.sink { names in 28 | UserDefaults.standard.set(names.asPropertyList, forKey: defaultsKey) 29 | } 30 | } 31 | 32 | func name(for document: EmojiArtDocument) -> String { 33 | if documentNames[document] == nil { 34 | documentNames[document] = "Untitled" 35 | } 36 | return documentNames[document]! 37 | } 38 | 39 | func setName(_ name: String, for document: EmojiArtDocument) { 40 | documentNames[document] = name 41 | } 42 | 43 | 44 | func addDocument(named name: String = "Untitled") { 45 | documentNames[EmojiArtDocument()] = name 46 | } 47 | 48 | func removeDocument(_ document: EmojiArtDocument) { 49 | documentNames[document] = nil 50 | } 51 | 52 | } 53 | 54 | extension Dictionary where Key == EmojiArtDocument, Value == String { 55 | var asPropertyList: [String:String] { 56 | var uuidToName = [String:String]() 57 | for (key, value) in self { 58 | uuidToName[key.id.uuidString] = value 59 | } 60 | return uuidToName 61 | } 62 | 63 | init(fromPropertyList plist: Any?) { 64 | self.init() 65 | let uuidToName = plist as? [String:String] ?? [:] 66 | for uuid in uuidToName.keys { 67 | self[EmojiArtDocument(id: UUID(uuidString: uuid))] = uuidToName[uuid] 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /projects/EmojiArt/EmojiArt/EmojiArtDocumentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmojiArtDocumentView.swift 3 | // EmojiArt 4 | // 5 | // Created by Tieda Wei on 2020-06-15. 6 | // Copyright © 2020 Tieda Wei. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct EmojiArtDocumentView: View { 12 | 13 | private let defaultEmojiSize: CGFloat = 40 14 | 15 | @ObservedObject var document: EmojiArtDocument 16 | @State private var chosenPalette: String 17 | @State private var explainBackgroundPaste = false 18 | @State private var confirmBackgroundPaste = false 19 | 20 | @GestureState private var gestureZoomScale: CGFloat = 1.0 21 | @GestureState private var gesturePanOffset: CGSize = .zero 22 | 23 | private var isLoading: Bool { document.backgroundImage == nil && document.backgroundUrl != nil } 24 | private var zoomScale: CGFloat { document.steadyStateZoomScale * gestureZoomScale } 25 | private var panOffset: CGSize { (document.steadyStatePanOffset + gesturePanOffset) * zoomScale } 26 | 27 | init(document: EmojiArtDocument) { 28 | self.document = document 29 | _chosenPalette = State(wrappedValue: document.defaultPalette) 30 | } 31 | 32 | var body: some View { 33 | VStack { 34 | HStack { 35 | PaletteChooser(document: document, chosenPalette: $chosenPalette) 36 | ScrollView(.horizontal) { 37 | HStack { 38 | ForEach(chosenPalette.map { String($0) }, id: \.self) { emoji in 39 | Text(emoji) 40 | .font(.system(size: self.defaultEmojiSize)) 41 | .onDrag { NSItemProvider(object: emoji as NSString) } 42 | } 43 | } 44 | } 45 | } 46 | 47 | GeometryReader { geometry in 48 | ZStack { 49 | Color.white.overlay( 50 | OptionalImage(uiImage: self.document.backgroundImage) 51 | .scaleEffect(self.zoomScale) 52 | .offset(self.panOffset) 53 | ).gesture(self.doubleTapToZoom(in: geometry.size)) 54 | 55 | if self.isLoading { 56 | Image(systemName: "hourglass").imageScale(.large).spinning() 57 | } else { 58 | ForEach(self.document.emojis) { emoji in 59 | Text(emoji.text).font(animatableWithSize: emoji.fontSize * self.zoomScale).position(self.position(for: emoji, in: geometry.size)) 60 | } 61 | } 62 | } 63 | .clipped() 64 | .gesture(self.panGesture()) // pan gesture has to come first? 65 | .gesture(self.zoomGesture()) 66 | .edgesIgnoringSafeArea([.horizontal, .bottom]) 67 | .onReceive(self.document.$backgroundImage) { self.zoomToFit($0, in: geometry.size) } 68 | .onDrop(of: ["public.image", "public.text"], isTargeted: nil) { providers, location in 69 | // SwiftUI bug (as of 13.4)? the location is supposed to be in our coordinate system 70 | // however, the y coordinate appears to be in the global coordinate system 71 | var location = geometry.convert(location, from: .global) 72 | location = CGPoint(x: location.x - geometry.size.width / 2, y: location.y - geometry.size.height / 2) 73 | location = CGPoint(x: location.x - self.panOffset.width, y: location.y - self.panOffset.height) 74 | location = CGPoint(x: location.x / self.zoomScale, y: location.y / self.zoomScale) 75 | return self.drop(providers: providers, at: location) 76 | } 77 | .navigationBarItems(trailing: Button(action: { 78 | if let url = UIPasteboard.general.url, url != self.document.backgroundUrl { 79 | self.confirmBackgroundPaste = true 80 | } else { 81 | self.explainBackgroundPaste = true 82 | } 83 | }, label: { 84 | Image(systemName: "doc.on.clipboard") 85 | .imageScale(.large) 86 | .alert(isPresented: self.$explainBackgroundPaste) { 87 | return Alert( 88 | title: Text("Paste Background"), 89 | message: Text("Copy the URL of an image to the clip board and touch this button to make it the background of your document."), 90 | dismissButton: .default(Text("OK")) 91 | ) 92 | } 93 | })) 94 | }.zIndex(-1) 95 | }.alert(isPresented: self.$confirmBackgroundPaste) { 96 | Alert( 97 | title: Text("Paste Background"), 98 | message: Text("Replace your background with \(UIPasteboard.general.url?.absoluteString ?? "nothing")?."), 99 | primaryButton: .default(Text("OK")) { 100 | self.document.backgroundUrl = UIPasteboard.general.url 101 | }, 102 | secondaryButton: .cancel() 103 | ) 104 | } 105 | } 106 | 107 | private func panGesture() -> some Gesture { 108 | DragGesture() 109 | .updating($gesturePanOffset) { latestDragGestureValue, gesturePanOffset, transaction in 110 | gesturePanOffset = latestDragGestureValue.translation / self.zoomScale 111 | } 112 | .onEnded { finalDragGestureValue in 113 | self.document.steadyStatePanOffset = self.document.steadyStatePanOffset + (finalDragGestureValue.translation / self.zoomScale) 114 | } 115 | } 116 | 117 | private func zoomGesture() -> some Gesture { 118 | MagnificationGesture() 119 | .updating($gestureZoomScale) { latestGestureScale, inOutGestureState, transaction in 120 | inOutGestureState = latestGestureScale 121 | } 122 | .onEnded { (finalGestureScale) in 123 | self.document.steadyStateZoomScale = finalGestureScale 124 | } 125 | } 126 | 127 | private func doubleTapToZoom(in size: CGSize) -> some Gesture { 128 | TapGesture(count: 2) 129 | .onEnded { 130 | withAnimation { 131 | self.zoomToFit(self.document.backgroundImage, in: size) 132 | } 133 | } 134 | } 135 | 136 | private func zoomToFit(_ image: UIImage?, in size: CGSize) { 137 | if let image = image, image.size.height > 0, image.size.width > 0, size.height > 0, size.width > 0 { 138 | let hZoom = size.width / image.size.width 139 | let vZoom = size.height / image.size.height 140 | self.document.steadyStatePanOffset = .zero 141 | self.document.steadyStateZoomScale = min(hZoom, vZoom) 142 | } 143 | } 144 | 145 | private func position(for emoji: EmojiArt.Emoji, in size: CGSize) -> CGPoint { 146 | var location = emoji.location 147 | location = CGPoint(x: location.x * zoomScale, y: location.y * zoomScale) 148 | location = CGPoint(x: location.x + size.width/2, y: location.y + size.height/2) 149 | location = CGPoint(x: location.x + self.panOffset.width, y: location.y + self.panOffset.height) 150 | return location 151 | } 152 | 153 | private func drop(providers: [NSItemProvider], at location: CGPoint) -> Bool { 154 | var found = providers.loadFirstObject(ofType: URL.self) { url in 155 | self.document.backgroundUrl = url 156 | } 157 | if !found { 158 | found = providers.loadObjects(ofType: String.self) { string in 159 | self.document.addEmoji(string, at: location, size: self.defaultEmojiSize) 160 | } 161 | } 162 | return found 163 | } 164 | } 165 | 166 | -------------------------------------------------------------------------------- /projects/EmojiArt/EmojiArt/EmojiArtExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmojiArtExtensions.swift 3 | // EmojiArt 4 | // 5 | // Created by Tieda Wei on 2020-06-15. 6 | // Copyright © 2020 Tieda Wei. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | extension Collection where Element: Identifiable { 12 | func firstIndex(matching element: Element) -> Self.Index? { 13 | firstIndex(where: { $0.id == element.id }) 14 | } 15 | // note that contains(matching:) is different than contains() 16 | // this version uses the Identifiable-ness of its elements 17 | // to see whether a member of the Collection has the same identity 18 | func contains(matching element: Element) -> Bool { 19 | self.contains(where: { $0.id == element.id }) 20 | } 21 | } 22 | 23 | extension Data { 24 | // just a simple converter from a Data to a String 25 | var utf8: String? { String(data: self, encoding: .utf8 ) } 26 | } 27 | 28 | extension URL { 29 | var imageURL: URL { 30 | // check to see if there is an embedded imgurl reference 31 | for query in query?.components(separatedBy: "&") ?? [] { 32 | let queryComponents = query.components(separatedBy: "=") 33 | if queryComponents.count == 2 { 34 | if queryComponents[0] == "imgurl", let url = URL(string: queryComponents[1].removingPercentEncoding ?? "") { 35 | return url 36 | } 37 | } 38 | } 39 | // this snippet supports the demo in Lecture 14 40 | // see storeInFilesystem below 41 | if isFileURL { 42 | var url = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first 43 | url = url?.appendingPathComponent(self.lastPathComponent) 44 | if url != nil { 45 | return url! 46 | } 47 | } 48 | return self.baseURL ?? self 49 | } 50 | } 51 | 52 | extension GeometryProxy { 53 | // converts from some other coordinate space to the proxy's own 54 | func convert(_ point: CGPoint, from coordinateSpace: CoordinateSpace) -> CGPoint { 55 | let frame = self.frame(in: coordinateSpace) 56 | return CGPoint(x: point.x-frame.origin.x, y: point.y-frame.origin.y) 57 | } 58 | } 59 | 60 | // simplifies the drag/drop portion of the demo 61 | // you might be able to grok this 62 | // but it does use a generic function 63 | // and also is doing multithreaded stuff here 64 | // and also is bridging to Objective-C-based API 65 | // so kind of too much to talk about during lecture at this point in the game! 66 | 67 | extension Array where Element == NSItemProvider { 68 | func loadObjects(ofType theType: T.Type, firstOnly: Bool = false, using load: @escaping (T) -> Void) -> Bool where T: NSItemProviderReading { 69 | if let provider = self.first(where: { $0.canLoadObject(ofClass: theType) }) { 70 | provider.loadObject(ofClass: theType) { object, error in 71 | if let value = object as? T { 72 | DispatchQueue.main.async { 73 | load(value) 74 | } 75 | } 76 | } 77 | return true 78 | } 79 | return false 80 | } 81 | func loadObjects(ofType theType: T.Type, firstOnly: Bool = false, using load: @escaping (T) -> Void) -> Bool where T: _ObjectiveCBridgeable, T._ObjectiveCType: NSItemProviderReading { 82 | if let provider = self.first(where: { $0.canLoadObject(ofClass: theType) }) { 83 | let _ = provider.loadObject(ofClass: theType) { object, error in 84 | if let value = object { 85 | DispatchQueue.main.async { 86 | load(value) 87 | } 88 | } 89 | } 90 | return true 91 | } 92 | return false 93 | } 94 | func loadFirstObject(ofType theType: T.Type, using load: @escaping (T) -> Void) -> Bool where T: NSItemProviderReading { 95 | self.loadObjects(ofType: theType, firstOnly: true, using: load) 96 | } 97 | func loadFirstObject(ofType theType: T.Type, using load: @escaping (T) -> Void) -> Bool where T: _ObjectiveCBridgeable, T._ObjectiveCType: NSItemProviderReading { 98 | self.loadObjects(ofType: theType, firstOnly: true, using: load) 99 | } 100 | } 101 | 102 | extension String { 103 | // returns ourself without any duplicate Characters 104 | // not very efficient, so only for use on small-ish Strings 105 | func uniqued() -> String { 106 | var uniqued = "" 107 | for ch in self { 108 | if !uniqued.contains(ch) { 109 | uniqued.append(ch) 110 | } 111 | } 112 | return uniqued 113 | } 114 | } 115 | 116 | // it cleans up our code to be able to do more "math" on points and sizes 117 | 118 | extension CGPoint { 119 | static func -(lhs: Self, rhs: Self) -> CGSize { 120 | CGSize(width: lhs.x - rhs.x, height: lhs.y - rhs.y) 121 | } 122 | static func +(lhs: Self, rhs: CGSize) -> CGPoint { 123 | CGPoint(x: lhs.x + rhs.width, y: lhs.y + rhs.height) 124 | } 125 | static func -(lhs: Self, rhs: CGSize) -> CGPoint { 126 | CGPoint(x: lhs.x - rhs.width, y: lhs.y - rhs.height) 127 | } 128 | static func *(lhs: Self, rhs: CGFloat) -> CGPoint { 129 | CGPoint(x: lhs.x * rhs, y: lhs.y * rhs) 130 | } 131 | static func /(lhs: Self, rhs: CGFloat) -> CGPoint { 132 | CGPoint(x: lhs.x / rhs, y: lhs.y / rhs) 133 | } 134 | } 135 | 136 | extension CGSize { 137 | static func +(lhs: Self, rhs: Self) -> CGSize { 138 | CGSize(width: lhs.width + rhs.width, height: lhs.height + rhs.height) 139 | } 140 | static func -(lhs: Self, rhs: Self) -> CGSize { 141 | CGSize(width: lhs.width - rhs.width, height: lhs.height - rhs.height) 142 | } 143 | static func *(lhs: Self, rhs: CGFloat) -> CGSize { 144 | CGSize(width: lhs.width * rhs, height: lhs.height * rhs) 145 | } 146 | static func /(lhs: Self, rhs: CGFloat) -> CGSize { 147 | CGSize(width: lhs.width/rhs, height: lhs.height/rhs) 148 | } 149 | } 150 | 151 | extension String 152 | { 153 | // returns ourself but with numbers appended to the end 154 | // if necessary to make ourself unique with respect to those other Strings 155 | func uniqued(withRespectTo otherStrings: StringCollection) -> String 156 | where StringCollection: Collection, StringCollection.Element == String { 157 | var unique = self 158 | while otherStrings.contains(unique) { 159 | unique = unique.incremented 160 | } 161 | return unique 162 | } 163 | 164 | // if a number is at the end of this String 165 | // this increments that number 166 | // otherwise, it appends the number 1 167 | var incremented: String { 168 | let prefix = String(self.reversed().drop(while: { $0.isNumber }).reversed()) 169 | if let number = Int(self.dropFirst(prefix.count)) { 170 | return "\(prefix)\(number+1)" 171 | } else { 172 | return "\(self) 1" 173 | } 174 | } 175 | } 176 | 177 | extension UIImage { 178 | // Lecture 14 support 179 | // stores ourself as jpeg in a file in the filesystem 180 | // in the Application Support directory in our sandbox 181 | // with the given name (or a unique name if no name provided) 182 | // and returns the URL to it 183 | // care must be taken if you hold on to a URL like this persistently 184 | // because your Application Support directory's URL 185 | // can change between instances of your application 186 | // (see some hackery in imageURL above to account for this) 187 | // if you wanted to hold on to a URL like this in the real world 188 | // (i.e. not in demo-ware) 189 | // you'd probably just hold onto the end part of the URL 190 | // (i.e. not including the Application Support directory's URL) 191 | // and then always prepend Application Support's URL upon use of the URL fragment 192 | // this function might also want to add a parameter for the compression quality 193 | // (currently it is best-quality compression) 194 | func storeInFilesystem(name: String = "\(Date().timeIntervalSince1970)") -> URL? { 195 | var url = try? FileManager.default.url( 196 | for: .applicationSupportDirectory, 197 | in: .userDomainMask, 198 | appropriateFor: nil, 199 | create: true 200 | ) 201 | url = url?.appendingPathComponent(name) 202 | if url != nil { 203 | do { 204 | try self.jpegData(compressionQuality: 1.0)?.write(to: url!) 205 | } catch { 206 | url = nil 207 | } 208 | } 209 | return url 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /projects/EmojiArt/EmojiArt/Grid.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Grid.swift 3 | // EmojiArt 4 | // 5 | // Created by Tieda Wei on 2020-07-01. 6 | // Copyright © 2020 Tieda Wei. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | extension Grid where Item: Identifiable, ID == Item.ID { // Item.ID is protocol Identifiable's associated type 12 | init(_ items: [Item], viewForItem: @escaping (Item) -> ItemView) { 13 | self.init(items, id: \Item.id, viewForItem: viewForItem) 14 | } 15 | } 16 | 17 | struct Grid: View where ID: Hashable, ItemView: View { 18 | private var items: [Item] 19 | private var id: KeyPath 20 | private var viewForItem: (Item) -> ItemView 21 | 22 | init(_ items: [Item], id: KeyPath, viewForItem: @escaping (Item) -> ItemView) { 23 | self.items = items 24 | self.id = id 25 | self.viewForItem = viewForItem 26 | } 27 | 28 | var body: some View { 29 | GeometryReader { geometry in 30 | self.body(for: GridLayout(itemCount: self.items.count, in: geometry.size)) 31 | } 32 | } 33 | 34 | private func body(for layout: GridLayout) -> some View { 35 | return ForEach(items, id: id) { item in 36 | self.body(for: item, in: layout) 37 | } 38 | } 39 | 40 | private func body(for item: Item, in layout: GridLayout) -> some View { 41 | let index = items.firstIndex(where: { item[keyPath: id] == $0[keyPath: id] } ) 42 | return Group { 43 | if index != nil { 44 | viewForItem(item) 45 | .frame(width: layout.itemSize.width, height: layout.itemSize.height) 46 | .position(layout.location(ofItemAt: index!)) 47 | } 48 | } 49 | } 50 | } 51 | 52 | -------------------------------------------------------------------------------- /projects/EmojiArt/EmojiArt/GridLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridLayout.swift 3 | // EmojiArt 4 | // 5 | // Created by Tieda Wei on 2020-07-01. 6 | // Copyright © 2020 Tieda Wei. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | import SwiftUI 12 | 13 | struct GridLayout { 14 | private(set) var size: CGSize 15 | private(set) var rowCount: Int = 0 16 | private(set) var columnCount: Int = 0 17 | 18 | init(itemCount: Int, nearAspectRatio desiredAspectRatio: Double = 1, in size: CGSize) { 19 | self.size = size 20 | // if our size is zero width or height or the itemCount is not > 0 21 | // then we have no work to do (because our rowCount & columnCount will be zero) 22 | guard size.width != 0, size.height != 0, itemCount > 0 else { return } 23 | // find the bestLayout 24 | // i.e., one which results in cells whose aspectRatio 25 | // has the smallestVariance from desiredAspectRatio 26 | // not necessarily most optimal code to do this, but easy to follow (hopefully) 27 | var bestLayout: (rowCount: Int, columnCount: Int) = (1, itemCount) 28 | var smallestVariance: Double? 29 | let sizeAspectRatio = abs(Double(size.width/size.height)) 30 | for rows in 1...itemCount { 31 | let columns = (itemCount / rows) + (itemCount % rows > 0 ? 1 : 0) 32 | if (rows - 1) * columns < itemCount { 33 | let itemAspectRatio = sizeAspectRatio * (Double(rows)/Double(columns)) 34 | let variance = abs(itemAspectRatio - desiredAspectRatio) 35 | if smallestVariance == nil || variance < smallestVariance! { 36 | smallestVariance = variance 37 | bestLayout = (rowCount: rows, columnCount: columns) 38 | } 39 | } 40 | } 41 | rowCount = bestLayout.rowCount 42 | columnCount = bestLayout.columnCount 43 | } 44 | 45 | var itemSize: CGSize { 46 | if rowCount == 0 || columnCount == 0 { 47 | return CGSize.zero 48 | } else { 49 | return CGSize( 50 | width: size.width / CGFloat(columnCount), 51 | height: size.height / CGFloat(rowCount) 52 | ) 53 | } 54 | } 55 | 56 | func location(ofItemAt index: Int) -> CGPoint { 57 | if rowCount == 0 || columnCount == 0 { 58 | return CGPoint.zero 59 | } else { 60 | return CGPoint( 61 | x: (CGFloat(index % columnCount) + 0.5) * itemSize.width, 62 | y: (CGFloat(index / columnCount) + 0.5) * itemSize.height 63 | ) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /projects/EmojiArt/EmojiArt/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 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | 37 | 38 | 39 | 40 | UILaunchStoryboardName 41 | LaunchScreen 42 | UIRequiredDeviceCapabilities 43 | 44 | armv7 45 | 46 | UISupportedInterfaceOrientations 47 | 48 | UIInterfaceOrientationPortrait 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | UISupportedInterfaceOrientations~ipad 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationPortraitUpsideDown 56 | UIInterfaceOrientationLandscapeLeft 57 | UIInterfaceOrientationLandscapeRight 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /projects/EmojiArt/EmojiArt/OptionalImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OptionalImage.swift 3 | // EmojiArt 4 | // 5 | // Created by Tieda Wei on 2020-06-24. 6 | // Copyright © 2020 Tieda Wei. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct OptionalImage: View { 12 | var uiImage: UIImage? 13 | 14 | var body: some View { 15 | Group { 16 | if uiImage != nil { 17 | Image(uiImage: uiImage!) 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /projects/EmojiArt/EmojiArt/PaletteChooser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PaletteChooser.swift 3 | // EmojiArt 4 | // 5 | // Created by Tieda Wei on 2020-06-28. 6 | // Copyright © 2020 Tieda Wei. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct PaletteChooser: View { 12 | @ObservedObject var document: EmojiArtDocument 13 | @Binding var chosenPalette: String 14 | @State private var showPaletteEditor = false 15 | 16 | var body: some View { 17 | HStack { 18 | Stepper(onIncrement: { 19 | self.chosenPalette = self.document.palette(after: self.chosenPalette) 20 | }, onDecrement: { 21 | self.chosenPalette = self.document.palette(before: self.chosenPalette) 22 | }, label: { EmptyView() }) 23 | Text(document.paletteNames[chosenPalette] ?? "") 24 | Image(systemName: "keyboard") 25 | .imageScale(.large) 26 | .onTapGesture { self.showPaletteEditor = true } 27 | .popover(isPresented: $showPaletteEditor) { 28 | PaletteEditor(chosenPalette: self.$chosenPalette, isShowing: self.$showPaletteEditor) 29 | .environmentObject(self.document) 30 | .frame(minWidth: 300, minHeight: 500) 31 | } 32 | } 33 | .fixedSize(horizontal: true, vertical: false) 34 | } 35 | } 36 | 37 | struct PaletteEditor: View { 38 | @EnvironmentObject var document: EmojiArtDocument 39 | 40 | @Binding var chosenPalette: String 41 | @Binding var isShowing: Bool 42 | @State private var paletteName: String = "" 43 | @State private var emojisToAdd: String = "" 44 | 45 | private var height: CGFloat { CGFloat((chosenPalette.count - 1) / 6) * 70 + 70 } 46 | private let fontSize: CGFloat = 40 47 | 48 | var body: some View { 49 | VStack(spacing: 0) { 50 | ZStack { 51 | Text("Palette Editor").font(.headline).padding() 52 | HStack { 53 | Spacer() 54 | Button(action: { 55 | self.isShowing = false 56 | }, label: { Text("Done") }).padding() 57 | } 58 | } 59 | Divider() 60 | Form { 61 | Section { 62 | TextField("Palette Name", text: $paletteName, onEditingChanged: { began in 63 | if !began { 64 | self.document.renamePalette(self.chosenPalette, to: self.paletteName) 65 | } 66 | }) 67 | TextField("Add Emoji", text: $emojisToAdd, onEditingChanged: { began in 68 | if !began { 69 | self.chosenPalette = self.document.addEmoji(self.emojisToAdd, toPalette: self.chosenPalette) 70 | self.emojisToAdd = "" 71 | } 72 | }) 73 | } 74 | Section(header: Text("Remove Emoji")) { 75 | Grid(chosenPalette.map { String($0) }, id: \.self) { emoji in 76 | Text(emoji).font(Font.system(size: self.fontSize)) 77 | .onTapGesture { 78 | self.chosenPalette = self.document.removeEmoji(emoji, fromPalette: self.chosenPalette) 79 | } 80 | } 81 | .frame(height: self.height) 82 | } 83 | } 84 | } 85 | .onAppear { self.paletteName = self.document.paletteNames[self.chosenPalette] ?? "" } 86 | } 87 | } 88 | 89 | 90 | struct PaletteChooser_Previews: PreviewProvider { 91 | static var previews: some View { 92 | PaletteChooser(document: EmojiArtDocument(), chosenPalette: .constant("")) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /projects/EmojiArt/EmojiArt/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /projects/EmojiArt/EmojiArt/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // EmojiArt 4 | // 5 | // Created by Tieda Wei on 2020-06-15. 6 | // Copyright © 2020 Tieda Wei. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftUI 11 | 12 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 18 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 19 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 20 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 21 | 22 | // Create the SwiftUI view that provides the window contents. 23 | 24 | let store = EmojiArtDocumentStore(named: "Emoji Art") 25 | let contentView = EmojiArtDocumentChooser().environmentObject(store) 26 | 27 | // Use a UIHostingController as window root view controller. 28 | if let windowScene = scene as? UIWindowScene { 29 | let window = UIWindow(windowScene: windowScene) 30 | window.rootViewController = UIHostingController(rootView: contentView) 31 | self.window = window 32 | window.makeKeyAndVisible() 33 | } 34 | } 35 | 36 | func sceneDidDisconnect(_ scene: UIScene) { 37 | // Called as the scene is being released by the system. 38 | // This occurs shortly after the scene enters the background, or when its session is discarded. 39 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 40 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 41 | } 42 | 43 | func sceneDidBecomeActive(_ scene: UIScene) { 44 | // Called when the scene has moved from an inactive state to an active state. 45 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 46 | } 47 | 48 | func sceneWillResignActive(_ scene: UIScene) { 49 | // Called when the scene will move from an active state to an inactive state. 50 | // This may occur due to temporary interruptions (ex. an incoming phone call). 51 | } 52 | 53 | func sceneWillEnterForeground(_ scene: UIScene) { 54 | // Called as the scene transitions from the background to the foreground. 55 | // Use this method to undo the changes made on entering the background. 56 | } 57 | 58 | func sceneDidEnterBackground(_ scene: UIScene) { 59 | // Called as the scene transitions from the foreground to the background. 60 | // Use this method to save data, release shared resources, and store enough scene-specific state information 61 | // to restore the scene back to its current state. 62 | } 63 | 64 | 65 | } 66 | 67 | -------------------------------------------------------------------------------- /projects/EmojiArt/EmojiArt/Spinning.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Spinning.swift 3 | // EmojiArt 4 | // 5 | // Created by Tieda Wei on 2020-06-28. 6 | // Copyright © 2020 Tieda Wei. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct Spinning: ViewModifier { 12 | @State var isVisible = false 13 | 14 | func body(content: Content) -> some View { 15 | content 16 | .rotationEffect(.init(degrees: isVisible ? 360 : 0)) 17 | .animation(Animation.linear(duration: 1).repeatForever(autoreverses: false)) 18 | .onAppear { 19 | self.isVisible = true 20 | } 21 | } 22 | } 23 | 24 | extension View { 25 | func spinning() -> some View { 26 | self.modifier(Spinning()) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /projects/Enroute L12/Enroute.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | DA47A9BD2469F34800C8E13C /* FilterFlights.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA47A9BC2469F34800C8E13C /* FilterFlights.swift */; }; 11 | DAB658A42467944B00A0F2BB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAB658A32467944B00A0F2BB /* AppDelegate.swift */; }; 12 | DAB658A62467944B00A0F2BB /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAB658A52467944B00A0F2BB /* SceneDelegate.swift */; }; 13 | DAB658A92467944B00A0F2BB /* Enroute.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = DAB658A72467944B00A0F2BB /* Enroute.xcdatamodeld */; }; 14 | DAB658AB2467944B00A0F2BB /* FlightsEnrouteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAB658AA2467944B00A0F2BB /* FlightsEnrouteView.swift */; }; 15 | DAB658AD2467944C00A0F2BB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DAB658AC2467944C00A0F2BB /* Assets.xcassets */; }; 16 | DAB658B02467944C00A0F2BB /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DAB658AF2467944C00A0F2BB /* Preview Assets.xcassets */; }; 17 | DAB658B32467944C00A0F2BB /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DAB658B12467944C00A0F2BB /* LaunchScreen.storyboard */; }; 18 | DAB658BB2467949600A0F2BB /* FAFlight.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAB658BA2467949600A0F2BB /* FAFlight.swift */; }; 19 | DAB658C2246794A800A0F2BB /* FoundationExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAB658BC246794A700A0F2BB /* FoundationExtensions.swift */; }; 20 | DAB658C3246794A800A0F2BB /* FlightAware+AirportInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAB658BD246794A700A0F2BB /* FlightAware+AirportInfo.swift */; }; 21 | DAB658C4246794A800A0F2BB /* FlightAware+AirlineInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAB658BE246794A700A0F2BB /* FlightAware+AirlineInfo.swift */; }; 22 | DAB658C5246794A800A0F2BB /* FlightAware+Enroute.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAB658BF246794A800A0F2BB /* FlightAware+Enroute.swift */; }; 23 | DAB658C6246794A800A0F2BB /* FlightAwareRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAB658C0246794A800A0F2BB /* FlightAwareRequest.swift */; }; 24 | DAB658C7246794A800A0F2BB /* FlightSimulationData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAB658C1246794A800A0F2BB /* FlightSimulationData.swift */; }; 25 | DAB658C92467A03800A0F2BB /* Airline.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAB658C82467A03800A0F2BB /* Airline.swift */; }; 26 | DAB658CB2467A05800A0F2BB /* Airport.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAB658CA2467A05800A0F2BB /* Airport.swift */; }; 27 | DAB658CD2467A0BF00A0F2BB /* Flight.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAB658CC2467A0BF00A0F2BB /* Flight.swift */; }; 28 | /* End PBXBuildFile section */ 29 | 30 | /* Begin PBXFileReference section */ 31 | DA47A9BC2469F34800C8E13C /* FilterFlights.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterFlights.swift; sourceTree = ""; }; 32 | DAB658A02467944B00A0F2BB /* Enroute.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Enroute.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33 | DAB658A32467944B00A0F2BB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 34 | DAB658A52467944B00A0F2BB /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 35 | DAB658A82467944B00A0F2BB /* Enroute.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Enroute.xcdatamodel; sourceTree = ""; }; 36 | DAB658AA2467944B00A0F2BB /* FlightsEnrouteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlightsEnrouteView.swift; sourceTree = ""; }; 37 | DAB658AC2467944C00A0F2BB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 38 | DAB658AF2467944C00A0F2BB /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 39 | DAB658B22467944C00A0F2BB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 40 | DAB658B42467944C00A0F2BB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 41 | DAB658BA2467949600A0F2BB /* FAFlight.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FAFlight.swift; sourceTree = ""; }; 42 | DAB658BC246794A700A0F2BB /* FoundationExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FoundationExtensions.swift; sourceTree = ""; }; 43 | DAB658BD246794A700A0F2BB /* FlightAware+AirportInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "FlightAware+AirportInfo.swift"; sourceTree = ""; }; 44 | DAB658BE246794A700A0F2BB /* FlightAware+AirlineInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "FlightAware+AirlineInfo.swift"; sourceTree = ""; }; 45 | DAB658BF246794A800A0F2BB /* FlightAware+Enroute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "FlightAware+Enroute.swift"; sourceTree = ""; }; 46 | DAB658C0246794A800A0F2BB /* FlightAwareRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlightAwareRequest.swift; sourceTree = ""; }; 47 | DAB658C1246794A800A0F2BB /* FlightSimulationData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlightSimulationData.swift; sourceTree = ""; }; 48 | DAB658C82467A03800A0F2BB /* Airline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Airline.swift; sourceTree = ""; }; 49 | DAB658CA2467A05800A0F2BB /* Airport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Airport.swift; sourceTree = ""; }; 50 | DAB658CC2467A0BF00A0F2BB /* Flight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Flight.swift; sourceTree = ""; }; 51 | /* End PBXFileReference section */ 52 | 53 | /* Begin PBXFrameworksBuildPhase section */ 54 | DAB6589D2467944B00A0F2BB /* Frameworks */ = { 55 | isa = PBXFrameworksBuildPhase; 56 | buildActionMask = 2147483647; 57 | files = ( 58 | ); 59 | runOnlyForDeploymentPostprocessing = 0; 60 | }; 61 | /* End PBXFrameworksBuildPhase section */ 62 | 63 | /* Begin PBXGroup section */ 64 | DA47A9BA2469F27800C8E13C /* FlightAware */ = { 65 | isa = PBXGroup; 66 | children = ( 67 | DAB658C0246794A800A0F2BB /* FlightAwareRequest.swift */, 68 | DAB658BA2467949600A0F2BB /* FAFlight.swift */, 69 | DAB658BF246794A800A0F2BB /* FlightAware+Enroute.swift */, 70 | DAB658BE246794A700A0F2BB /* FlightAware+AirlineInfo.swift */, 71 | DAB658BD246794A700A0F2BB /* FlightAware+AirportInfo.swift */, 72 | DAB658C1246794A800A0F2BB /* FlightSimulationData.swift */, 73 | ); 74 | path = FlightAware; 75 | sourceTree = ""; 76 | }; 77 | DAA4E825246C5F160072134C /* L12 */ = { 78 | isa = PBXGroup; 79 | children = ( 80 | DAB658CC2467A0BF00A0F2BB /* Flight.swift */, 81 | DAB658CA2467A05800A0F2BB /* Airport.swift */, 82 | DAB658C82467A03800A0F2BB /* Airline.swift */, 83 | ); 84 | path = L12; 85 | sourceTree = ""; 86 | }; 87 | DAB658972467944B00A0F2BB = { 88 | isa = PBXGroup; 89 | children = ( 90 | DAB658A22467944B00A0F2BB /* Enroute */, 91 | DAB658A12467944B00A0F2BB /* Products */, 92 | ); 93 | sourceTree = ""; 94 | }; 95 | DAB658A12467944B00A0F2BB /* Products */ = { 96 | isa = PBXGroup; 97 | children = ( 98 | DAB658A02467944B00A0F2BB /* Enroute.app */, 99 | ); 100 | name = Products; 101 | sourceTree = ""; 102 | }; 103 | DAB658A22467944B00A0F2BB /* Enroute */ = { 104 | isa = PBXGroup; 105 | children = ( 106 | DA47A9BC2469F34800C8E13C /* FilterFlights.swift */, 107 | DAB658AA2467944B00A0F2BB /* FlightsEnrouteView.swift */, 108 | DAB658A72467944B00A0F2BB /* Enroute.xcdatamodeld */, 109 | DAA4E825246C5F160072134C /* L12 */, 110 | DA47A9BA2469F27800C8E13C /* FlightAware */, 111 | DAB658BC246794A700A0F2BB /* FoundationExtensions.swift */, 112 | DAB658AC2467944C00A0F2BB /* Assets.xcassets */, 113 | DAB658A32467944B00A0F2BB /* AppDelegate.swift */, 114 | DAB658A52467944B00A0F2BB /* SceneDelegate.swift */, 115 | DAB658B12467944C00A0F2BB /* LaunchScreen.storyboard */, 116 | DAB658B42467944C00A0F2BB /* Info.plist */, 117 | DAB658AE2467944C00A0F2BB /* Preview Content */, 118 | ); 119 | path = Enroute; 120 | sourceTree = ""; 121 | }; 122 | DAB658AE2467944C00A0F2BB /* Preview Content */ = { 123 | isa = PBXGroup; 124 | children = ( 125 | DAB658AF2467944C00A0F2BB /* Preview Assets.xcassets */, 126 | ); 127 | path = "Preview Content"; 128 | sourceTree = ""; 129 | }; 130 | /* End PBXGroup section */ 131 | 132 | /* Begin PBXNativeTarget section */ 133 | DAB6589F2467944B00A0F2BB /* Enroute */ = { 134 | isa = PBXNativeTarget; 135 | buildConfigurationList = DAB658B72467944C00A0F2BB /* Build configuration list for PBXNativeTarget "Enroute" */; 136 | buildPhases = ( 137 | DAB6589C2467944B00A0F2BB /* Sources */, 138 | DAB6589D2467944B00A0F2BB /* Frameworks */, 139 | DAB6589E2467944B00A0F2BB /* Resources */, 140 | ); 141 | buildRules = ( 142 | ); 143 | dependencies = ( 144 | ); 145 | name = Enroute; 146 | productName = Enroute; 147 | productReference = DAB658A02467944B00A0F2BB /* Enroute.app */; 148 | productType = "com.apple.product-type.application"; 149 | }; 150 | /* End PBXNativeTarget section */ 151 | 152 | /* Begin PBXProject section */ 153 | DAB658982467944B00A0F2BB /* Project object */ = { 154 | isa = PBXProject; 155 | attributes = { 156 | LastSwiftUpdateCheck = 1140; 157 | LastUpgradeCheck = 1140; 158 | ORGANIZATIONNAME = "Stanford University"; 159 | TargetAttributes = { 160 | DAB6589F2467944B00A0F2BB = { 161 | CreatedOnToolsVersion = 11.4.1; 162 | }; 163 | }; 164 | }; 165 | buildConfigurationList = DAB6589B2467944B00A0F2BB /* Build configuration list for PBXProject "Enroute" */; 166 | compatibilityVersion = "Xcode 9.3"; 167 | developmentRegion = en; 168 | hasScannedForEncodings = 0; 169 | knownRegions = ( 170 | en, 171 | Base, 172 | ); 173 | mainGroup = DAB658972467944B00A0F2BB; 174 | productRefGroup = DAB658A12467944B00A0F2BB /* Products */; 175 | projectDirPath = ""; 176 | projectRoot = ""; 177 | targets = ( 178 | DAB6589F2467944B00A0F2BB /* Enroute */, 179 | ); 180 | }; 181 | /* End PBXProject section */ 182 | 183 | /* Begin PBXResourcesBuildPhase section */ 184 | DAB6589E2467944B00A0F2BB /* Resources */ = { 185 | isa = PBXResourcesBuildPhase; 186 | buildActionMask = 2147483647; 187 | files = ( 188 | DAB658B32467944C00A0F2BB /* LaunchScreen.storyboard in Resources */, 189 | DAB658B02467944C00A0F2BB /* Preview Assets.xcassets in Resources */, 190 | DAB658AD2467944C00A0F2BB /* Assets.xcassets in Resources */, 191 | ); 192 | runOnlyForDeploymentPostprocessing = 0; 193 | }; 194 | /* End PBXResourcesBuildPhase section */ 195 | 196 | /* Begin PBXSourcesBuildPhase section */ 197 | DAB6589C2467944B00A0F2BB /* Sources */ = { 198 | isa = PBXSourcesBuildPhase; 199 | buildActionMask = 2147483647; 200 | files = ( 201 | DAB658C92467A03800A0F2BB /* Airline.swift in Sources */, 202 | DAB658C6246794A800A0F2BB /* FlightAwareRequest.swift in Sources */, 203 | DAB658C7246794A800A0F2BB /* FlightSimulationData.swift in Sources */, 204 | DAB658A92467944B00A0F2BB /* Enroute.xcdatamodeld in Sources */, 205 | DAB658C4246794A800A0F2BB /* FlightAware+AirlineInfo.swift in Sources */, 206 | DAB658A42467944B00A0F2BB /* AppDelegate.swift in Sources */, 207 | DAB658CB2467A05800A0F2BB /* Airport.swift in Sources */, 208 | DA47A9BD2469F34800C8E13C /* FilterFlights.swift in Sources */, 209 | DAB658C2246794A800A0F2BB /* FoundationExtensions.swift in Sources */, 210 | DAB658C3246794A800A0F2BB /* FlightAware+AirportInfo.swift in Sources */, 211 | DAB658C5246794A800A0F2BB /* FlightAware+Enroute.swift in Sources */, 212 | DAB658AB2467944B00A0F2BB /* FlightsEnrouteView.swift in Sources */, 213 | DAB658BB2467949600A0F2BB /* FAFlight.swift in Sources */, 214 | DAB658A62467944B00A0F2BB /* SceneDelegate.swift in Sources */, 215 | DAB658CD2467A0BF00A0F2BB /* Flight.swift in Sources */, 216 | ); 217 | runOnlyForDeploymentPostprocessing = 0; 218 | }; 219 | /* End PBXSourcesBuildPhase section */ 220 | 221 | /* Begin PBXVariantGroup section */ 222 | DAB658B12467944C00A0F2BB /* LaunchScreen.storyboard */ = { 223 | isa = PBXVariantGroup; 224 | children = ( 225 | DAB658B22467944C00A0F2BB /* Base */, 226 | ); 227 | name = LaunchScreen.storyboard; 228 | sourceTree = ""; 229 | }; 230 | /* End PBXVariantGroup section */ 231 | 232 | /* Begin XCBuildConfiguration section */ 233 | DAB658B52467944C00A0F2BB /* Debug */ = { 234 | isa = XCBuildConfiguration; 235 | buildSettings = { 236 | ALWAYS_SEARCH_USER_PATHS = NO; 237 | CLANG_ANALYZER_NONNULL = YES; 238 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 239 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 240 | CLANG_CXX_LIBRARY = "libc++"; 241 | CLANG_ENABLE_MODULES = YES; 242 | CLANG_ENABLE_OBJC_ARC = YES; 243 | CLANG_ENABLE_OBJC_WEAK = YES; 244 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 245 | CLANG_WARN_BOOL_CONVERSION = YES; 246 | CLANG_WARN_COMMA = YES; 247 | CLANG_WARN_CONSTANT_CONVERSION = YES; 248 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 249 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 250 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 251 | CLANG_WARN_EMPTY_BODY = YES; 252 | CLANG_WARN_ENUM_CONVERSION = YES; 253 | CLANG_WARN_INFINITE_RECURSION = YES; 254 | CLANG_WARN_INT_CONVERSION = YES; 255 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 256 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 257 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 258 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 259 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 260 | CLANG_WARN_STRICT_PROTOTYPES = YES; 261 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 262 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 263 | CLANG_WARN_UNREACHABLE_CODE = YES; 264 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 265 | COPY_PHASE_STRIP = NO; 266 | DEBUG_INFORMATION_FORMAT = dwarf; 267 | ENABLE_STRICT_OBJC_MSGSEND = YES; 268 | ENABLE_TESTABILITY = YES; 269 | GCC_C_LANGUAGE_STANDARD = gnu11; 270 | GCC_DYNAMIC_NO_PIC = NO; 271 | GCC_NO_COMMON_BLOCKS = YES; 272 | GCC_OPTIMIZATION_LEVEL = 0; 273 | GCC_PREPROCESSOR_DEFINITIONS = ( 274 | "DEBUG=1", 275 | "$(inherited)", 276 | ); 277 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 278 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 279 | GCC_WARN_UNDECLARED_SELECTOR = YES; 280 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 281 | GCC_WARN_UNUSED_FUNCTION = YES; 282 | GCC_WARN_UNUSED_VARIABLE = YES; 283 | IPHONEOS_DEPLOYMENT_TARGET = 13.4; 284 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 285 | MTL_FAST_MATH = YES; 286 | ONLY_ACTIVE_ARCH = YES; 287 | SDKROOT = iphoneos; 288 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 289 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 290 | }; 291 | name = Debug; 292 | }; 293 | DAB658B62467944C00A0F2BB /* Release */ = { 294 | isa = XCBuildConfiguration; 295 | buildSettings = { 296 | ALWAYS_SEARCH_USER_PATHS = NO; 297 | CLANG_ANALYZER_NONNULL = YES; 298 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 299 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 300 | CLANG_CXX_LIBRARY = "libc++"; 301 | CLANG_ENABLE_MODULES = YES; 302 | CLANG_ENABLE_OBJC_ARC = YES; 303 | CLANG_ENABLE_OBJC_WEAK = YES; 304 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 305 | CLANG_WARN_BOOL_CONVERSION = YES; 306 | CLANG_WARN_COMMA = YES; 307 | CLANG_WARN_CONSTANT_CONVERSION = YES; 308 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 309 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 310 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 311 | CLANG_WARN_EMPTY_BODY = YES; 312 | CLANG_WARN_ENUM_CONVERSION = YES; 313 | CLANG_WARN_INFINITE_RECURSION = YES; 314 | CLANG_WARN_INT_CONVERSION = YES; 315 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 316 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 317 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 318 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 319 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 320 | CLANG_WARN_STRICT_PROTOTYPES = YES; 321 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 322 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 323 | CLANG_WARN_UNREACHABLE_CODE = YES; 324 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 325 | COPY_PHASE_STRIP = NO; 326 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 327 | ENABLE_NS_ASSERTIONS = NO; 328 | ENABLE_STRICT_OBJC_MSGSEND = YES; 329 | GCC_C_LANGUAGE_STANDARD = gnu11; 330 | GCC_NO_COMMON_BLOCKS = YES; 331 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 332 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 333 | GCC_WARN_UNDECLARED_SELECTOR = YES; 334 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 335 | GCC_WARN_UNUSED_FUNCTION = YES; 336 | GCC_WARN_UNUSED_VARIABLE = YES; 337 | IPHONEOS_DEPLOYMENT_TARGET = 13.4; 338 | MTL_ENABLE_DEBUG_INFO = NO; 339 | MTL_FAST_MATH = YES; 340 | SDKROOT = iphoneos; 341 | SWIFT_COMPILATION_MODE = wholemodule; 342 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 343 | VALIDATE_PRODUCT = YES; 344 | }; 345 | name = Release; 346 | }; 347 | DAB658B82467944C00A0F2BB /* Debug */ = { 348 | isa = XCBuildConfiguration; 349 | buildSettings = { 350 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 351 | CODE_SIGN_STYLE = Automatic; 352 | DEVELOPMENT_ASSET_PATHS = "\"Enroute/Preview Content\""; 353 | DEVELOPMENT_TEAM = ""; 354 | ENABLE_PREVIEWS = YES; 355 | INFOPLIST_FILE = Enroute/Info.plist; 356 | LD_RUNPATH_SEARCH_PATHS = ( 357 | "$(inherited)", 358 | "@executable_path/Frameworks", 359 | ); 360 | "OTHER_SWIFT_FLAGS[sdk=*]" = "-DCOREDATA"; 361 | PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.cs193p.instructor.Enroute; 362 | PRODUCT_NAME = "$(TARGET_NAME)"; 363 | SWIFT_VERSION = 5.0; 364 | TARGETED_DEVICE_FAMILY = "1,2"; 365 | }; 366 | name = Debug; 367 | }; 368 | DAB658B92467944C00A0F2BB /* Release */ = { 369 | isa = XCBuildConfiguration; 370 | buildSettings = { 371 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 372 | CODE_SIGN_STYLE = Automatic; 373 | DEVELOPMENT_ASSET_PATHS = "\"Enroute/Preview Content\""; 374 | DEVELOPMENT_TEAM = ""; 375 | ENABLE_PREVIEWS = YES; 376 | INFOPLIST_FILE = Enroute/Info.plist; 377 | LD_RUNPATH_SEARCH_PATHS = ( 378 | "$(inherited)", 379 | "@executable_path/Frameworks", 380 | ); 381 | PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.cs193p.instructor.Enroute; 382 | PRODUCT_NAME = "$(TARGET_NAME)"; 383 | SWIFT_VERSION = 5.0; 384 | TARGETED_DEVICE_FAMILY = "1,2"; 385 | }; 386 | name = Release; 387 | }; 388 | /* End XCBuildConfiguration section */ 389 | 390 | /* Begin XCConfigurationList section */ 391 | DAB6589B2467944B00A0F2BB /* Build configuration list for PBXProject "Enroute" */ = { 392 | isa = XCConfigurationList; 393 | buildConfigurations = ( 394 | DAB658B52467944C00A0F2BB /* Debug */, 395 | DAB658B62467944C00A0F2BB /* Release */, 396 | ); 397 | defaultConfigurationIsVisible = 0; 398 | defaultConfigurationName = Release; 399 | }; 400 | DAB658B72467944C00A0F2BB /* Build configuration list for PBXNativeTarget "Enroute" */ = { 401 | isa = XCConfigurationList; 402 | buildConfigurations = ( 403 | DAB658B82467944C00A0F2BB /* Debug */, 404 | DAB658B92467944C00A0F2BB /* Release */, 405 | ); 406 | defaultConfigurationIsVisible = 0; 407 | defaultConfigurationName = Release; 408 | }; 409 | /* End XCConfigurationList section */ 410 | 411 | /* Begin XCVersionGroup section */ 412 | DAB658A72467944B00A0F2BB /* Enroute.xcdatamodeld */ = { 413 | isa = XCVersionGroup; 414 | children = ( 415 | DAB658A82467944B00A0F2BB /* Enroute.xcdatamodel */, 416 | ); 417 | currentVersion = DAB658A82467944B00A0F2BB /* Enroute.xcdatamodel */; 418 | path = Enroute.xcdatamodeld; 419 | sourceTree = ""; 420 | versionGroupType = wrapper.xcdatamodel; 421 | }; 422 | /* End XCVersionGroup section */ 423 | }; 424 | rootObject = DAB658982467944B00A0F2BB /* Project object */; 425 | } 426 | -------------------------------------------------------------------------------- /projects/Enroute L12/Enroute.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /projects/Enroute L12/Enroute.xcodeproj/project.xcworkspace/xcuserdata/Tieda.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weitieda/cs193p-2020-swiftui/ba90cdc1d0b0ae59e386e9640c2f2d27bc2bcc15/projects/Enroute L12/Enroute.xcodeproj/project.xcworkspace/xcuserdata/Tieda.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /projects/Enroute L12/Enroute.xcodeproj/project.xcworkspace/xcuserdata/cs193p.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weitieda/cs193p-2020-swiftui/ba90cdc1d0b0ae59e386e9640c2f2d27bc2bcc15/projects/Enroute L12/Enroute.xcodeproj/project.xcworkspace/xcuserdata/cs193p.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /projects/Enroute L12/Enroute.xcodeproj/project.xcworkspace/xcuserdata/paul.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weitieda/cs193p-2020-swiftui/ba90cdc1d0b0ae59e386e9640c2f2d27bc2bcc15/projects/Enroute L12/Enroute.xcodeproj/project.xcworkspace/xcuserdata/paul.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /projects/Enroute L12/Enroute.xcodeproj/xcuserdata/Tieda.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Enroute.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /projects/Enroute L12/Enroute.xcodeproj/xcuserdata/cs193p.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Enroute.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /projects/Enroute L12/Enroute.xcodeproj/xcuserdata/paul.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /projects/Enroute L12/Enroute.xcodeproj/xcuserdata/paul.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Enroute.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /projects/Enroute L12/Enroute/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Enroute 4 | // 5 | // Created by CS193p Instructor. 6 | // Copyright © 2020 Stanford University. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import CoreData 11 | 12 | @UIApplicationMain 13 | class AppDelegate: UIResponder, UIApplicationDelegate { 14 | 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | // MARK: UISceneSession Lifecycle 23 | 24 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 25 | // Called when a new scene session is being created. 26 | // Use this method to select a configuration to create the new scene with. 27 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 28 | } 29 | 30 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 31 | // Called when the user discards a scene session. 32 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 33 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 34 | } 35 | 36 | // MARK: - Core Data stack 37 | 38 | lazy var persistentContainer: NSPersistentCloudKitContainer = { 39 | /* 40 | The persistent container for the application. This implementation 41 | creates and returns a container, having loaded the store for the 42 | application to it. This property is optional since there are legitimate 43 | error conditions that could cause the creation of the store to fail. 44 | */ 45 | let container = NSPersistentCloudKitContainer(name: "Enroute") 46 | container.loadPersistentStores(completionHandler: { (storeDescription, error) in 47 | if let error = error as NSError? { 48 | // Replace this implementation with code to handle the error appropriately. 49 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 50 | 51 | /* 52 | Typical reasons for an error here include: 53 | * The parent directory does not exist, cannot be created, or disallows writing. 54 | * The persistent store is not accessible, due to permissions or data protection when the device is locked. 55 | * The device is out of space. 56 | * The store could not be migrated to the current model version. 57 | Check the error message to determine what the actual problem was. 58 | */ 59 | fatalError("Unresolved error \(error), \(error.userInfo)") 60 | } 61 | }) 62 | return container 63 | }() 64 | 65 | // MARK: - Core Data Saving support 66 | 67 | func saveContext () { 68 | let context = persistentContainer.viewContext 69 | if context.hasChanges { 70 | do { 71 | try context.save() 72 | } catch { 73 | // Replace this implementation with code to handle the error appropriately. 74 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 75 | let nserror = error as NSError 76 | fatalError("Unresolved error \(nserror), \(nserror.userInfo)") 77 | } 78 | } 79 | } 80 | 81 | } 82 | 83 | -------------------------------------------------------------------------------- /projects/Enroute L12/Enroute/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /projects/Enroute L12/Enroute/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /projects/Enroute L12/Enroute/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 | -------------------------------------------------------------------------------- /projects/Enroute L12/Enroute/Enroute.xcdatamodeld/.xccurrentversion: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | _XCCurrentVersionName 6 | Enroute.xcdatamodel 7 | 8 | 9 | -------------------------------------------------------------------------------- /projects/Enroute L12/Enroute/Enroute.xcdatamodeld/Enroute.xcdatamodel/contents: -------------------------------------------------------------------------------- 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 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /projects/Enroute L12/Enroute/FilterFlights.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FilterFlights.swift 3 | // Enroute 4 | // 5 | // Created by CS193p Instructor. 6 | // Copyright © 2020 Stanford University. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct FilterFlights: View { 12 | @FetchRequest(fetchRequest: Airport.fetchRequest(.all)) var airports: FetchedResults 13 | @FetchRequest(fetchRequest: Airline.fetchRequest(.all)) var airlines: FetchedResults 14 | 15 | @Binding var flightSearch: FlightSearch 16 | @Binding var isPresented: Bool 17 | 18 | @State private var draft: FlightSearch 19 | 20 | init(flightSearch: Binding, isPresented: Binding) { 21 | _flightSearch = flightSearch 22 | _isPresented = isPresented 23 | _draft = State(wrappedValue: flightSearch.wrappedValue) 24 | } 25 | 26 | var body: some View { 27 | NavigationView { 28 | Form { 29 | Picker("Destination", selection: $draft.destination) { 30 | ForEach(airports.sorted(), id: \.self) { airport in 31 | Text("\(airport.friendlyName)").tag(airport) 32 | } 33 | } 34 | Picker("Origin", selection: $draft.origin) { 35 | Text("Any").tag(Airport?.none) 36 | ForEach(airports.sorted(), id: \.self) { (airport: Airport?) in 37 | Text("\(airport?.friendlyName ?? "Any")").tag(airport) 38 | } 39 | } 40 | Picker("Airline", selection: $draft.airline) { 41 | Text("Any").tag(Airline?.none) 42 | ForEach(airlines.sorted(), id: \.self) { (airline: Airline?) in 43 | Text("\(airline?.friendlyName ?? "Any")").tag(airline) 44 | } 45 | } 46 | Toggle(isOn: $draft.inTheAir) { Text("Enroute Only") } 47 | } 48 | .navigationBarTitle("Filter Flights") 49 | .navigationBarItems(leading: cancel, trailing: done) 50 | } 51 | } 52 | 53 | var cancel: some View { 54 | Button("Cancel") { 55 | self.isPresented = false 56 | } 57 | } 58 | 59 | var done: some View { 60 | Button("Done") { 61 | if self.draft.destination != self.flightSearch.destination { 62 | self.draft.destination.fetchIncomingFlights() 63 | } 64 | self.flightSearch = self.draft 65 | self.isPresented = false 66 | } 67 | } 68 | } 69 | 70 | //struct FilterFlights_Previews: PreviewProvider { 71 | // static var previews: some View { 72 | // FilterFlights() 73 | // } 74 | //} 75 | -------------------------------------------------------------------------------- /projects/Enroute L12/Enroute/FlightAware/FAFlight.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FAFlight.swift 3 | // Enroute 4 | // 5 | // Created by CS193p Instructor. 6 | // Copyright © 2020 Stanford University. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // json decoded directly from what comes back from FlightAware's "Enroute?" 12 | 13 | struct FAFlight: Codable, Hashable, Identifiable, Comparable, CustomStringConvertible 14 | { 15 | private(set) var ident: String 16 | private(set) var aircraft: String 17 | 18 | var number: Int { Int(String(ident.drop(while: { !$0.isNumber }))) ?? 0 } 19 | var airlineCode: String { String(ident.prefix(while: { !$0.isNumber })) } 20 | 21 | var departure: Date? { actualdeparturetime > 0 ? Date(timeIntervalSince1970: TimeInterval(actualdeparturetime)) : nil } 22 | var arrival: Date { Date(timeIntervalSince1970: TimeInterval(estimatedarrivaltime)) } 23 | var filed: Date { Date(timeIntervalSince1970: TimeInterval(filed_departuretime)) } 24 | 25 | private(set) var destination: String 26 | private(set) var destinationName: String 27 | private(set) var destinationCity: String 28 | 29 | private(set) var origin: String 30 | private(set) var originName: String 31 | private(set) var originCity: String 32 | 33 | var originFullName: String { 34 | let origin = self.origin.first == "K" ? String(self.origin.dropFirst()) : self.origin 35 | if originName.contains(elementIn: originCity.components(separatedBy: ",")) { 36 | return origin + " " + originCity 37 | } 38 | return origin + " \(originName), \(originCity)" 39 | } 40 | 41 | private enum CodingKeys: String, CodingKey { 42 | case ident 43 | case aircraft = "aircrafttype" 44 | case actualdeparturetime, estimatedarrivaltime, filed_departuretime 45 | case origin, destination 46 | case originName, originCity 47 | case destinationName, destinationCity 48 | } 49 | 50 | private var actualdeparturetime: Int 51 | private var estimatedarrivaltime: Int 52 | private var filed_departuretime: Int 53 | 54 | var id: String { ident } 55 | func hash(into hasher: inout Hasher) { hasher.combine(id) } 56 | static func ==(lhs: FAFlight, rhs: FAFlight) -> Bool { lhs.id == rhs.id } 57 | 58 | static func < (lhs: FAFlight, rhs: FAFlight) -> Bool { 59 | if lhs.arrival < rhs.arrival { 60 | return true 61 | } else if rhs.arrival < lhs.arrival { 62 | return false 63 | } else { 64 | return lhs.departure ?? lhs.filed < rhs.departure ?? rhs.filed 65 | } 66 | } 67 | 68 | var description: String { 69 | if let departure = self.departure { 70 | return "\(ident) departed \(origin) at \(departure) arriving \(arrival)" 71 | } else { 72 | return "\(ident) scheduled to depart \(origin) at \(filed) arriving \(arrival)" 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /projects/Enroute L12/Enroute/FlightAware/FlightAware+AirlineInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AirlineInfo.swift 3 | // Enroute 4 | // 5 | // Created by CS193p Instructor. 6 | // Copyright © 2020 Stanford University. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | 12 | // json decoded directly from what comes back from FlightAware's "AirlineInfo?" 13 | 14 | struct AirlineInfo: Codable, Hashable, Identifiable, Comparable { 15 | fileprivate(set) var code: String? 16 | private(set) var callsign: String 17 | private(set) var country: String 18 | private(set) var location: String 19 | private(set) var name: String 20 | private(set) var phone: String 21 | private(set) var shortname: String 22 | private(set) var url: String 23 | 24 | var friendlyName: String { shortname.isEmpty ? (name.isEmpty ? (code ?? "???") : name) : shortname } 25 | 26 | var id: String { code ?? callsign } 27 | func hash(into hasher: inout Hasher) { hasher.combine(id) } 28 | static func == (lhs: AirlineInfo, rhs: AirlineInfo) -> Bool { lhs.id == rhs.id } 29 | static func < (lhs: AirlineInfo, rhs: AirlineInfo) -> Bool { lhs.id < rhs.id } 30 | } 31 | 32 | // TODO: share code with AirportInfoRequest 33 | 34 | class AirlineInfoRequest: FlightAwareRequest, Codable { 35 | private(set) var airline: String? 36 | 37 | static var all: [AirlineInfo] { 38 | requests.values.compactMap({ $0.results.value.first }).sorted() 39 | } 40 | 41 | var info: AirlineInfo? { results.value.first } 42 | 43 | private static var requests = [String:AirlineInfoRequest]() 44 | private static var cancellables = [AnyCancellable]() 45 | 46 | @discardableResult 47 | static func fetch(_ airline: String, perform: ((AirlineInfo) -> Void)? = nil) -> AirlineInfo? { 48 | let request = Self.requests[airline] 49 | if request == nil { 50 | Self.requests[airline] = AirlineInfoRequest(airline: airline) 51 | Self.requests[airline]?.fetch() 52 | return self.fetch(airline, perform: perform) 53 | } else if perform != nil { 54 | if let info = request!.info { 55 | perform!(info) 56 | } else { 57 | request!.results.sink { infos in 58 | if let info = infos.first { 59 | perform!(info) 60 | } 61 | }.store(in: &Self.cancellables) 62 | } 63 | } 64 | return Self.requests[airline]?.results.value.first 65 | } 66 | 67 | private init(airline: String) { 68 | super.init() 69 | self.airline = airline 70 | } 71 | 72 | override var query: String { 73 | var request = "AirlineInfo?" 74 | request.addFlightAwareArgument("airlineCode", airline) 75 | return request 76 | } 77 | 78 | override var cacheKey: String? { "\(type(of: self)).\(airline!)" } 79 | 80 | override func decode(_ data: Data) -> Set { 81 | var result = (try? JSONDecoder().decode(AirlineInfoRequest.self, from: data))?.flightAwareResult 82 | result?.code = airline 83 | return Set(result == nil ? [] : [result!]) 84 | } 85 | 86 | private var flightAwareResult: AirlineInfo? 87 | 88 | private enum CodingKeys: String, CodingKey { 89 | case flightAwareResult = "AirlineInfoResult" 90 | } 91 | } 92 | 93 | -------------------------------------------------------------------------------- /projects/Enroute L12/Enroute/FlightAware/FlightAware+AirportInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AirportInfo.swift 3 | // Enroute 4 | // 5 | // Created by CS193p Instructor. 6 | // Copyright © 2020 Stanford University. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | 12 | // json decoded directly from what comes back from FlightAware's "AirportInfo?" 13 | 14 | struct AirportInfo: Codable, Hashable, Identifiable, Comparable { 15 | fileprivate(set) var icao: String? 16 | private(set) var latitude: Double 17 | private(set) var longitude: Double 18 | private(set) var location: String 19 | private(set) var name: String 20 | private(set) var timezone: String 21 | 22 | var friendlyName: String { 23 | Self.friendlyName(name: name, location: location) 24 | } 25 | 26 | static func friendlyName(name: String, location: String) -> String { 27 | var shortName = name 28 | .replacingOccurrences(of: " Intl", with: " ") 29 | .replacingOccurrences(of: " Int'l", with: " ") 30 | .replacingOccurrences(of: "Intl ", with: " ") 31 | .replacingOccurrences(of: "Int'l ", with: " ") 32 | for nameComponent in location.components(separatedBy: ",").map({ $0.trim }) { 33 | shortName = shortName 34 | .replacingOccurrences(of: nameComponent+" ", with: " ") 35 | .replacingOccurrences(of: " "+nameComponent, with: " ") 36 | } 37 | shortName = shortName.trim 38 | shortName = shortName.components(separatedBy: CharacterSet.whitespaces).joined(separator: " ") 39 | if !shortName.isEmpty { 40 | return "\(shortName), \(location)" 41 | } else { 42 | return location 43 | } 44 | } 45 | 46 | var id: String { icao ?? name } 47 | func hash(into hasher: inout Hasher) { hasher.combine(id) } 48 | static func == (lhs: AirportInfo, rhs: AirportInfo) -> Bool { lhs.id == rhs.id } 49 | static func < (lhs: AirportInfo, rhs: AirportInfo) -> Bool { lhs.id < rhs.id } 50 | } 51 | 52 | // TODO: share code with AirlineInfoRequest 53 | 54 | class AirportInfoRequest: FlightAwareRequest, Codable { 55 | private(set) var airport: String? 56 | 57 | static var all: [AirportInfo] { 58 | requests.values.compactMap({ $0.results.value.first }).sorted() 59 | } 60 | 61 | var info: AirportInfo? { results.value.first } 62 | 63 | private static var requests = [String:AirportInfoRequest]() 64 | private static var cancellables = [AnyCancellable]() 65 | 66 | @discardableResult 67 | static func fetch(_ airport: String, perform: ((AirportInfo) -> Void)? = nil) -> AirportInfo? { 68 | let request = Self.requests[airport] 69 | if request == nil { 70 | Self.requests[airport] = AirportInfoRequest(airport: airport) 71 | Self.requests[airport]?.fetch() 72 | return self.fetch(airport, perform: perform) 73 | } else if perform != nil { 74 | if let info = request!.info { 75 | perform!(info) 76 | } else { 77 | request!.results.sink { infos in 78 | if let info = infos.first { 79 | perform!(info) 80 | } 81 | }.store(in: &Self.cancellables) 82 | } 83 | } 84 | return Self.requests[airport]?.results.value.first 85 | } 86 | 87 | private init(airport: String) { 88 | super.init() 89 | self.airport = airport 90 | } 91 | 92 | override var query: String { 93 | var request = "AirportInfo?" 94 | request.addFlightAwareArgument("airportCode", airport) 95 | return request 96 | } 97 | 98 | override var cacheKey: String? { "\(type(of: self)).\(airport!)" } 99 | 100 | override func decode(_ data: Data) -> Set { 101 | var result = (try? JSONDecoder().decode(AirportInfoRequest.self, from: data))?.flightAwareResult 102 | result?.icao = airport 103 | return Set(result == nil ? [] : [result!]) 104 | } 105 | 106 | private var flightAwareResult: AirportInfo? 107 | 108 | private enum CodingKeys: String, CodingKey { 109 | case flightAwareResult = "AirportInfoResult" 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /projects/Enroute L12/Enroute/FlightAware/FlightAware+Enroute.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EnroutRequest.swift 3 | // Enroute 4 | // 5 | // Created by CS193p Instructor. 6 | // Copyright © 2020 Stanford University. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | 12 | // fetches FAFlight objects from FlightAware using "Enroute?" 13 | // (flights enroute to a specified airport) 14 | // generally supports fetching only one airport's enroute flights at a time 15 | // (just to minimize FlightAware API requests) 16 | 17 | class EnrouteRequest: FlightAwareRequest, Codable 18 | { 19 | private(set) var airport: String! 20 | 21 | private static var requests = [String:EnrouteRequest]() 22 | 23 | static func create(airport: String, howMany: Int? = nil) -> EnrouteRequest { 24 | if let request = requests[airport] { 25 | request.howMany = howMany ?? request.howMany 26 | return request 27 | } else { 28 | let request = EnrouteRequest(airport: airport, howMany: howMany) 29 | requests[airport] = request 30 | return request 31 | } 32 | } 33 | 34 | private init(airport: String, howMany: Int? = nil) { 35 | super.init() 36 | self.airport = airport 37 | if howMany != nil { self.howMany = howMany! } 38 | } 39 | 40 | private static var sharedFetchTimer: Timer? 41 | 42 | override var fetchTimer: Timer? { 43 | get { Self.sharedFetchTimer } 44 | set { 45 | Self.sharedFetchTimer?.invalidate() 46 | Self.sharedFetchTimer = newValue 47 | } 48 | } 49 | 50 | override var cacheKey: String? { "\(type(of: self)).\(airport!)" } 51 | 52 | override func decode(_ data: Data) -> Set { 53 | let result = (try? JSONDecoder().decode(EnrouteRequest.self, from: data))?.flightAwareResult 54 | offset = result?.next_offset ?? 0 55 | return Set(result?.enroute ?? []) 56 | } 57 | 58 | override func filter(_ results: Set) -> Set { 59 | results.filter { $0.arrival > Date.currentFlightTime } 60 | } 61 | 62 | override var query: String { 63 | var request = "Enroute?" 64 | request.addFlightAwareArgument("airport", airport) 65 | request.addFlightAwareArgument("howMany", batchSize) 66 | request.addFlightAwareArgument("filter", "airline") 67 | request.addFlightAwareArgument("offset", offset) 68 | return request 69 | } 70 | 71 | private var flightAwareResult: EnrouteResult? 72 | 73 | private enum CodingKeys: String, CodingKey { 74 | case flightAwareResult = "EnrouteResult" 75 | } 76 | 77 | private struct EnrouteResult: Codable { 78 | var next_offset: Int 79 | var enroute: [FAFlight] 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /projects/Enroute L12/Enroute/FlightAware/FlightAwareRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlightAwareRequest.swift 3 | // Enroute 4 | // 5 | // Created by CS193p Instructor. 6 | // Copyright © 2020 Stanford University. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Combine 11 | 12 | // very simple scheduled, sequential fetcher of FlightAware data 13 | // using the FlightAware REST API 14 | // just enough to support our demo needs 15 | // has some simple cacheing to make starting/stopping in demo all the time 16 | // so that it does not overwhelm with FlightAware requests 17 | // (also, FlightAware requests are not free!) 18 | // also has a simple "simulation mode" 19 | // so that it will "work" when no valid FlightAware credentials exist 20 | 21 | // to make this actually fetch from FlightAware 22 | // you need a FlightAware account and an API key 23 | // (fetches are not free, see flightaware.com/api for details) 24 | // put your account name and API key in the Info.plist 25 | // under the key "FlightAware Credentials" 26 | // example credentials: "joepilot:2ab78c93fccc11f999999111030304" 27 | // if that key does not exist, simulation mode automatically kicks in 28 | 29 | class FlightAwareRequest where Fetched: Codable, Fetched: Hashable 30 | { 31 | // this is the latest accumulation of results from fetches 32 | // this is a CurrentValueSubject 33 | // a CurrentValueSubject is a Publisher that holds a value 34 | // and publishes it whenver it changes 35 | private(set) var results = CurrentValueSubject, Never>([]) 36 | 37 | let batchSize = 15 38 | var offset: Int = 0 39 | lazy var howMany: Int = batchSize 40 | private(set) var fetchInterval: TimeInterval = 0 41 | 42 | // MARK: - Subclassers Overrides 43 | 44 | var cacheKey: String? { return nil } // nil means no cacheing 45 | var query: String { "" } // e.g. Enroute?airport=KSFO 46 | func decode(_ json: Data) -> Set { Set() } // json is JSON received from FlightAware 47 | func filter(_ results: Set) -> Set { results } // optional filtering of results 48 | var fetchTimer: Timer? // so that subclasses can throttle fetches of their kind of object 49 | 50 | // MARK: - Private Data 51 | 52 | private var captureSimulationData = false 53 | 54 | private var urlRequest: URLRequest? { Self.authorizedURLRequest(query: query) } 55 | private var fetchCancellable: AnyCancellable? 56 | private var fetchSequenceCount: Int = 0 57 | 58 | private var cacheData: Data? { cacheKey != nil ? UserDefaults.standard.data(forKey: cacheKey!) : nil } 59 | private var cacheTimestampKey: String { (cacheKey ?? "")+".timestamp" } 60 | private var cacheAge: TimeInterval? { 61 | let since1970 = UserDefaults.standard.double(forKey: cacheTimestampKey) 62 | if since1970 > 0 { 63 | return Date.currentFlightTime.timeIntervalSince1970 - since1970 64 | } else { 65 | return nil 66 | } 67 | } 68 | 69 | // MARK: - Fetching 70 | 71 | // sets the fetchInterval to interval and fetch()es 72 | func fetch(andRepeatEvery interval: TimeInterval, useCache: Bool? = nil) { 73 | fetchInterval = interval 74 | if useCache != nil { 75 | fetch(useCache: useCache!) 76 | } else { 77 | fetch() 78 | } 79 | } 80 | 81 | // stops fetching 82 | // fetching can be restarted by calling one of the fetch functions 83 | func stopFetching() { 84 | fetchCancellable?.cancel() 85 | fetchTimer?.invalidate() 86 | fetchInterval = 0 87 | fetchSequenceCount = 0 88 | } 89 | 90 | // immediately fetches new data (from cache if available and requested) 91 | // and, when that data returns, calls handleResults with it 92 | // (which will schedule the next fetch if appropriate) 93 | func fetch(useCache: Bool = true) { 94 | if !useCache || !fetchFromCache() { 95 | if let urlRequest = self.urlRequest { 96 | print("fetching \(urlRequest)") 97 | if offset == 0 { fetchSequenceCount = 0 } 98 | fetchCancellable = URLSession.shared.dataTaskPublisher(for: urlRequest) 99 | .map { [weak self] data, response in 100 | if self?.captureSimulationData ?? false { 101 | flightSimulationData[self?.query ?? ""] = data.utf8 102 | } 103 | return self?.decode(data) ?? [] 104 | } 105 | .replaceError(with: []) 106 | .receive(on: DispatchQueue.main) 107 | .sink { [weak self] results in self?.handleResults(results) } 108 | } else { 109 | if let json = flightSimulationData[query]?.data(using: .utf8) { 110 | print("simulating \(query)") 111 | handleResults(decode(json), isCacheable: false) 112 | } 113 | } 114 | } 115 | } 116 | 117 | // unions the newResults with our existing results.value 118 | // keeps fetching immediately (1s later) if ... 119 | // our results.value.count < howMany 120 | // and we haven't done howMany/15 fetches in a row (throttle) 121 | // otherwise schedules our next fetch after fetchInterval (and caches results) 122 | private func handleResults(_ newResults: Set, age: TimeInterval = 0, isCacheable: Bool = true) { 123 | let existingCount = results.value.count 124 | let newValue = fetchSequenceCount > 0 ? results.value.union(newResults) : newResults.union(results.value) 125 | let added = newValue.count - existingCount 126 | results.value = filter(newValue) 127 | let sequencing = age == 0 && added == batchSize && results.value.count < howMany && fetchSequenceCount < (howMany-(batchSize-1))/batchSize 128 | let interval = sequencing ? 1 : (age > 0 && age < fetchInterval) ? fetchInterval - age : fetchInterval 129 | if isCacheable, age == 0, !sequencing { 130 | cache(newValue) 131 | } 132 | if interval > 0 { // }, urlRequest != nil { 133 | if sequencing { 134 | fetchSequenceCount += 1 135 | } else { 136 | offset = 0 137 | fetchSequenceCount = 0 138 | } 139 | fetchTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: false, block: { [weak self] timer in 140 | if (self?.fetchInterval ?? 0) > 0 || (self?.fetchSequenceCount ?? 0) > 0 { 141 | self?.fetch() 142 | } 143 | }) 144 | } 145 | } 146 | 147 | // MARK: - Cacheing 148 | 149 | // this is mostly because, during a demo, we're constantly re-launching the application 150 | // and there's no need to be refetching data that was just fetched 151 | // the real solution to this is to make the data persistent 152 | // (for example, in Core Data) 153 | 154 | private func fetchFromCache() -> Bool { // returns whether we were able to 155 | if fetchSequenceCount == 0, let key = cacheKey, let age = cacheAge { 156 | if age > 0, (fetchInterval == 0) || (age < fetchInterval) || urlRequest == nil, let data = cacheData { 157 | if let cachedResults = try? JSONDecoder().decode(Set.self, from: data) { 158 | print("using \(Int(age))s old cache \(key)") 159 | handleResults(cachedResults, age: age) 160 | return true 161 | } else { 162 | print("couldn't decode information from \(Int(age))s old cache \(cacheKey!)") 163 | } 164 | } 165 | } 166 | return false 167 | } 168 | 169 | private func cache(_ results: Set) { 170 | if let key = self.cacheKey, let data = try? JSONEncoder().encode(results) { 171 | print("caching \(key) at \(DateFormatter.short.string(from: Date.currentFlightTime))") 172 | UserDefaults.standard.set(Date.currentFlightTime.timeIntervalSince1970, forKey: self.cacheTimestampKey) 173 | UserDefaults.standard.set(data, forKey: key) 174 | } 175 | } 176 | 177 | // MARK: - Utility 178 | 179 | static func authorizedURLRequest(query: String, credentials: String? = Bundle.main.object(forInfoDictionaryKey: "FlightAware Credentials") as? String) -> URLRequest? { 180 | let flightAware = "https://flightxml.flightaware.com/json/FlightXML2/" 181 | if let url = URL(string: flightAware + query), let credentials = (credentials?.isEmpty ?? true) ? nil : credentials?.base64 { 182 | var request = URLRequest(url: url) 183 | request.setValue("Basic \(credentials)", forHTTPHeaderField: "Authorization") 184 | return request 185 | } 186 | return nil 187 | } 188 | } 189 | 190 | // MARK: - Extensions 191 | 192 | extension String { 193 | mutating func addFlightAwareArgument(_ name: String, _ value: Int? = nil, `default` defaultValue: Int = 0) { 194 | if value != nil, value != defaultValue { 195 | addFlightAwareArgument(name, "\(value!)") 196 | } 197 | } 198 | mutating func addFlightAwareArgument(_ name: String, _ value: Date?) { 199 | if value != nil { 200 | addFlightAwareArgument(name, "\(Int(value!.timeIntervalSince1970))") 201 | } 202 | } 203 | 204 | mutating func addFlightAwareArgument(_ name: String, _ value: String?) { 205 | if value != nil { 206 | self += (hasSuffix("?") ? "" : "&") + name + "=" + value! 207 | } 208 | } 209 | } 210 | 211 | // MARK: - Simulation Support 212 | 213 | // while simulating, we pretend its the time the simulation data was grabbed 214 | 215 | extension Date { 216 | private static let launch = Date() 217 | 218 | static var currentFlightTime: Date { 219 | let credentials = Bundle.main.object(forInfoDictionaryKey: "FlightAware Credentials") as? String 220 | if credentials == nil || credentials!.isEmpty, !flightSimulationData.isEmpty, let simulationDate = flightSimulationDate { 221 | return simulationDate.addingTimeInterval(Date().timeIntervalSince(launch)) 222 | } else { 223 | return Date() 224 | } 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /projects/Enroute L12/Enroute/FlightsEnrouteView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlightsEnrouteView.swift 3 | // Enroute 4 | // 5 | // Created by CS193p Instructor. 6 | // Copyright © 2020 Stanford University. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import CoreData 11 | 12 | struct FlightSearch { 13 | var destination: Airport 14 | var origin: Airport? 15 | var airline: Airline? 16 | var inTheAir: Bool = true 17 | } 18 | 19 | extension FlightSearch { 20 | var predicate: NSPredicate { 21 | var format = "destination_ = %@" 22 | var args: [NSManagedObject] = [destination] // args could be [Any] if needed 23 | if origin != nil { 24 | format += " and origin_ = %@" 25 | args.append(origin!) 26 | } 27 | if airline != nil { 28 | format += " and airline_ = %@" 29 | args.append(airline!) 30 | } 31 | if inTheAir { format += " and departure != nil" } 32 | return NSPredicate(format: format, argumentArray: args) 33 | } 34 | } 35 | 36 | struct FlightsEnrouteView: View { 37 | @Environment(\.managedObjectContext) var context 38 | 39 | @State var flightSearch: FlightSearch 40 | 41 | var body: some View { 42 | NavigationView { 43 | FlightList(flightSearch) 44 | .navigationBarItems(leading: simulation, trailing: filter) 45 | } 46 | } 47 | 48 | @State private var showFilter = false 49 | 50 | var filter: some View { 51 | Button("Filter") { 52 | self.showFilter = true 53 | } 54 | .sheet(isPresented: $showFilter) { 55 | FilterFlights(flightSearch: self.$flightSearch, isPresented: self.$showFilter) 56 | .environment(\.managedObjectContext, self.context) 57 | } 58 | } 59 | 60 | // if no FlightAware credentials exist in Info.plist 61 | // then we simulate data from KSFO and KLAS (Las Vegas, NV) 62 | // the simulation time must match the times in the simulation data 63 | // so, to orient the UI, this simulation View shows the time we are simulating 64 | var simulation: some View { 65 | let isSimulating = Date.currentFlightTime.timeIntervalSince(Date()) < -1 66 | return Text(isSimulating ? DateFormatter.shortTime.string(from: Date.currentFlightTime) : "") 67 | } 68 | } 69 | 70 | struct FlightList: View { 71 | @FetchRequest var flights: FetchedResults 72 | 73 | init(_ flightSearch: FlightSearch) { 74 | let request = Flight.fetchRequest(flightSearch.predicate) 75 | _flights = FetchRequest(fetchRequest: request) 76 | } 77 | 78 | var body: some View { 79 | List { 80 | ForEach(flights, id: \.ident) { flight in 81 | FlightListEntry(flight: flight) 82 | } 83 | } 84 | .navigationBarTitle(title) 85 | } 86 | 87 | private var title: String { 88 | let title = "Flights" 89 | if let destination = flights.first?.destination.icao { 90 | return title + " to \(destination)" 91 | } else { 92 | return title 93 | } 94 | } 95 | } 96 | 97 | struct FlightListEntry: View { 98 | @ObservedObject var flight: Flight 99 | 100 | var body: some View { 101 | VStack(alignment: .leading) { 102 | Text(name) 103 | Text(arrives).font(.caption) 104 | Text(origin).font(.caption) 105 | } 106 | .lineLimit(1) 107 | } 108 | 109 | var name: String { 110 | return "\(flight.airline.friendlyName) \(flight.number)" 111 | } 112 | 113 | var arrives: String { 114 | let time = DateFormatter.stringRelativeToToday(Date.currentFlightTime, from: flight.arrival) 115 | if flight.departure == nil { 116 | return "scheduled to arrive \(time) (not departed)" 117 | } else if flight.arrival < Date.currentFlightTime { 118 | return "arrived \(time)" 119 | } else { 120 | return "arrives \(time)" 121 | } 122 | } 123 | 124 | var origin: String { 125 | return "from " + (flight.origin.friendlyName) 126 | } 127 | } 128 | 129 | //struct ContentView_Previews: PreviewProvider { 130 | // static var previews: some View { 131 | // FlightsEnrouteView(flightSearch: FlightSearch(destination: "KSFO")) 132 | // } 133 | //} 134 | -------------------------------------------------------------------------------- /projects/Enroute L12/Enroute/FoundationExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FoundationExtensions.swift 3 | // Enroute 4 | // 5 | // Created by CS193p Instructor. 6 | // Copyright © 2020 Stanford University. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension NSPredicate { 12 | static var all = NSPredicate(format: "TRUEPREDICATE") 13 | static var none = NSPredicate(format: "FALSEPREDICATE") 14 | } 15 | 16 | extension Data { 17 | var utf8: String? { String(data: self, encoding: .utf8 ) } 18 | } 19 | 20 | extension String { 21 | var trim: String { 22 | var trimmed = self.drop(while: { $0.isWhitespace }) 23 | while trimmed.last?.isWhitespace ?? false { 24 | trimmed = trimmed.dropLast() 25 | } 26 | return String(trimmed) 27 | } 28 | 29 | var base64: String? { self.data(using: .utf8)?.base64EncodedString() } 30 | 31 | func contains(elementIn array: [String]) -> Bool { 32 | array.contains(where: { self.contains($0) }) 33 | } 34 | } 35 | 36 | extension DateFormatter { 37 | static var short: DateFormatter = { 38 | let formatter = DateFormatter() 39 | formatter.locale = Locale(identifier: "en_US") 40 | formatter.dateStyle = .short 41 | formatter.timeStyle = .short 42 | return formatter 43 | }() 44 | 45 | static var shortTime: DateFormatter = { 46 | let formatter = DateFormatter() 47 | formatter.locale = Locale(identifier: "en_US") 48 | formatter.dateStyle = .none 49 | formatter.timeStyle = .short 50 | return formatter 51 | }() 52 | 53 | static var shortDate: DateFormatter = { 54 | let formatter = DateFormatter() 55 | formatter.locale = Locale(identifier: "en_US") 56 | formatter.dateStyle = .short 57 | formatter.timeStyle = .none 58 | return formatter 59 | }() 60 | 61 | static func stringRelativeToToday(_ today: Date, from date: Date) -> String { 62 | let dateComponents = Calendar.current.dateComponents(in: .current, from: date) 63 | var nowComponents = Calendar.current.dateComponents(in: .current, from: today) 64 | if dateComponents.isSameDay(as: nowComponents) { 65 | return "today at " + DateFormatter.shortTime.string(from: date) 66 | } 67 | nowComponents = Calendar.current.dateComponents(in: .current, from: today.addingTimeInterval(24*60*60)) 68 | if dateComponents.isSameDay(as: nowComponents) { 69 | return "tomorrow at " + DateFormatter.shortTime.string(from: date) 70 | } 71 | nowComponents = Calendar.current.dateComponents(in: .current, from: today.addingTimeInterval(-24*60*60)) 72 | if dateComponents.isSameDay(as: nowComponents) { 73 | return "yesterday at " + DateFormatter.shortTime.string(from: date) 74 | } 75 | return DateFormatter.short.string(from: date) 76 | } 77 | } 78 | 79 | extension DateComponents { 80 | func isSameDay(as other: DateComponents) -> Bool { 81 | return self.year == other.year && self.month == other.month && self.day == other.day 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /projects/Enroute L12/Enroute/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 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | 37 | 38 | 39 | 40 | UILaunchStoryboardName 41 | LaunchScreen 42 | UIRequiredDeviceCapabilities 43 | 44 | armv7 45 | 46 | UISupportedInterfaceOrientations 47 | 48 | UIInterfaceOrientationPortrait 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | FlightAware Credentials 53 | 54 | UISupportedInterfaceOrientations~ipad 55 | 56 | UIInterfaceOrientationPortrait 57 | UIInterfaceOrientationPortraitUpsideDown 58 | UIInterfaceOrientationLandscapeLeft 59 | UIInterfaceOrientationLandscapeRight 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /projects/Enroute L12/Enroute/L12/Airline.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Airline.swift 3 | // Enroute 4 | // 5 | // Created by CS193p Instructor. 6 | // Copyright © 2020 Stanford University. All rights reserved. 7 | // 8 | 9 | import CoreData 10 | import Combine 11 | 12 | extension Airline: Identifiable, Comparable { 13 | static func withCode(_ code: String, in context: NSManagedObjectContext) -> Airline { 14 | let request = fetchRequest(NSPredicate(format: "code_ = %@", code)) 15 | let results = (try? context.fetch(request)) ?? [] 16 | if let airline = results.first { 17 | return airline 18 | } else { 19 | let airline = Airline(context: context) 20 | airline.code = code 21 | AirlineInfoRequest.fetch(code) { info in 22 | let airline = self.withCode(code, in: context) 23 | airline.name = info.name 24 | airline.shortname = info.shortname 25 | airline.objectWillChange.send() 26 | airline.flights.forEach { $0.objectWillChange.send() } 27 | try? context.save() 28 | } 29 | return airline 30 | } 31 | } 32 | 33 | static func fetchRequest(_ predicate: NSPredicate) -> NSFetchRequest { 34 | let request = NSFetchRequest(entityName: "Airline") 35 | request.sortDescriptors = [NSSortDescriptor(key: "name_", ascending: true)] 36 | request.predicate = predicate 37 | return request 38 | } 39 | 40 | var code: String { 41 | get { code_! } // TODO: maybe protect against when app ships? 42 | set { code_ = newValue } 43 | } 44 | var name: String { 45 | get { name_ ?? code } 46 | set { name_ = newValue } 47 | } 48 | var shortname: String { 49 | get { (shortname_ ?? "").isEmpty ? name : shortname_! } 50 | set { shortname_ = newValue } 51 | } 52 | var flights: Set { 53 | get { (flights_ as? Set) ?? [] } 54 | set { flights_ = newValue as NSSet } 55 | } 56 | var friendlyName: String { shortname.isEmpty ? name : shortname } 57 | 58 | public var id: String { code } 59 | 60 | public static func < (lhs: Airline, rhs: Airline) -> Bool { 61 | lhs.name < rhs.name 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /projects/Enroute L12/Enroute/L12/Airport.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Airport.swift 3 | // Enroute 4 | // 5 | // Created by CS193p Instructor. 6 | // Copyright © 2020 Stanford University. All rights reserved. 7 | // 8 | 9 | import CoreData 10 | import Combine 11 | 12 | extension Airport: Identifiable, Comparable { 13 | static func withICAO(_ icao: String, context: NSManagedObjectContext) -> Airport { 14 | // look up icao in Core Data 15 | let request = fetchRequest(NSPredicate(format: "icao_ = %@", icao)) 16 | let airports = (try? context.fetch(request)) ?? [] 17 | if let airport = airports.first { 18 | // if found, return it 19 | return airport 20 | } else { 21 | // if not, create one and fetch from FlightAware 22 | let airport = Airport(context: context) 23 | airport.icao = icao 24 | AirportInfoRequest.fetch(icao) { airportInfo in 25 | self.update(from: airportInfo, context: context) 26 | } 27 | return airport 28 | } 29 | } 30 | 31 | func fetchIncomingFlights() { 32 | Self.flightAwareRequest?.stopFetching() 33 | if let context = managedObjectContext { 34 | Self.flightAwareRequest = EnrouteRequest.create(airport: icao, howMany: 90) 35 | Self.flightAwareRequest?.fetch(andRepeatEvery: 60) 36 | Self.flightAwareResultsCancellable = Self.flightAwareRequest?.results.sink { results in 37 | for faflight in results { 38 | Flight.update(from: faflight, in: context) 39 | } 40 | do { 41 | try context.save() 42 | } catch(let error) { 43 | print("couldn't save flight update to CoreData: \(error.localizedDescription)") 44 | } 45 | } 46 | } 47 | } 48 | 49 | private static var flightAwareRequest: EnrouteRequest! 50 | private static var flightAwareResultsCancellable: AnyCancellable? 51 | 52 | static func update(from info: AirportInfo, context: NSManagedObjectContext) { 53 | if let icao = info.icao { 54 | let airport = self.withICAO(icao, context: context) 55 | airport.latitude = info.latitude 56 | airport.longitude = info.longitude 57 | airport.name = info.name 58 | airport.location = info.location 59 | airport.timezone = info.timezone 60 | airport.objectWillChange.send() 61 | airport.flightsTo.forEach { $0.objectWillChange.send() } 62 | airport.flightsFrom.forEach { $0.objectWillChange.send() } 63 | try? context.save() 64 | } 65 | } 66 | 67 | static func fetchRequest(_ predicate: NSPredicate) -> NSFetchRequest { 68 | let request = NSFetchRequest(entityName: "Airport") 69 | request.sortDescriptors = [NSSortDescriptor(key: "location", ascending: true)] 70 | request.predicate = predicate 71 | return request 72 | } 73 | 74 | var flightsTo: Set { 75 | get { (flightsTo_ as? Set) ?? [] } 76 | set { flightsTo_ = newValue as NSSet } 77 | } 78 | var flightsFrom: Set { 79 | get { (flightsFrom_ as? Set) ?? [] } 80 | set { flightsFrom_ = newValue as NSSet } 81 | } 82 | 83 | var icao: String { 84 | get { icao_! } // TODO: maybe protect against when app ships? 85 | set { icao_ = newValue } 86 | } 87 | 88 | var friendlyName: String { 89 | let friendly = AirportInfo.friendlyName(name: self.name ?? "", location: self.location ?? "") 90 | return friendly.isEmpty ? icao : friendly 91 | } 92 | 93 | public var id: String { icao } 94 | 95 | public static func < (lhs: Airport, rhs: Airport) -> Bool { 96 | lhs.location ?? lhs.friendlyName < rhs.location ?? rhs.friendlyName 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /projects/Enroute L12/Enroute/L12/Flight.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Flight.swift 3 | // Enroute 4 | // 5 | // Created by CS193p Instructor. 6 | // Copyright © 2020 Stanford University. All rights reserved. 7 | // 8 | 9 | import CoreData 10 | import Combine 11 | 12 | extension Flight { // should probably be Identifiable & Comparable 13 | @discardableResult 14 | static func update(from faflight: FAFlight, in context: NSManagedObjectContext) -> Flight { 15 | let request = fetchRequest(NSPredicate(format: "ident_ = %@", faflight.ident)) 16 | let results = (try? context.fetch(request)) ?? [] 17 | let flight = results.first ?? Flight(context: context) 18 | flight.ident = faflight.ident 19 | flight.origin = Airport.withICAO(faflight.origin, context: context) 20 | flight.destination = Airport.withICAO(faflight.destination, context: context) 21 | flight.arrival = faflight.arrival 22 | flight.departure = faflight.departure 23 | flight.filed = faflight.filed 24 | flight.aircraft = faflight.aircraft 25 | flight.airline = Airline.withCode(faflight.airlineCode, in: context) 26 | flight.objectWillChange.send() 27 | // might want to save() here 28 | // Flights are currently only loaded from Airport.fetchIncomingFlights() 29 | // which saves 30 | // but it might be nice if this method could stand on its own and save itself 31 | return flight 32 | } 33 | 34 | static func fetchRequest(_ predicate: NSPredicate) -> NSFetchRequest { 35 | let request = NSFetchRequest(entityName: "Flight") 36 | request.sortDescriptors = [NSSortDescriptor(key: "arrival_", ascending: true)] 37 | request.predicate = predicate 38 | return request 39 | } 40 | 41 | var arrival: Date { 42 | get { arrival_ ?? Date(timeIntervalSinceReferenceDate: 0) } 43 | set { arrival_ = newValue } 44 | } 45 | var ident: String { 46 | get { ident_ ?? "Unknown" } 47 | set { ident_ = newValue } 48 | } 49 | var destination: Airport { 50 | get { destination_! } // TODO: protect against nil before shipping? 51 | set { destination_ = newValue } 52 | } 53 | var origin: Airport { 54 | get { origin_! } // TODO: maybe protect against when app ships? 55 | set { origin_ = newValue } 56 | } 57 | var airline: Airline { 58 | get { airline_! } // TODO: maybe protect against when app ships? 59 | set { airline_ = newValue } 60 | } 61 | var number: Int { 62 | Int(String(ident.drop(while: { !$0.isNumber }))) ?? 0 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /projects/Enroute L12/Enroute/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /projects/Enroute L12/Enroute/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // Enroute 4 | // 5 | // Created by CS193p Instructor. 6 | // Copyright © 2020 Stanford University. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftUI 11 | 12 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 18 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 19 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 20 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 21 | 22 | // Get the managed object context from the shared persistent container. 23 | let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext 24 | 25 | // Create the SwiftUI view and set the context as the value for the managedObjectContext environment keyPath. 26 | // Add `@Environment(\.managedObjectContext)` in the views that will need the context. 27 | let airport = Airport.withICAO("KSFO", context: context) 28 | airport.fetchIncomingFlights() 29 | let contentView = FlightsEnrouteView(flightSearch: FlightSearch(destination: airport)) 30 | .environment(\.managedObjectContext, context) 31 | 32 | // Use a UIHostingController as window root view controller. 33 | if let windowScene = scene as? UIWindowScene { 34 | let window = UIWindow(windowScene: windowScene) 35 | window.rootViewController = UIHostingController(rootView: contentView) 36 | self.window = window 37 | window.makeKeyAndVisible() 38 | } 39 | } 40 | 41 | func sceneDidDisconnect(_ scene: UIScene) { 42 | // Called as the scene is being released by the system. 43 | // This occurs shortly after the scene enters the background, or when its session is discarded. 44 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 45 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 46 | } 47 | 48 | func sceneDidBecomeActive(_ scene: UIScene) { 49 | // Called when the scene has moved from an inactive state to an active state. 50 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 51 | } 52 | 53 | func sceneWillResignActive(_ scene: UIScene) { 54 | // Called when the scene will move from an active state to an inactive state. 55 | // This may occur due to temporary interruptions (ex. an incoming phone call). 56 | } 57 | 58 | func sceneWillEnterForeground(_ scene: UIScene) { 59 | // Called as the scene transitions from the background to the foreground. 60 | // Use this method to undo the changes made on entering the background. 61 | } 62 | 63 | func sceneDidEnterBackground(_ scene: UIScene) { 64 | // Called as the scene transitions from the foreground to the background. 65 | // Use this method to save data, release shared resources, and store enough scene-specific state information 66 | // to restore the scene back to its current state. 67 | 68 | // Save changes in the application's managed object context when the application transitions to the background. 69 | (UIApplication.shared.delegate as? AppDelegate)?.saveContext() 70 | } 71 | 72 | 73 | } 74 | 75 | -------------------------------------------------------------------------------- /projects/Memorize/Memorize.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 83258E8A2478A76700722F18 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83258E892478A76700722F18 /* AppDelegate.swift */; }; 11 | 83258E8C2478A76700722F18 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83258E8B2478A76700722F18 /* SceneDelegate.swift */; }; 12 | 83258E8E2478A76700722F18 /* EmojiMemoryGameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83258E8D2478A76700722F18 /* EmojiMemoryGameView.swift */; }; 13 | 83258E902478A76A00722F18 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 83258E8F2478A76A00722F18 /* Assets.xcassets */; }; 14 | 83258E932478A76A00722F18 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 83258E922478A76A00722F18 /* Preview Assets.xcassets */; }; 15 | 83258E962478A76A00722F18 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 83258E942478A76A00722F18 /* LaunchScreen.storyboard */; }; 16 | 83258E9E2478A79F00722F18 /* MemoryGame.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83258E9D2478A79F00722F18 /* MemoryGame.swift */; }; 17 | 83258EA02478AA3200722F18 /* EmojiMemoryGame.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83258E9F2478AA3200722F18 /* EmojiMemoryGame.swift */; }; 18 | 83AD67972483264100425D69 /* Grid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83AD67962483264000425D69 /* Grid.swift */; }; 19 | 83AD679B24833ACF00425D69 /* GridLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83AD679A24833ACF00425D69 /* GridLayout.swift */; }; 20 | 83AD679D2484285F00425D69 /* Array+Identifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83AD679C2484285F00425D69 /* Array+Identifiable.swift */; }; 21 | 83AD679F2484834300425D69 /* Array+Only.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83AD679E2484834300425D69 /* Array+Only.swift */; }; 22 | 83AD67A124888A3300425D69 /* Pie.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83AD67A024888A3300425D69 /* Pie.swift */; }; 23 | 83AD67A32488A44600425D69 /* Cardify.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83AD67A22488A44600425D69 /* Cardify.swift */; }; 24 | /* End PBXBuildFile section */ 25 | 26 | /* Begin PBXFileReference section */ 27 | 83258E862478A76700722F18 /* Memorize.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Memorize.app; sourceTree = BUILT_PRODUCTS_DIR; }; 28 | 83258E892478A76700722F18 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 29 | 83258E8B2478A76700722F18 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 30 | 83258E8D2478A76700722F18 /* EmojiMemoryGameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiMemoryGameView.swift; sourceTree = ""; }; 31 | 83258E8F2478A76A00722F18 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 32 | 83258E922478A76A00722F18 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 33 | 83258E952478A76A00722F18 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 34 | 83258E972478A76A00722F18 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 35 | 83258E9D2478A79F00722F18 /* MemoryGame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryGame.swift; sourceTree = ""; }; 36 | 83258E9F2478AA3200722F18 /* EmojiMemoryGame.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiMemoryGame.swift; sourceTree = ""; }; 37 | 83AD67962483264000425D69 /* Grid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Grid.swift; sourceTree = ""; }; 38 | 83AD679A24833ACF00425D69 /* GridLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridLayout.swift; sourceTree = ""; }; 39 | 83AD679C2484285F00425D69 /* Array+Identifiable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Identifiable.swift"; sourceTree = ""; }; 40 | 83AD679E2484834300425D69 /* Array+Only.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Only.swift"; sourceTree = ""; }; 41 | 83AD67A024888A3300425D69 /* Pie.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pie.swift; sourceTree = ""; }; 42 | 83AD67A22488A44600425D69 /* Cardify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cardify.swift; sourceTree = ""; }; 43 | /* End PBXFileReference section */ 44 | 45 | /* Begin PBXFrameworksBuildPhase section */ 46 | 83258E832478A76700722F18 /* Frameworks */ = { 47 | isa = PBXFrameworksBuildPhase; 48 | buildActionMask = 2147483647; 49 | files = ( 50 | ); 51 | runOnlyForDeploymentPostprocessing = 0; 52 | }; 53 | /* End PBXFrameworksBuildPhase section */ 54 | 55 | /* Begin PBXGroup section */ 56 | 83258E7D2478A76700722F18 = { 57 | isa = PBXGroup; 58 | children = ( 59 | 83258E882478A76700722F18 /* Memorize */, 60 | 83258E872478A76700722F18 /* Products */, 61 | ); 62 | sourceTree = ""; 63 | }; 64 | 83258E872478A76700722F18 /* Products */ = { 65 | isa = PBXGroup; 66 | children = ( 67 | 83258E862478A76700722F18 /* Memorize.app */, 68 | ); 69 | name = Products; 70 | sourceTree = ""; 71 | }; 72 | 83258E882478A76700722F18 /* Memorize */ = { 73 | isa = PBXGroup; 74 | children = ( 75 | 83258E9D2478A79F00722F18 /* MemoryGame.swift */, 76 | 83AD679C2484285F00425D69 /* Array+Identifiable.swift */, 77 | 83AD679E2484834300425D69 /* Array+Only.swift */, 78 | 83258E892478A76700722F18 /* AppDelegate.swift */, 79 | 83258E9F2478AA3200722F18 /* EmojiMemoryGame.swift */, 80 | 83AD679A24833ACF00425D69 /* GridLayout.swift */, 81 | 83AD67A024888A3300425D69 /* Pie.swift */, 82 | 83AD67A22488A44600425D69 /* Cardify.swift */, 83 | 83258E8B2478A76700722F18 /* SceneDelegate.swift */, 84 | 83AD67962483264000425D69 /* Grid.swift */, 85 | 83258E8D2478A76700722F18 /* EmojiMemoryGameView.swift */, 86 | 83258E8F2478A76A00722F18 /* Assets.xcassets */, 87 | 83258E942478A76A00722F18 /* LaunchScreen.storyboard */, 88 | 83258E972478A76A00722F18 /* Info.plist */, 89 | 83258E912478A76A00722F18 /* Preview Content */, 90 | ); 91 | path = Memorize; 92 | sourceTree = ""; 93 | }; 94 | 83258E912478A76A00722F18 /* Preview Content */ = { 95 | isa = PBXGroup; 96 | children = ( 97 | 83258E922478A76A00722F18 /* Preview Assets.xcassets */, 98 | ); 99 | path = "Preview Content"; 100 | sourceTree = ""; 101 | }; 102 | /* End PBXGroup section */ 103 | 104 | /* Begin PBXNativeTarget section */ 105 | 83258E852478A76700722F18 /* Memorize */ = { 106 | isa = PBXNativeTarget; 107 | buildConfigurationList = 83258E9A2478A76A00722F18 /* Build configuration list for PBXNativeTarget "Memorize" */; 108 | buildPhases = ( 109 | 83258E822478A76700722F18 /* Sources */, 110 | 83258E832478A76700722F18 /* Frameworks */, 111 | 83258E842478A76700722F18 /* Resources */, 112 | ); 113 | buildRules = ( 114 | ); 115 | dependencies = ( 116 | ); 117 | name = Memorize; 118 | productName = Memorize; 119 | productReference = 83258E862478A76700722F18 /* Memorize.app */; 120 | productType = "com.apple.product-type.application"; 121 | }; 122 | /* End PBXNativeTarget section */ 123 | 124 | /* Begin PBXProject section */ 125 | 83258E7E2478A76700722F18 /* Project object */ = { 126 | isa = PBXProject; 127 | attributes = { 128 | LastSwiftUpdateCheck = 1140; 129 | LastUpgradeCheck = 1140; 130 | ORGANIZATIONNAME = "Tieda Wei"; 131 | TargetAttributes = { 132 | 83258E852478A76700722F18 = { 133 | CreatedOnToolsVersion = 11.4.1; 134 | }; 135 | }; 136 | }; 137 | buildConfigurationList = 83258E812478A76700722F18 /* Build configuration list for PBXProject "Memorize" */; 138 | compatibilityVersion = "Xcode 9.3"; 139 | developmentRegion = en; 140 | hasScannedForEncodings = 0; 141 | knownRegions = ( 142 | en, 143 | Base, 144 | ); 145 | mainGroup = 83258E7D2478A76700722F18; 146 | productRefGroup = 83258E872478A76700722F18 /* Products */; 147 | projectDirPath = ""; 148 | projectRoot = ""; 149 | targets = ( 150 | 83258E852478A76700722F18 /* Memorize */, 151 | ); 152 | }; 153 | /* End PBXProject section */ 154 | 155 | /* Begin PBXResourcesBuildPhase section */ 156 | 83258E842478A76700722F18 /* Resources */ = { 157 | isa = PBXResourcesBuildPhase; 158 | buildActionMask = 2147483647; 159 | files = ( 160 | 83258E962478A76A00722F18 /* LaunchScreen.storyboard in Resources */, 161 | 83258E932478A76A00722F18 /* Preview Assets.xcassets in Resources */, 162 | 83258E902478A76A00722F18 /* Assets.xcassets in Resources */, 163 | ); 164 | runOnlyForDeploymentPostprocessing = 0; 165 | }; 166 | /* End PBXResourcesBuildPhase section */ 167 | 168 | /* Begin PBXSourcesBuildPhase section */ 169 | 83258E822478A76700722F18 /* Sources */ = { 170 | isa = PBXSourcesBuildPhase; 171 | buildActionMask = 2147483647; 172 | files = ( 173 | 83AD67A32488A44600425D69 /* Cardify.swift in Sources */, 174 | 83AD67A124888A3300425D69 /* Pie.swift in Sources */, 175 | 83258E8A2478A76700722F18 /* AppDelegate.swift in Sources */, 176 | 83258E8C2478A76700722F18 /* SceneDelegate.swift in Sources */, 177 | 83258E9E2478A79F00722F18 /* MemoryGame.swift in Sources */, 178 | 83AD679F2484834300425D69 /* Array+Only.swift in Sources */, 179 | 83258E8E2478A76700722F18 /* EmojiMemoryGameView.swift in Sources */, 180 | 83258EA02478AA3200722F18 /* EmojiMemoryGame.swift in Sources */, 181 | 83AD679D2484285F00425D69 /* Array+Identifiable.swift in Sources */, 182 | 83AD679B24833ACF00425D69 /* GridLayout.swift in Sources */, 183 | 83AD67972483264100425D69 /* Grid.swift in Sources */, 184 | ); 185 | runOnlyForDeploymentPostprocessing = 0; 186 | }; 187 | /* End PBXSourcesBuildPhase section */ 188 | 189 | /* Begin PBXVariantGroup section */ 190 | 83258E942478A76A00722F18 /* LaunchScreen.storyboard */ = { 191 | isa = PBXVariantGroup; 192 | children = ( 193 | 83258E952478A76A00722F18 /* Base */, 194 | ); 195 | name = LaunchScreen.storyboard; 196 | sourceTree = ""; 197 | }; 198 | /* End PBXVariantGroup section */ 199 | 200 | /* Begin XCBuildConfiguration section */ 201 | 83258E982478A76A00722F18 /* Debug */ = { 202 | isa = XCBuildConfiguration; 203 | buildSettings = { 204 | ALWAYS_SEARCH_USER_PATHS = NO; 205 | CLANG_ANALYZER_NONNULL = YES; 206 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 207 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 208 | CLANG_CXX_LIBRARY = "libc++"; 209 | CLANG_ENABLE_MODULES = YES; 210 | CLANG_ENABLE_OBJC_ARC = YES; 211 | CLANG_ENABLE_OBJC_WEAK = YES; 212 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 213 | CLANG_WARN_BOOL_CONVERSION = YES; 214 | CLANG_WARN_COMMA = YES; 215 | CLANG_WARN_CONSTANT_CONVERSION = YES; 216 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 217 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 218 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 219 | CLANG_WARN_EMPTY_BODY = YES; 220 | CLANG_WARN_ENUM_CONVERSION = YES; 221 | CLANG_WARN_INFINITE_RECURSION = YES; 222 | CLANG_WARN_INT_CONVERSION = YES; 223 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 224 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 225 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 226 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 227 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 228 | CLANG_WARN_STRICT_PROTOTYPES = YES; 229 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 230 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 231 | CLANG_WARN_UNREACHABLE_CODE = YES; 232 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 233 | COPY_PHASE_STRIP = NO; 234 | DEBUG_INFORMATION_FORMAT = dwarf; 235 | ENABLE_STRICT_OBJC_MSGSEND = YES; 236 | ENABLE_TESTABILITY = YES; 237 | GCC_C_LANGUAGE_STANDARD = gnu11; 238 | GCC_DYNAMIC_NO_PIC = NO; 239 | GCC_NO_COMMON_BLOCKS = YES; 240 | GCC_OPTIMIZATION_LEVEL = 0; 241 | GCC_PREPROCESSOR_DEFINITIONS = ( 242 | "DEBUG=1", 243 | "$(inherited)", 244 | ); 245 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 246 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 247 | GCC_WARN_UNDECLARED_SELECTOR = YES; 248 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 249 | GCC_WARN_UNUSED_FUNCTION = YES; 250 | GCC_WARN_UNUSED_VARIABLE = YES; 251 | IPHONEOS_DEPLOYMENT_TARGET = 13.4; 252 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 253 | MTL_FAST_MATH = YES; 254 | ONLY_ACTIVE_ARCH = YES; 255 | SDKROOT = iphoneos; 256 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 257 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 258 | }; 259 | name = Debug; 260 | }; 261 | 83258E992478A76A00722F18 /* Release */ = { 262 | isa = XCBuildConfiguration; 263 | buildSettings = { 264 | ALWAYS_SEARCH_USER_PATHS = NO; 265 | CLANG_ANALYZER_NONNULL = YES; 266 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 267 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 268 | CLANG_CXX_LIBRARY = "libc++"; 269 | CLANG_ENABLE_MODULES = YES; 270 | CLANG_ENABLE_OBJC_ARC = YES; 271 | CLANG_ENABLE_OBJC_WEAK = YES; 272 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 273 | CLANG_WARN_BOOL_CONVERSION = YES; 274 | CLANG_WARN_COMMA = YES; 275 | CLANG_WARN_CONSTANT_CONVERSION = YES; 276 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 277 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 278 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 279 | CLANG_WARN_EMPTY_BODY = YES; 280 | CLANG_WARN_ENUM_CONVERSION = YES; 281 | CLANG_WARN_INFINITE_RECURSION = YES; 282 | CLANG_WARN_INT_CONVERSION = YES; 283 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 284 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 285 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 286 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 287 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 288 | CLANG_WARN_STRICT_PROTOTYPES = YES; 289 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 290 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 291 | CLANG_WARN_UNREACHABLE_CODE = YES; 292 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 293 | COPY_PHASE_STRIP = NO; 294 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 295 | ENABLE_NS_ASSERTIONS = NO; 296 | ENABLE_STRICT_OBJC_MSGSEND = YES; 297 | GCC_C_LANGUAGE_STANDARD = gnu11; 298 | GCC_NO_COMMON_BLOCKS = YES; 299 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 300 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 301 | GCC_WARN_UNDECLARED_SELECTOR = YES; 302 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 303 | GCC_WARN_UNUSED_FUNCTION = YES; 304 | GCC_WARN_UNUSED_VARIABLE = YES; 305 | IPHONEOS_DEPLOYMENT_TARGET = 13.4; 306 | MTL_ENABLE_DEBUG_INFO = NO; 307 | MTL_FAST_MATH = YES; 308 | SDKROOT = iphoneos; 309 | SWIFT_COMPILATION_MODE = wholemodule; 310 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 311 | VALIDATE_PRODUCT = YES; 312 | }; 313 | name = Release; 314 | }; 315 | 83258E9B2478A76A00722F18 /* Debug */ = { 316 | isa = XCBuildConfiguration; 317 | buildSettings = { 318 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 319 | CODE_SIGN_STYLE = Automatic; 320 | DEVELOPMENT_ASSET_PATHS = "\"Memorize/Preview Content\""; 321 | DEVELOPMENT_TEAM = T64RAU2S7P; 322 | ENABLE_PREVIEWS = YES; 323 | INFOPLIST_FILE = Memorize/Info.plist; 324 | LD_RUNPATH_SEARCH_PATHS = ( 325 | "$(inherited)", 326 | "@executable_path/Frameworks", 327 | ); 328 | PRODUCT_BUNDLE_IDENTIFIER = com.tiedawei.Memorize; 329 | PRODUCT_NAME = "$(TARGET_NAME)"; 330 | SWIFT_VERSION = 5.0; 331 | TARGETED_DEVICE_FAMILY = "1,2"; 332 | }; 333 | name = Debug; 334 | }; 335 | 83258E9C2478A76A00722F18 /* Release */ = { 336 | isa = XCBuildConfiguration; 337 | buildSettings = { 338 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 339 | CODE_SIGN_STYLE = Automatic; 340 | DEVELOPMENT_ASSET_PATHS = "\"Memorize/Preview Content\""; 341 | DEVELOPMENT_TEAM = T64RAU2S7P; 342 | ENABLE_PREVIEWS = YES; 343 | INFOPLIST_FILE = Memorize/Info.plist; 344 | LD_RUNPATH_SEARCH_PATHS = ( 345 | "$(inherited)", 346 | "@executable_path/Frameworks", 347 | ); 348 | PRODUCT_BUNDLE_IDENTIFIER = com.tiedawei.Memorize; 349 | PRODUCT_NAME = "$(TARGET_NAME)"; 350 | SWIFT_VERSION = 5.0; 351 | TARGETED_DEVICE_FAMILY = "1,2"; 352 | }; 353 | name = Release; 354 | }; 355 | /* End XCBuildConfiguration section */ 356 | 357 | /* Begin XCConfigurationList section */ 358 | 83258E812478A76700722F18 /* Build configuration list for PBXProject "Memorize" */ = { 359 | isa = XCConfigurationList; 360 | buildConfigurations = ( 361 | 83258E982478A76A00722F18 /* Debug */, 362 | 83258E992478A76A00722F18 /* Release */, 363 | ); 364 | defaultConfigurationIsVisible = 0; 365 | defaultConfigurationName = Release; 366 | }; 367 | 83258E9A2478A76A00722F18 /* Build configuration list for PBXNativeTarget "Memorize" */ = { 368 | isa = XCConfigurationList; 369 | buildConfigurations = ( 370 | 83258E9B2478A76A00722F18 /* Debug */, 371 | 83258E9C2478A76A00722F18 /* Release */, 372 | ); 373 | defaultConfigurationIsVisible = 0; 374 | defaultConfigurationName = Release; 375 | }; 376 | /* End XCConfigurationList section */ 377 | }; 378 | rootObject = 83258E7E2478A76700722F18 /* Project object */; 379 | } 380 | -------------------------------------------------------------------------------- /projects/Memorize/Memorize.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /projects/Memorize/Memorize.xcodeproj/project.xcworkspace/xcuserdata/Tieda.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weitieda/cs193p-2020-swiftui/ba90cdc1d0b0ae59e386e9640c2f2d27bc2bcc15/projects/Memorize/Memorize.xcodeproj/project.xcworkspace/xcuserdata/Tieda.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /projects/Memorize/Memorize.xcodeproj/xcuserdata/Tieda.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Memorize.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /projects/Memorize/Memorize/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Memorize 4 | // 5 | // Created by Tieda Wei on 2020-05-22. 6 | // Copyright © 2020 Tieda Wei. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | // MARK: UISceneSession Lifecycle 22 | 23 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 24 | // Called when a new scene session is being created. 25 | // Use this method to select a configuration to create the new scene with. 26 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 27 | } 28 | 29 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 30 | // Called when the user discards a scene session. 31 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 32 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 33 | } 34 | 35 | 36 | } 37 | 38 | -------------------------------------------------------------------------------- /projects/Memorize/Memorize/Array+Identifiable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Array+Identifiable.swift 3 | // Memorize 4 | // 5 | // Created by Tieda Wei on 2020-05-31. 6 | // Copyright © 2020 Tieda Wei. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Array where Element: Identifiable { 12 | func firstIndex(matching: Element) -> Int? { 13 | for index in 0.. 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 | -------------------------------------------------------------------------------- /projects/Memorize/Memorize/Cardify.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Cardify.swift 3 | // Memorize 4 | // 5 | // Created by Tieda Wei on 2020-06-03. 6 | // Copyright © 2020 Tieda Wei. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct Cardify: AnimatableModifier { 12 | private let cornerRadius: CGFloat = 10 13 | private let edgeLineWidth: CGFloat = 3 14 | 15 | var rotation: Double 16 | 17 | var isFaceUp: Bool { 18 | rotation < 90 19 | } 20 | 21 | var animatableData: Double { 22 | get { rotation } 23 | set { rotation = newValue } 24 | } 25 | 26 | init(isFaceUp: Bool) { 27 | rotation = isFaceUp ? 0 : 180 28 | } 29 | 30 | func body(content: Content) -> some View { 31 | ZStack { 32 | Group { 33 | RoundedRectangle(cornerRadius: cornerRadius).fill(Color.white) 34 | RoundedRectangle(cornerRadius: cornerRadius).stroke(lineWidth: edgeLineWidth) 35 | content 36 | } 37 | .opacity(isFaceUp ? 1 : 0) 38 | 39 | RoundedRectangle(cornerRadius: cornerRadius) 40 | .fill() 41 | .opacity(isFaceUp ? 0 : 1) 42 | } 43 | .rotation3DEffect(Angle(degrees: rotation), axis: (0, 1, 0)) 44 | } 45 | } 46 | 47 | extension View { 48 | func cardify(isFaceUp: Bool) -> some View { 49 | self.modifier(Cardify(isFaceUp: isFaceUp)) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /projects/Memorize/Memorize/EmojiMemoryGame.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmojiMemoryGame.swift 3 | // Memorize 4 | // 5 | // Created by Tieda Wei on 2020-05-22. 6 | // Copyright © 2020 Tieda Wei. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | // ViewModel: portal for view to get model 12 | // ViewModel never talks to its View(that's why no View related stuff in ViewModel class), it's the View talks to its ViewModel 13 | // Many views may use the same ViewModel, that's why it's a class(reference) 14 | class EmojiMemoryGame: ObservableObject { 15 | 16 | // You should never name a variable "model", here's teaching purpose, name it "game" instead 17 | // "portal/doorway" for the view 18 | @Published private var model: MemoryGame = createMemoryGame() 19 | 20 | var cards: [MemoryGame.Card] { 21 | model.cards 22 | } 23 | 24 | static private func createMemoryGame() -> MemoryGame { 25 | let emojis = ["😎", "👻", "🎃"] 26 | return MemoryGame(numberOfPairsOfCard: emojis.count) { pairIndex in emojis[pairIndex]} 27 | } 28 | 29 | // (User) Intent 30 | func choose(card: MemoryGame.Card) { 31 | model.choose(card: card) 32 | } 33 | 34 | func resetGame() { 35 | model = EmojiMemoryGame.createMemoryGame() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /projects/Memorize/Memorize/EmojiMemoryGameView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmojiMemoryGameView.swift 3 | // Memorize 4 | // 5 | // Created by Tieda Wei on 2020-05-22. 6 | // Copyright © 2020 Tieda Wei. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct EmojiMemoryGameView: View { 12 | 13 | @ObservedObject var viewModel: EmojiMemoryGame 14 | 15 | // You never access this "var body" 16 | // This "body" is called by the system when it needs to draw the View 17 | var body: some View { 18 | VStack { 19 | Grid(viewModel.cards) { card in 20 | CardView(card: card).onTapGesture { 21 | withAnimation(.easeInOut(duration: 0.75)) { 22 | self.viewModel.choose(card: card) 23 | } 24 | }.padding(5) 25 | } 26 | .padding() 27 | .foregroundColor(Color.orange) 28 | 29 | Button(action: { 30 | withAnimation(.easeInOut) { 31 | self.viewModel.resetGame() 32 | } 33 | }, label: { 34 | Text("New Game") 35 | }) 36 | } 37 | } 38 | } 39 | 40 | struct CardView: View { 41 | let card: MemoryGame.Card 42 | 43 | var body: some View { 44 | GeometryReader { geometry in 45 | self.body(for: geometry.size) 46 | } 47 | } 48 | 49 | @State private var animatedBonusRemaining: Double = 0 50 | 51 | private func startBonusTimeAnimation() { 52 | animatedBonusRemaining = card.bonusRemaining 53 | withAnimation(.linear(duration: card.bonusTimeRemaining)) { 54 | animatedBonusRemaining = 0 55 | } 56 | } 57 | 58 | @ViewBuilder 59 | private func body(for size: CGSize) -> some View { 60 | if card.isFaceUp || !card.isMatched { 61 | ZStack { 62 | Group { 63 | if card.isConsumingBonusTime { 64 | Pie(startAngle: Angle.degrees(-90), endAngle: Angle.degrees(-animatedBonusRemaining*360-90)) 65 | .onAppear { 66 | self.startBonusTimeAnimation() 67 | } 68 | } else { 69 | Pie(startAngle: Angle.degrees(-90), endAngle: Angle.degrees(-card.bonusRemaining*360-90)) 70 | } 71 | } 72 | .padding(5) 73 | .opacity(0.4) 74 | 75 | Text(card.content) 76 | .font(Font.system(size: fontSize(for: size))) 77 | .rotationEffect(Angle(degrees: card.isMatched ? 360 : 0)) 78 | .animation(card.isMatched ? Animation.linear(duration: 1).repeatForever(autoreverses: false) : .default) 79 | } 80 | .cardify(isFaceUp: card.isFaceUp) 81 | .transition(.scale) 82 | } 83 | } 84 | 85 | private func fontSize(for size: CGSize) -> CGFloat { 86 | min(size.width, size.height) * 0.7 87 | } 88 | } 89 | 90 | struct ContentView_Previews: PreviewProvider { 91 | 92 | static var previews: some View { 93 | let game = EmojiMemoryGame() 94 | game.choose(card: game.cards[0]) 95 | return EmojiMemoryGameView(viewModel: game) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /projects/Memorize/Memorize/Grid.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Grid.swift 3 | // Memorize 4 | // 5 | // Created by Tieda Wei on 2020-05-30. 6 | // Copyright © 2020 Tieda Wei. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct Grid: View where Item: Identifiable, ItemView: View { 12 | 13 | private var items: [Item] 14 | private var viewForItem: (Item) -> ItemView 15 | 16 | // @escaping: viewViewItem, is not used in the Initializer; this closure will be called out of Initializer (in the var body) 17 | init(_ items: [Item], viewForItem: @escaping (Item) -> ItemView) { 18 | self.items = items 19 | self.viewForItem = viewForItem 20 | } 21 | 22 | var body: some View { 23 | GeometryReader { geometry in 24 | self.body(for: GridLayout(itemCount: self.items.count, in: geometry.size)) 25 | } 26 | } 27 | 28 | private func body(for layout: GridLayout) -> some View { 29 | ForEach(items) { item in 30 | self.body(for: item, in: layout) 31 | } 32 | } 33 | 34 | private func body(for item: Item, in layout: GridLayout) -> some View { 35 | let index = items.firstIndex(matching: item)! 36 | return viewForItem(item) 37 | .frame(width: layout.itemSize.width, height: layout.itemSize.height) 38 | .position(layout.location(ofItemAt: index)) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /projects/Memorize/Memorize/GridLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GridLayout.swift 3 | // Memorize 4 | // 5 | // Created by CS193p Instructor. 6 | // Copyright © 2020 Stanford University. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct GridLayout { 12 | private(set) var size: CGSize 13 | private(set) var rowCount: Int = 0 14 | private(set) var columnCount: Int = 0 15 | 16 | init(itemCount: Int, nearAspectRatio desiredAspectRatio: Double = 1, in size: CGSize) { 17 | self.size = size 18 | // if our size is zero width or height or the itemCount is not > 0 19 | // then we have no work to do (because our rowCount & columnCount will be zero) 20 | guard size.width != 0, size.height != 0, itemCount > 0 else { return } 21 | // find the bestLayout 22 | // i.e., one which results in cells whose aspectRatio 23 | // has the smallestVariance from desiredAspectRatio 24 | // not necessarily most optimal code to do this, but easy to follow (hopefully) 25 | var bestLayout: (rowCount: Int, columnCount: Int) = (1, itemCount) 26 | var smallestVariance: Double? 27 | let sizeAspectRatio = abs(Double(size.width/size.height)) 28 | for rows in 1...itemCount { 29 | let columns = (itemCount / rows) + (itemCount % rows > 0 ? 1 : 0) 30 | if (rows - 1) * columns < itemCount { 31 | let itemAspectRatio = sizeAspectRatio * (Double(rows)/Double(columns)) 32 | let variance = abs(itemAspectRatio - desiredAspectRatio) 33 | if smallestVariance == nil || variance < smallestVariance! { 34 | smallestVariance = variance 35 | bestLayout = (rowCount: rows, columnCount: columns) 36 | } 37 | } 38 | } 39 | rowCount = bestLayout.rowCount 40 | columnCount = bestLayout.columnCount 41 | } 42 | 43 | var itemSize: CGSize { 44 | if rowCount == 0 || columnCount == 0 { 45 | return CGSize.zero 46 | } else { 47 | return CGSize( 48 | width: size.width / CGFloat(columnCount), 49 | height: size.height / CGFloat(rowCount) 50 | ) 51 | } 52 | } 53 | 54 | func location(ofItemAt index: Int) -> CGPoint { 55 | if rowCount == 0 || columnCount == 0 { 56 | return CGPoint.zero 57 | } else { 58 | return CGPoint( 59 | x: (CGFloat(index % columnCount) + 0.5) * itemSize.width, 60 | y: (CGFloat(index / columnCount) + 0.5) * itemSize.height 61 | ) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /projects/Memorize/Memorize/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 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | 37 | 38 | 39 | 40 | UILaunchStoryboardName 41 | LaunchScreen 42 | UIRequiredDeviceCapabilities 43 | 44 | armv7 45 | 46 | UISupportedInterfaceOrientations 47 | 48 | UIInterfaceOrientationPortrait 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | UISupportedInterfaceOrientations~ipad 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationPortraitUpsideDown 56 | UIInterfaceOrientationLandscapeLeft 57 | UIInterfaceOrientationLandscapeRight 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /projects/Memorize/Memorize/MemoryGame.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MemoryGame.swift 3 | // Memorize 4 | // 5 | // Created by Tieda Wei on 2020-05-22. 6 | // Copyright © 2020 Tieda Wei. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct MemoryGame where CardContent: Equatable { 12 | private(set) var cards: [Card] 13 | 14 | private var indexOfTheOneAndOnlyFaceUpCard: Int? { 15 | get { cards.indices.filter {cards[$0].isFaceUp}.only } 16 | set { 17 | for index in cards.indices { 18 | cards[index].isFaceUp = index == newValue 19 | } 20 | } 21 | } 22 | 23 | init(numberOfPairsOfCard: Int, cardContentFactory: (Int) -> CardContent) { 24 | cards = Array() 25 | for pairIndex in 0.. 0 && bonusTimeRemaining > 0) ? bonusTimeRemaining/bonusTimeLimit : 0 86 | } 87 | 88 | var hasEarnedBonus: Bool { 89 | isMatched && bonusTimeRemaining > 0 90 | } 91 | 92 | var isConsumingBonusTime: Bool { 93 | isFaceUp && !isMatched && bonusTimeRemaining > 0 94 | } 95 | 96 | private mutating func startUsingBonusTime() { 97 | if isConsumingBonusTime, lastFaceUpDate == nil { 98 | lastFaceUpDate = Date() 99 | } 100 | } 101 | 102 | private mutating func stopUsingBonusTime() { 103 | pastFaceUpTime = faceUpTime 104 | lastFaceUpDate = nil 105 | } 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /projects/Memorize/Memorize/Pie.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Pie.swift 3 | // Memorize 4 | // 5 | // Created by Tieda Wei on 2020-06-03. 6 | // Copyright © 2020 Tieda Wei. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct Pie: Shape { 12 | 13 | var startAngle: Angle 14 | var endAngle: Angle 15 | 16 | var clockwise = true 17 | 18 | var animatableData: AnimatablePair { 19 | get { 20 | AnimatablePair(startAngle.radians, endAngle.radians) 21 | } 22 | set { 23 | startAngle = Angle.radians(newValue.first) 24 | endAngle = Angle.radians(newValue.second) 25 | } 26 | } 27 | 28 | func path(in rect: CGRect) -> Path { 29 | 30 | let center = CGPoint(x: rect.midX, y: rect.midY) 31 | let radius = min(rect.width, rect.height) / 2 32 | let start = CGPoint( 33 | x: center.x + radius * cos(CGFloat(startAngle.radians)), 34 | y: center.y + radius * sin(CGFloat(startAngle.radians)) 35 | ) 36 | 37 | var p = Path() 38 | p.move(to: center) 39 | p.addLine(to: start) 40 | p.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: clockwise) 41 | p.addLine(to: center) 42 | 43 | return p 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /projects/Memorize/Memorize/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /projects/Memorize/Memorize/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // Memorize 4 | // 5 | // Created by Tieda Wei on 2020-05-22. 6 | // Copyright © 2020 Tieda Wei. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftUI 11 | 12 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 18 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 19 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 20 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 21 | 22 | // Create the SwiftUI view that provides the window contents. 23 | let game = EmojiMemoryGame() 24 | let contentView = EmojiMemoryGameView(viewModel: game) 25 | 26 | // Use a UIHostingController as window root view controller. 27 | if let windowScene = scene as? UIWindowScene { 28 | let window = UIWindow(windowScene: windowScene) 29 | window.rootViewController = UIHostingController(rootView: contentView) 30 | self.window = window 31 | window.makeKeyAndVisible() 32 | } 33 | } 34 | 35 | func sceneDidDisconnect(_ scene: UIScene) { 36 | // Called as the scene is being released by the system. 37 | // This occurs shortly after the scene enters the background, or when its session is discarded. 38 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 39 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 40 | } 41 | 42 | func sceneDidBecomeActive(_ scene: UIScene) { 43 | // Called when the scene has moved from an inactive state to an active state. 44 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 45 | } 46 | 47 | func sceneWillResignActive(_ scene: UIScene) { 48 | // Called when the scene will move from an active state to an inactive state. 49 | // This may occur due to temporary interruptions (ex. an incoming phone call). 50 | } 51 | 52 | func sceneWillEnterForeground(_ scene: UIScene) { 53 | // Called as the scene transitions from the background to the foreground. 54 | // Use this method to undo the changes made on entering the background. 55 | } 56 | 57 | func sceneDidEnterBackground(_ scene: UIScene) { 58 | // Called as the scene transitions from the foreground to the background. 59 | // Use this method to save data, release shared resources, and store enough scene-specific state information 60 | // to restore the scene back to its current state. 61 | } 62 | 63 | 64 | } 65 | 66 | --------------------------------------------------------------------------------