Lecture Website(Official): https://cs193p.sites.stanford.edu
Lecure Github(by me, including course resources, assignments , solutions) : https://github.com/azataiot/learn_cs193
Prerequisite Coureses:
- OOP
In standford, students have prerequisite to take at least three and recommended four, intro CS courses.
What this course dose not teach you:
- OOP basics, which you have to learn elsewhere. Though I’m assuming if you can get through the course and understand it and do projects on your own even though if it takes time then you already know OOP
- Keychain
- Programmatically write your UI, Autolayout.
- Push notifications
- How to write protocol oriented.
- How to use generics
- How to write your own delegates and completionHanlders and callbacks…
- How to handle race conditions
- How to usage instruments
- TDD
- Provisioning profile, Entitlements, Code signing
- Automation, fastlane, Jenkins, CI/CD
- Localization
- Advanced Git hooks and resolving git conflicts
Lecture 1: Getting Started with SwiftUI
The first lecture jumps right into building the first application of the quarter: a card-matching game called Memorize. It will be the foundation for the first few weeks of course material.
Watch Video
What will be build during the course?
Download and install Xcode
(Skipped)
Creating the Xcode project
There is two ways to create a project:
- By cliking
Create a new Xcode Project
- By Cloning an existing project source code from any repository
We will use the first method:
-
- Do not select Use Core Data as it is not used for now.
- Do not select Inlcude Tests as it is not used for now.
Default Code that generated from the XCode
Running the iOS App
There is two ways to run the iOS app:
- Using the Genral iOS simulator
- Using the New Automatic App Preview Tool
Preview and Inspect Elements (Coded Views)
Xcode provides very easy wat to inspect and debug UI elements. we can select the like in editor window and it will also open the element inspector in the right panel, also highlights the element in the middle previow window. (vise versa if we select the element in the preview window, code line which contains the UI will be highlighted in the editor)
Those three window (Editor window, Preview window, Inspector window) are linked, and we can change the content of the UI either on the inspector by clicking or on the editor by coding.
Preview Window
We can make more than one preview , for example one for light and another one for dark.
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
ContentView()
.preferredColorScheme(.dark)
}
}
Now we have two different preview, one for light theme and another one for the dark theme.
Lego Analogy
Almost every element on the screen, as elements are legos, we build small legos and use them to build bigger and comples legos.
When we use Legos to build something, all of our legos, even they have different shape or different color, they should, at least, looks like a Lego (as View in swfit code), cause only when they as a Lego, then we are able to combine them together, and plug in-out them with each other.
Rounded Rectangle
struct ContentView: View {
var body: some View {
return RoundedRectangle(cornerRadius: 20)
.stroke(lineWidth: 3)
.padding()
}
}
Adding color from the inspector
ZStack
ZStack is a view builder. We can think the ZStack as an image layer on the Photoshop.
ZStack is used to place items on top of each other.
In our design, we have the card, it can be easily defined as a rounded rectangle with stroked frame, and it has some emoji text in the middle. so we need to stack them on top of each other.
struct ContentView: View {
var body: some View {
ZStack{
RoundedRectangle(cornerRadius: 20)
.stroke(lineWidth: 3)
.padding()
.foregroundColor(.orange)
Text("some text")
.foregroundColor(.orange)
.padding()
}
}
}
Finishing code of the lecture 1:
import SwiftUI
struct ContentView: View {
var body: some View {
ZStack{
RoundedRectangle(cornerRadius: 20)
.stroke(lineWidth: 3)
Text("some text")
}
.padding(.horizontal)
.foregroundColor(.orange)
}
}
Lecture 2: Learning More about SwiftUI
Development on Memorize continues. Creating reusable components (a Card in the game) and combining them to make more complex user-interfaces.
Watch Video
HStack
import SwiftUI
struct ContentView: View {
var body: some View {
HStack{
ZStack{
RoundedRectangle(cornerRadius: 20)
.stroke(lineWidth: 3)
Text("some text")
}
ZStack{
RoundedRectangle(cornerRadius: 20)
.stroke(lineWidth: 3)
Text("some text")
}
ZStack{
RoundedRectangle(cornerRadius: 20)
.stroke(lineWidth: 3)
Text("some text")
}
}
.padding(.horizontal)
.foregroundColor(.orange)
}
}
HStack or Horizontal Stack is used to stack items Horizontally.
import SwiftUI
struct ContentView: View {
var body: some View {
HStack{
CardView()
CardView()
CardView()
CardView()
}
.padding(.horizontal)
.foregroundColor(.orange)
}
}
struct CardView : View {
var body: some View {
ZStack{
RoundedRectangle(cornerRadius: 20)
.stroke(lineWidth: 3)
Text("✈️")
.font(.largeTitle)
}
}
}
import SwiftUI
struct ContentView: View {
var body: some View {
HStack{
CardView()
CardView()
CardView()
CardView()
}
.padding(.horizontal)
.foregroundColor(.red)
}
}
struct CardView : View {
var body: some View {
ZStack{
RoundedRectangle(cornerRadius: 20)
.fill()
.foregroundColor(.white)
RoundedRectangle(cornerRadius: 20)
.stroke(lineWidth: 3)
Text("✈️")
.font(.largeTitle)
}
}
}
FaceUp and FaceDown
In Swift you can not have vairables that dose not have a value. In swift , all variables should have values.
struct ContentView: View {
var body: some View {
HStack{
CardView()
CardView(isFaceUp: false)
CardView(isFaceUp: false)
CardView()
}
.padding(.horizontal)
.foregroundColor(.red)
}
}
struct CardView : View {
var isFaceUp:Bool = true
var body: some View {
ZStack {
if isFaceUp{
RoundedRectangle(cornerRadius: 20)
.fill()
.foregroundColor(.white)
RoundedRectangle(cornerRadius: 20)
.stroke(lineWidth: 3)
Text("✈️")
.font(.largeTitle)
} else {
RoundedRectangle(cornerRadius: 20)
.fill()
}
}
}
}
We can also reuse code by introducing local variables
struct CardView : View {
var isFaceUp:Bool = true
var body: some View {
ZStack {
let shape = RoundedRectangle(cornerRadius: 20)
if isFaceUp{
shape.fill().foregroundColor(.white)
shape.stroke(lineWidth: 3)
Text("✈️").font(.largeTitle)
} else {
shape.fill()
}
}
}
}
Change State of the Card
Because all UI is immutable, UI changes by repainting the entire UI, and replacing the old views. So if we cant to change something inside the view, the best way is to use the logic (controller) to do that, or we can temporarily use a not recommended way, that is to use @State, pointing the variable to somewhere outside state.
struct CardView : View {
@State var isFaceUp:Bool = true
var body: some View {
ZStack {
let shape = RoundedRectangle(cornerRadius: 20)
if isFaceUp{
shape.fill().foregroundColor(.white)
shape.stroke(lineWidth: 3)
Text("✈️").font(.largeTitle)
} else {
shape.fill()
}
}
onTapGesture {
isFaceUp = !isFaceUp
}
}
}
Remmeber, state is only used for temporarily vairables.
To preview the interaction, we need to use the realmode instead of the select mode of the previewer
Dynamically assigning the Text content:
struct ContentView: View {
var body: some View {
HStack{
CardView(content: "🚗")
CardView(content: "🚐")
CardView(content: "🚑")
CardView(content: "🚔")
}
.padding(.horizontal)
.foregroundColor(.red)
}
}
struct CardView : View {
var content:String
@State var isFaceUp:Bool = true
var body: some View {
ZStack {
let shape = RoundedRectangle(cornerRadius: 20)
if isFaceUp{
shape.fill().foregroundColor(.white)
shape.stroke(lineWidth: 3)
Text(content).font(.largeTitle)
} else {
shape.fill()
}
}
.onTapGesture {
isFaceUp = !isFaceUp
}
}
}
struct ContentView: View {
var emojis = ["🚗","🚐","🚑","🚔"]
var body: some View {
HStack{
CardView(content: emojis[0])
CardView(content: emojis[1])
CardView(content: emojis[2])
CardView(content: emojis[3])
}
.padding(.horizontal)
.foregroundColor(.red)
}
}
ForEach
struct ContentView: View {
var emojis = ["🚗","🚐","🚑","🚔","🚇","🚔"]
var body: some View {
HStack{
ForEach(emojis,id: \.self, content: { emoji in
CardView(content: emoji)
})
}
.padding(.horizontal)
.foregroundColor(.red)
}
}
Error : Cause we used the string itself as the key to the ForEach, and we have the same String twice, it will broke it. For now, we just need to make sure that we have different kind of strings there.
Adding and Removing Cards and VStack
To add or remvove cards, we need our emojis provided to teh HStack can not be a fixed number of string.
struct ContentView: View {
var emojis = ["🚗","🚕","🚙","🚌","🚎","🏎","🚓","🚑","🚒","🚐","🛻","🚚","🚛","🚜","🦯","🦽","🦼","🩼","🛴","🚲","🛵","🏍","🛺","🛞"]
var body: some View {
HStack{
ForEach(emojis[0..<6],id: \.self, content: { emoji in
CardView(content: emoji)
})
}
.padding(.horizontal)
.foregroundColor(.red)
}
}
VStack
struct ContentView: View {
var emojis = ["🚗","🚕","🚙","🚌","🚎","🏎","🚓","🚑","🚒","🚐","🛻","🚚","🚛","🚜","🦯","🦽","🦼","🩼","🛴","🚲","🛵","🏍","🛺","🛞"]
var emojiCount = 6
var body: some View {
VStack{
HStack{
ForEach(emojis[0..<emojiCount ],id: \.self, content: { emoji in
CardView(content: emoji)
})
}
HStack{
Button(action: {}, label: {
VStack{
Text("Remove")
Text("Card")
}
})
Spacer()
Button(action: {}, label: {
VStack{
Text("Add")
Text("Card")
}
})
}
.padding(.horizontal)
}
.padding(.horizontal)
.foregroundColor(.red)
}
}
Or for better readability:
struct ContentView: View {
var emojis = ["🚗","🚕","🚙","🚌","🚎","🏎","🚓","🚑","🚒","🚐","🛻","🚚","🚛","🚜","🦯","🦽","🦼","🩼","🛴","🚲","🛵","🏍","🛺","🛞"]
var emojiCount = 6
var body: some View {
VStack{
HStack{
ForEach(emojis[0..<emojiCount ],id: \.self, content: { emoji in
CardView(content: emoji)
})
}
HStack{
remove
Spacer()
add
}
.padding(.horizontal)
}
.padding(.horizontal)
.foregroundColor(.red)
}
var remove:some View {
Button(action: {}, label: {
VStack{
Text("Remove")
Text("Card")
}
})
}
var add:some View {
Button(action: {}, label: {
VStack{
Text("Add")
Text("Card")
}
})
}
}
Or even using image icons (here we are using the system icons provided by the SFSymbols)
struct ContentView: View {
var emojis = ["🚗","🚕","🚙","🚌","🚎","🏎","🚓","🚑","🚒","🚐","🛻","🚚","🚛","🚜","🦯","🦽","🦼","🩼","🛴","🚲","🛵","🏍","🛺","🛞"]
var emojiCount = 6
var body: some View {
VStack{
HStack{
ForEach(emojis[0..<emojiCount ],id: \.self, content: { emoji in
CardView(content: emoji)
})
}
Spacer()
HStack{
remove
Spacer()
add
}
.padding(.horizontal)
.font(.largeTitle)
}
.padding(.horizontal)
.foregroundColor(.red)
}
var remove:some View {
Button(action: {}, label: {
Image(systemName: "minus.circle")
})
}
var add:some View {
Button(action: {}, label: {
Image(systemName: "plus.circle")
})
}
}
and make the font larger:
Add actions to the buttons
struct ContentView: View {
var emojis = ["🚗","🚕","🚙","🚌","🚎","🏎","🚓","🚑","🚒","🚐","🛻","🚚","🚛","🚜","🦯","🦽","🦼","🩼","🛴","🚲","🛵","🏍","🛺","🛞"]
@State var emojiCount = 6
var body: some View {
VStack{
HStack{
ForEach(emojis[0..<emojiCount ],id: \.self, content: { emoji in
CardView(content: emoji)
})
}
Spacer()
HStack{
remove
Spacer()
add
}
.padding(.horizontal)
.font(.largeTitle)
}
.padding(.horizontal)
.foregroundColor(.red)
}
var remove:some View {
Button(action: {
if emojiCount > 1 {
emojiCount -= 1
}
}, label: {
Image(systemName: "minus.circle")
})
}
var add:some View {
Button(action: {
if emojiCount < emojis.count {
emojiCount += 1
}
}, label: {
Image(systemName: "plus.circle")
})
}
}
LazyVGrid
struct ContentView: View {
var emojis = ["🚗","🚕","🚙","🚌","🚎","🏎","🚓","🚑","🚒","🚐","🛻","🚚","🚛","🚜","🦯","🦽","🦼","🩼","🛴","🚲","🛵","🏍","🛺","🛞"]
@State var emojiCount = 6
var body: some View {
VStack{
LazyVGrid(columns: [GridItem(),GridItem(),GridItem()]){
ForEach(emojis[0..<emojiCount ],id: \.self, content: { emoji in
CardView(content: emoji)
})
}
.foregroundColor(.red)
Spacer()
HStack{
remove
Spacer()
add
}
.padding(.horizontal)
.font(.largeTitle)
}
.padding(.horizontal)
}
var remove:some View {
Button(action: {
if emojiCount > 1 {
emojiCount -= 1
}
}, label: {
Image(systemName: "minus.circle")
})
}
var add:some View {
Button(action: {
if emojiCount < emojis.count {
emojiCount += 1
}
}, label: {
Image(systemName: "plus.circle")
})
}
}
Can even fix the width
Note:
HStack uses all the space Vertically.
VGridStack uses all the horizontal space but uses as small as possible vertical space.
With Aspect Ration:
ScrollView
struct ContentView: View {
var emojis = ["🚗","🚕","🚙","🚌","🚎","🏎","🚓","🚑","🚒","🚐","🛻","🚚","🚛","🚜","🦯","🦽","🦼","🩼","🛴","🚲","🛵","🏍","🛺","🛞"]
@State var emojiCount = 6
var body: some View {
VStack{
ScrollView{
LazyVGrid(columns: [GridItem(),GridItem(),GridItem()]){
ForEach(emojis[0..<emojiCount ],id: \.self, content: { emoji in
CardView(content: emoji).aspectRatio(2/3, contentMode: .fit)
})
}
}
.foregroundColor(.red)
Spacer()
HStack{
remove
Spacer()
add
}
.padding(.horizontal)
.font(.largeTitle)
}
.padding(.horizontal)
}
var remove:some View {
Button(action: {
if emojiCount > 1 {
emojiCount -= 1
}
}, label: {
Image(systemName: "minus.circle")
})
}
var add:some View {
Button(action: {
if emojiCount < emojis.count {
emojiCount += 1
}
}, label: {
Image(systemName: "plus.circle")
})
}
}
Adaptive GridItem
Fix the lanscap view
Problem:
Fix:
struct ContentView: View {
var emojis = ["🚗","🚕","🚙","🚌","🚎","🏎","🚓","🚑","🚒","🚐","🛻","🚚","🚛","🚜","🦯","🦽","🦼","🩼","🛴","🚲","🛵","🏍","🛺","🛞"]
@State var emojiCount = 6
var body: some View {
VStack{
ScrollView{
LazyVGrid(columns: [GridItem(.adaptive(minimum: 64))]){
ForEach(emojis[0..<emojiCount ],id: \.self, content: { emoji in
CardView(content: emoji).aspectRatio(2/3, contentMode: .fit)
})
}
}
.foregroundColor(.red)
Spacer()
HStack{
remove
Spacer()
add
}
.padding(.horizontal)
.font(.largeTitle)
}
.padding(.horizontal)
}
var remove:some View {
Button(action: {
if emojiCount > 1 {
emojiCount -= 1
}
}, label: {
Image(systemName: "minus.circle")
})
}
var add:some View {
Button(action: {
if emojiCount < emojis.count {
emojiCount += 1
}
}, label: {
Image(systemName: "plus.circle")
})
}
}
Reading Assignment 1
A Swift Tour
The Basics
Basic Operations
Programming Assignment 1
import SwiftUI
struct ContentView: View {
static let emojisCar = ["🚗", "🚕", "🚙", "🚌", "🚎", "🏎", "🚓", "🚑", "🚒", "🚐", "🛻", "🚚", "🚛", "🚜", "🦯", "🦽", "🦼", "🩼", "🛴", "🚲", "🛵", "🏍", "🛺", "🛞"]
static let emojiFood = ["🌭", "🌮", "🌯", "🍏", "🍎", "🍐", "🍊", "🍋", "🍌", "🍉", "🍇", "🍓", "🫐"]
static let emojiHeart = ["❤️", "🧡", "💛", "💚", "💙", "💜", "🖤", "🤍", "🤎", "💔", "❤️🔥", "❤️🩹", "❣️"]
// Task 5 card number is a variable in range 7..<12
@State var emojiCount = 12
@State var emojis = emojisCar
@State var themeColor: Color = .red
var body: some View {
VStack {
HStack {
// Task 3. Add title Memorize!
Text("Memorize!")
.font(.largeTitle)
}
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 64))]) {
ForEach(emojis[0..<emojiCount], id: \.self, content: { emoji in
CardView(content: emoji).aspectRatio(2 / 3, contentMode: .fit)
})
}
}
.foregroundColor(themeColor)
Spacer()
HStack {
// Task 4. Theme choosing buttons
Button(action: {
// Task 6. array content is shuffled.
emojis = ContentView.emojisCar.shuffled()
themeColor = .red
emojiCount = Int.random(in: 7..<12)
}, label: {
VStack {
Image(systemName: "car").font(.largeTitle)
Text("Vehicles")
}
})
.foregroundColor(.red)
Spacer()
// Task 7 & 8. Button including an image with SF Symbol and Text label
Button(action: {
emojis = ContentView.emojiFood.shuffled()
themeColor = .orange
emojiCount = Int.random(in: 7..<12)
}, label: {
VStack {
Image(systemName: "fork.knife").font(.largeTitle)
Text("Food")
}
})
.foregroundColor(.orange)
Spacer()
Button(action: {
emojis = ContentView.emojiHeart.shuffled()
themeColor = .green
emojiCount = Int.random(in: 7..<12)
}, label: {
VStack {
Image(systemName: "heart").font(.largeTitle)
Text("Heart")
}
})
.foregroundColor(.green)
}
.padding(.horizontal)
// Task 9 using smaller font
.font(.title3)
}
.padding()
}
// UI works in both portrait and landscape mode
struct CardView: View {
var content: String
@State var isFaceUp: Bool = true
var body: some View {
ZStack {
let shape = RoundedRectangle(cornerRadius: 20)
if isFaceUp {
shape.fill().foregroundColor(.white)
shape.strokeBorder(lineWidth: 3)
Text(content).font(.largeTitle)
} else {
shape.fill()
}
}
.onTapGesture {
isFaceUp = !isFaceUp
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.previewInterfaceOrientation(.portrait)
ContentView()
.preferredColorScheme(.dark)
}
}
}
Lecture3: MVVM
Conceptual overview of the architectural paradigm underlying the development of applications for iOS using SwiftUI (known as MVVM) and an explanation of a fundamental component of understanding the Swift programming language: its type system. Then both of these are applied to the Memorize application started in the first two lectures.
Watch Video
MVVM Design Paradigm
Common Software Degisn Paradigm
MVVM
- MVVM is a way of organizing architecture paradigm.
- Must be adhered to for SwitUI to work
Model (State + Logic)
Notices changes ‘’->
- UI independet
- Data + Logic (what your app is and dose)
- Only source of
the Truth
ViewModel (Interactivity )
Publishes “something changed” – - >
Modifies Model - ->
- Binds View to Model
- Interpreter
- GateKeeper
View (Presence)
Automatically observes publications, pull data and rebuilt. ->
Calls intent function “->
- Reflects the Model
- Stateless
- Declared (How, how they look like)
Swift Type System
struct and class
Similarity
-
both can have stored
vars
-
both can have
computed functions
-
both can have
constants
-
both can have
functions
-
both can have external label and internal label for functions
func multiply(_ operand: Int, by otherOperand: Int) -> Int { return operand * otherOperand } // multiply(5, by:6)
-
both can have
initializers
, and both can have multiple init function
Differences
- Structs
- value type
- copied when passed or assigned
- copy on write (only copied when modified)
- the heart of Functional programming
- NO inheritance
- “Free”
init
initializes ALLvars
- Mutability must be explicitly stated
- much more proveable
- Your “go to ” data structure
- Use case:
Model
UI Views
(UI Elements)
- Classes
- reference type
- passed around via
pointers
- memory cleaned up on swift using automatically reference counting
- if no one points to that class address it will be removed
- heart of Object Oriented Programming
- Inheritance (swift support single inheritance )
- “Free”
init
initializes NOvars
- Always mutable
- Used in specific circumstances
- Use case:
ViewModel
is always a class
protocol
“Don’t care type” aka generics
Cause swift is strongly typed, we need to give types for everything in swift, and for some var we can give generic type and will finally define the var as a basic type.
Example of Generics:
-
Array
struct Array<Element> { ... func append(_ element:Element) {...} }
We do not care the type of Element, but array works for Element Type
enum
functions (are types in swift)
In functional programming, functions should be a first class type.
Closures
It’s common to pass functrions around that we are vey often inlining
them, those ‘inlie’ functions are called clousers.
MVVM and Types in Action
- We are going to use special
init
function ( in both our Model and our ViewModel) - Use generics in our implementation of our Model
- use a function as a type in our Model
- see a class for the first time (our ViewModel will be a class)
- implement an “Intent” in our MVVM
- make UI “reactive” through our MVVM design
Creating the Game Model
Cause our model is UI independet, so will hide the canvas(preview windows) for now.
We will add new swift file to the project y:
select the folder and the file name for the model to create the file.
This will create the model file
import Foundation
Cause model is UI independet, so we import Foundation instead of SwiftUI.
Foundation
includes all basic swift types and other important structs and functions.,
struct MemoryGame {
// Memorize Game's state and controller.
// in our game, we need a bounch of cards
var cards : Array<Card>
func choose(card: Card) {
}
}
cause our card in the choose is so obvious, we can just remove the argument label.
func choose(_ card: Card) {
}
We will create another struct inside our MemoryGame struct, that represents our single Card. as MemoryGame
‘s Card
.
struct Card {
var isFaceUp: Bool
var isMatched: Bool
var content: String
}
Our content in this time is Emoji, that is String, but we want our game UI imdependent, and we readlly dont care the content is a String or an Image, so we are really dont care, this time we use the type “dont care”
struct MemoryGame {
// Memorize Game's state and controller.
// in our game, we need a bounch of cards
var cards : Array<Card>
func choose(_ card: Card) {
}
struct Card {
var isFaceUp: Bool
var isMatched: Bool
var content: CardContent
}
}
Remember that if we use dont care
type, we have to announce to the world that our model uses this by
struct MemoryGame<CardContent> {
// Memorize Game's state and controller.
// in our game, we need a bounch of cards
var cards : Array<Card>
func choose(_ card: Card) {
}
struct Card {
var isFaceUp: Bool
var isMatched: Bool
var content: CardContent
}
}
Whoever later uses this model, have to provide the CardContent
and fill out this dont care
Creating the ViewModel
import SwiftUI
We import SwiftUI here, cause it uses both model and UI.
var model: MemoryGame<String>
// making our model private makes our ViewModel(VM) as a gatekeeper
private var model: MemoryGame<String>
We can also use private(set)
to guard it, it makes the var read-only.
prvate
is fully private.
class EmojiMemoryGame {
// our ViewModel is going to create it's own model that will specifically to the UI.
// making our model private makes our VM as a gatekeeper
private var model: MemoryGame<String>
var cards: Array<MemoryGame<String>.Card> {
return model.cards
}
}
in calss all the vars have to have some value or we need to init them.
So we need to initalize it;s unset vars in our Model.
struct MemoryGame<CardContent> {
// Memorize Game's state and controller.
// in our game, we need a bounch of cards
var cards : Array<Card>
func choose(_ card: Card) {
}
init(numberOfPairsOfCards: Int){
cards = Array<Card>()
// add numberOfPairsOfCards x2 cards to cards array.
}
struct Card {
var isFaceUp: Bool
var isMatched: Bool
var content: CardContent
}
}
struct MemoryGame<CardContent> {
// Memorize Game's state and controller.
// in our game, we need a bounch of cards
var cards : Array<Card>
func choose(_ card: Card) {
}
init(numberOfPairsOfCards: Int){
cards = Array<Card>()
// add numberOfPairsOfCards x2 cards to cards array.
for pairIndex in 0..<numberOfPairsOfCards {
cards.append(Card())
cards.append(Card())
}
}
struct Card {
var isFaceUp: Bool = false
var isMatched: Bool = false
var content: CardContent
}
}
We are going to create a function and pass that to it, the function should later be implemented by the thing that whoever uses our model.
struct MemoryGame<CardContent> {
// Memorize Game's state and controller.
// in our game, we need a bounch of cards
var cards : Array<Card>
func choose(_ card: Card) {
}
init(numberOfPairsOfCards: Int, createCardContent: (Int)->CardContent){
cards = Array<Card>()
// add numberOfPairsOfCards x2 cards to cards array.
for pairIndex in 0..<numberOfPairsOfCards {
let content: CardContent = createCardContent(pairIndex)
cards.append(Card(content: content))
cards.append(Card(content: content))
}
}
struct Card {
var isFaceUp: Bool = false
var isMatched: Bool = false
var content: CardContent
}
}
Now we have to imlemenmt that func in our VM.
func makeCardContent(index: Int) -> String {
return "😀"
}
class EmojiMemoryGame {
// our ViewModel is going to create it's own model that will specifically to the UI.
// making our model private makes our VM as a gatekeeper
private var model: MemoryGame<String> = MemoryGame<String>(numberOfPairsOfCards: 6,createCardContent: makeCardContent)
var cards: Array<MemoryGame<String>.Card> {
return model.cards
}
}
We can also use clousure to make it simple:
class EmojiMemoryGame {
// our ViewModel is going to create it's own model that will specifically to the UI.
// making our model private makes our VM as a gatekeeper
private var model: MemoryGame<String> = MemoryGame<String>(numberOfPairsOfCards: 6,createCardContent: {(index: Int) -> String in
return "😀"
})
var cards: Array<MemoryGame<String>.Card> {
return model.cards
}
}
in
is used to seperate the result from arguments
We can also go better:
class EmojiMemoryGame {
// our ViewModel is going to create it's own model that will specifically to the UI.
// making our model private makes our VM as a gatekeeper
private var model: MemoryGame<String> = MemoryGame<String>(numberOfPairsOfCards: 6,createCardContent: { index in "😀" })
var cards: Array<MemoryGame<String>.Card> {
return model.cards
}
}
And even further cause it is the last argument of the function and it takes a function as it’s :
class EmojiMemoryGame {
// our ViewModel is going to create it's own model that will specifically to the UI.
// making our model private makes our VM as a gatekeeper
private var model: MemoryGame<String> = MemoryGame<String>(numberOfPairsOfCards: 6) { index in "😀" }
var cards: Array<MemoryGame<String>.Card> {
return model.cards
}
}
or even because we are not using index
in our func body:
private var model: MemoryGame<String> = MemoryGame<String>(numberOfPairsOfCards: 6) { _ in "😀" }
class EmojiMemoryGame {
var emojis = ["🚗","🚕","🚙","🚌","🚎","🏎","🚓","🚑","🚒","🚐","🛻","🚚","🚛","🚜","🦯","🦽","🦼","🩼","🛴","🚲","🛵","🏍","🛺","🛞"]
// our ViewModel is going to create it's own model that will specifically to the UI.
// making our model private makes our VM as a gatekeeper
private var model: MemoryGame<String> = MemoryGame<String>(numberOfPairsOfCards: 4) { pairIndex in "😀"
emojis[pairIndex]
}
var cards: Array<MemoryGame<String>.Card> {
return model.cards
}
}
Property initializer run before the class itself (self). So at the time property initialized, the emojis var still might not been loaded to the memory.
to solve this, we can do several ways of trips:
- write our own
init
func and initialize them in the right order - take the
emojis
outside class and make it global.- cause the global thing loaded first emojis always available
- but global variable is not good, it might pollute the name space
- we an also use them essentially global but name spaced using
static
- static means they are essentially global but just namespaced (prefixed) with the class name.
class EmojiMemoryGame {
static let emojis = ["🚗","🚕","🚙","🚌","🚎","🏎","🚓","🚑","🚒","🚐","🛻","🚚","🚛","🚜","🦯","🦽","🦼","🩼","🛴","🚲","🛵","🏍","🛺","🛞"]
// our ViewModel is going to create it's own model that will specifically to the UI.
// making our model private makes our VM as a gatekeeper
private var model: MemoryGame<String> = MemoryGame<String>(numberOfPairsOfCards: 4) { pairIndex in
emojis[pairIndex]
}
var cards: Array<MemoryGame<String>.Card> {
return model.cards
}
}
class EmojiMemoryGame {
// our ViewModel is going to create it's own model that will specifically to the UI.
// making our model private makes our VM as a gatekeeper
static let emojis = ["🚗","🚕","🚙","🚌","🚎","🏎","🚓","🚑","🚒","🚐","🛻","🚚","🚛","🚜","🦯","🦽","🦼","🩼","🛴","🚲","🛵","🏍","🛺","🛞"]
static func createMemoryGame() -> MemoryGame<String> {
MemoryGame<String>(numberOfPairsOfCards: 4) { pairIndex in
EmojiMemoryGame.emojis[pairIndex]
}
}
private var model: MemoryGame<String> = EmojiMemoryGame.createMemoryGame()
var cards: Array<MemoryGame<String>.Card> {
return model.cards
}
}
Or we can make it simpler:
class EmojiMemoryGame {
// our ViewModel is going to create it's own model that will specifically to the UI.
// making our model private makes our VM as a gatekeeper
static let emojis = ["🚗","🚕","🚙","🚌","🚎","🏎","🚓","🚑","🚒","🚐","🛻","🚚","🚛","🚜","🦯","🦽","🦼","🩼","🛴","🚲","🛵","🏍","🛺","🛞"]
static func createMemoryGame() -> MemoryGame<String> {
MemoryGame<String>(numberOfPairsOfCards: 4) { pairIndex in
emojis[pairIndex]
}
}
private var model: MemoryGame<String> = createMemoryGame()
var cards: Array<MemoryGame<String>.Card> {
return model.cards
}
}
Because we are already in the same class scope, we can remove the EmojiMemoryGame
Lecture 4: More MVVM enum Optionlas
The MVVM architecture is fully applied to Memorize. The important Swift concepts of enums and Optionals are covered and used to finish off the game logic of the Memorize game.
Watch Video
Enum and Optional
Our content needs to see the model, so we need to add the model to the View.
We decalare a var as viewModel
to the View:
struct ContentView: View {
var viewModel: EmojiMemoryGame
var emojis = ["🚗", "🚕", "🚙", "🚌", "🚎", "🏎", "🚓", "🚑", "🚒", "🚐", "🛻", "🚚", "🚛", "🚜", "🦯", "🦽", "🦼", "🩼", "🛴", "🚲", "🛵", "🏍", "🛺", "🛞"]
@State var emojiCount = 20
var body: some View {
VStack {
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 64))]) {
ForEach(emojis[0..<emojiCount], id: \.self) { emoji in
CardView(content: emoji).aspectRatio(2 / 3, contentMode: .fit)
}
}
}
.foregroundColor(.red)
}
.padding(.horizontal)
}
}
And we will pass the viewModel
to the in our app:
struct MemorizeApp: App {
// this is a class, the game is a pointer to the class, we not change the class, we change the content of the class.
// so game is let.
let game = EmojiMemoryGame()
var body: some Scene {
WindowGroup {
ContentView(viewModel:game)
}
}
}
Generic struct 'ForEach' requires that 'MemoryGame<String>.Card' conform to 'Hashable'
struct Card: Identifiable {
var id: ObjectIdentifier
var isFaceUp: Bool = false
var isMatched: Bool = false
var content: CardContent
}
init(numberOfPairsOfCards: Int, createCardContent: (Int) -> CardContent) {
cards = Array<Card>()
// add numberOfPairsOfCards x2 cards to cards array.
for pairIndex in 0..<numberOfPairsOfCards {
let content: CardContent = createCardContent(pairIndex)
cards.append(Card(id: pairIndex * 2, content: content))
cards.append(Card(id: pairIndex * 2 + 1, content: content))
}
}
Choose Function Implementation
If we want to change a struct member, we have to change it directly, cause it will copy a new struct every time we reference it.
mutating func choose(_ card: Card) {
// all arguments to functions are lets
let chosenIndex = index(of: card)
cards[chosenIndex].isFaceUp.toggle()
print(cards)
}
class EmojiMemoryGame: ObservableObject {
// our ViewModel is going to create it's own model that will specifically to the UI.
// making our model private makes our VM as a gatekeeper
static let emojis = ["🚗", "🚕", "🚙", "🚌", "🚎", "🏎", "🚓", "🚑", "🚒", "🚐", "🛻", "🚚", "🚛", "🚜", "🦯", "🦽", "🦼", "🩼", "🛴", "🚲", "🛵", "🏍", "🛺", "🛞"]
static func createMemoryGame() -> MemoryGame<String> {
MemoryGame<String>(numberOfPairsOfCards: 4) { pairIndex in
emojis[pairIndex]
}
}
private var model: MemoryGame<String> = createMemoryGame()
var cards: Array<MemoryGame<String>.Card> {
model.cards
}
// MARK: - Intent(s)
func choose(_ card: MemoryGame<String>.Card) {
// simply call our models choose. this can be a database operation or a fetch api data operation etc.
objectWillChange.send()
model.choose(card)
}
}
using Optionals for index:
struct MemoryGame<CardContent> {
// Memorize Game's state and controller.
// in our game, we need a bunch of cards
private(set) var cards: Array<Card>
mutating func choose(_ card: Card) {
// all arguments to functions are lets
if let chosenIndex = index(of: card) {
cards[chosenIndex].isFaceUp.toggle()
}
print(cards)
}
func index(of card: Card) -> Int? {
for index in 0..<cards.count {
if cards[index].id == card.id {
return index
}
}
return nil
}
init(numberOfPairsOfCards: Int, createCardContent: (Int) -> CardContent) {
cards = Array<Card>()
// add numberOfPairsOfCards x2 cards to cards array.
for pairIndex in 0..<numberOfPairsOfCards {
let content: CardContent = createCardContent(pairIndex)
cards.append(Card(id: pairIndex * 2, content: content))
cards.append(Card(id: pairIndex * 2 + 1, content: content))
}
}
struct Card: Identifiable {
var id: Int
var isFaceUp: Bool = true
var isMatched: Bool = false
var content: CardContent
}
}
or using the builtin function:
mutating func choose(_ card: Card) {
// all arguments to functions are lets
if let chosenIndex = cards.firstIndex(where: { aCardInTheCardsArray in aCardInTheCardsArray.id == card.id }) {
cards[chosenIndex].isFaceUp.toggle()
}
print(cards)
}
or more simplier :
mutating func choose(_ card: Card) {
// all arguments to functions are lets
if let chosenIndex = cards.firstIndex(where: { $0.id == card.id }) {
cards[chosenIndex].isFaceUp.toggle()
}
print(cards)
}
Interactivity
struct CardView: View {
let card: MemoryGame<String>.Card
var body: some View {
ZStack {
let shape = RoundedRectangle(cornerRadius: 20)
if card.isFaceUp {
shape.fill().foregroundColor(.white)
shape.strokeBorder(lineWidth: 3)
Text(card.content).font(.largeTitle)
} else if card.isMatched {
shape.opacity(0)
} else {
shape.fill()
}
}
}
}
struct ContentView: View {
@ObservedObject var viewModel: EmojiMemoryGame
var body: some View {
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 65))]) {
ForEach(viewModel.cards) { card in
CardView(card: card)
.aspectRatio(2 / 3, contentMode: .fit)
.onTapGesture {
viewModel.choose(card)
}
}
}
}
.foregroundColor(.red)
.padding(.horizontal)
}
}
mutating func choose(_ card: Card) {
// all arguments to functions are lets
if let chosenIndex = cards.firstIndex(where: { $0.id == card.id }),
!cards[chosenIndex].isFaceUp,
!cards[chosenIndex].isMatched {
if let potentialMatchIndex = indexOfTheOnlyFaceUpCard {
if cards[chosenIndex].content == cards[potentialMatchIndex].content {
cards[chosenIndex].isMatched = true
cards[potentialMatchIndex].isMatched = true
}
indexOfTheOnlyFaceUpCard = nil
} else {
for index in cards.indices {
cards[index].isFaceUp = false
}
indexOfTheOnlyFaceUpCard = chosenIndex
}
cards[chosenIndex].isFaceUp.toggle()
}
print(cards)
}
Reading Assignment 2
Programming Assignment 2
//
// EmojiMemoryGame.swift
// Memorize
//
// Created by Azat Yaakov on 5.09.2022.
//
// noteL:https://www.youtube.com/watch?v=jlnnxMkdlAk
import SwiftUI
class EmojiMemoryGame: ObservableObject {
init() {
theme = EmojiMemoryGame.themes.randomElement()! // Required Task 11
theme.emojis.shuffle()
model = EmojiMemoryGame.createMemoryGame(theme: theme)
}
// our ViewModel is going to create it's own model that will specifically to the UI.
// making our model private makes our VM as a gatekeeper
// 2.8 support at least 6 different themes.
static private let vehicleEmojis = ["🚗", "🛴", "✈️", "🛵", "⛵️", "🚎", "🚐", "🚛", "🛻", "🏎", "🚂", "🚊", "🚀", "🚁", "🚢", "🛶", "🛥", "🚞", "🚟", "🚃"]
static private let animalEmojis = ["🐶", "🐱", "🐭", "🐹", "🐰", "🦊", "🐻", "🐼", "🐻❄️", "🐨", "🐯", "🦁", "🐮", "🐷", "🐵"]
static private let foodEmojis = ["🍔", "🥐", "🍕", "🥗", "🥟", "🍣", "🍪", "🍚", "🍝", "🥙", "🍭", "🍤", "🥞", "🍦", "🍛", "🍗"]
static private let heartEmojis = ["❤️", "🧡", "💛", "💚", "💙", "💜"]
static private let sportsEmojis = ["⚽️", "🏀", "🏈", "⚾️", "🎾", "🏉", "🥏", "🏐", "🎱", "🏓", "🏸", "🏒", "🥊", "🚴♂️", "🏊", "🧗♀️", "🤺", "🏇", "🏋️♀️", "⛸", "⛷", "🏄", "🤼"]
static private let weatherEmojis = ["☀️", "🌪", "☁️", "☔️", "❄️"]
static private let colors = ["black", "gray", "red", "green", "blue", "orange", "yellow", "pink", "purple", "fuchsia", "beige", "gold"]
static func createTheme(_ title: String, _ emojis: [String]) -> Theme {
// just pick a random color
let colorIndex = Int.random(in: 0..<colors.count)
let randomColor = colors[colorIndex]
// just show random pairs of cards
let randomNumberOfPairsOfCards = Int.random(in: 1..<emojis.count)
return Theme(title: title, emojis: emojis, numberOfPairsOfCards: randomNumberOfPairsOfCards, color: randomColor)
}
static var themes: Array<Theme> = [
createTheme("Vehicles", vehicleEmojis),
createTheme("Animals", animalEmojis),
createTheme("Food", foodEmojis),
createTheme("Hearts", heartEmojis),
createTheme("Sports", sportsEmojis),
createTheme("Weather", weatherEmojis),
]
static func createMemoryGame(theme: Theme) -> MemoryGame<String> {
MemoryGame<String>(numberOfPairsOfCards: theme.numberOfPairsOfCards) { pairIndex in
theme.emojis[pairIndex]
}
}
@Published private var model: MemoryGame<String>
private var theme: Theme
var cards: Array<MemoryGame<String>.Card> {
model.cards
}
// for UI
var themeName: String {
theme.title
}
var themeColor: Color {
switch theme.color {
case "black":
return .black
case "gray":
return .gray
case "red":
return .red
case "green":
return .green
case "blue":
return .blue
case "orange":
return .orange
case "yellow":
return .yellow
case "pink":
return .pink
case "purple":
return .purple
default:
return .red
}
}
// MARK: - Intent(s)
func choose(_ card: MemoryGame<String>.Card) {
// simply call our models choose. this can be a database operation or a fetch api data operation etc.
objectWillChange.send()
model.choose(card)
}
func newGame() {
theme = EmojiMemoryGame.themes.randomElement()! // Required Task 11
theme.emojis.shuffle()
model = EmojiMemoryGame.createMemoryGame(theme: theme)
}
}
//
// Created by Azat Yaakov on 21.09.2022.
//
import Foundation
// Add the formal concept of theme.
struct Theme {
var title: String
var emojis: Array<String>
var numberOfPairsOfCards: Int
var color: String
}
Lecture 5: Properties Layout @ViewBuilder
Explore property observers, computed properties, @State and @ViewBuilder. The mechanisms behind how Views are layed out on screen are examined followed by a demo which chooses a better font for each card in Memorize depending on the space available. Along the way, apply better access control to Memorize’s internal API.
Watch Video
Using Calculated Properties
//
// MemoryGame.swift
// Memorize
// Model of The Game
// Created by Azat Yaakov on 5.09.2022.
//
import Foundation
struct MemoryGame<CardContent> where CardContent: Equatable {
// Memorize Game's state and controller.
// in our game, we need a bunch of cards
private(set) var cards: Array<Card>
private var indexOfTheOnlyFaceUpCard: Int?{
get { cards.indices.filter({cards[$0].isFaceUp}).oneAndOnly }
set { cards.indices.forEach { cards[$0].isFaceUp = ($0 == newValue) }}
// all other cards except the new opened one face down
}
// this func chooose can not be private because we want to expose it
// internal is the default and it means we can use it anywhere inside the app.
internal mutating func choose(_ card: Card) {
// all arguments to functions are lets
if let chosenIndex = cards.firstIndex(where: { $0.id == card.id }),
!cards[chosenIndex].isFaceUp,
!cards[chosenIndex].isMatched {
if let potentialMatchIndex = indexOfTheOnlyFaceUpCard {
if cards[chosenIndex].content == cards[potentialMatchIndex].content {
cards[chosenIndex].isMatched = true
cards[potentialMatchIndex].isMatched = true
}
cards[chosenIndex].isFaceUp = true
} else {
indexOfTheOnlyFaceUpCard = chosenIndex
}
}
}
init(numberOfPairsOfCards: Int, createCardContent: (Int) -> CardContent) {
cards = []
// add numberOfPairsOfCards x2 cards to cards array.
for pairIndex in 0..<numberOfPairsOfCards {
let content: CardContent = createCardContent(pairIndex)
cards.append(Card(id: pairIndex * 2, content: content))
cards.append(Card(id: pairIndex * 2 + 1, content: content))
}
}
struct Card: Identifiable {
var id: Int
var isFaceUp = false
var isMatched = false
var content: CardContent
}
}
extension Array {
// Array's dont care is called Element
var oneAndOnly:Element? { // we want one and only, if only return it else return nil
if self.count == 1 {
return self.first
} else {
return nil
}
}
}
Refactoring the ViewModel
// This is the main Game UI
// EmojiMemoryGameView.swift
// Memorize
//
// Created by Azat Yaakov on 2.09.2022.
//
import SwiftUI
/// Entry point of the APP UI
struct EmojiMemoryGameView: View {
@ObservedObject var game: EmojiMemoryGame
var body: some View {
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 65))]) {
ForEach(game.cards) { card in
CardView(card: card)
.aspectRatio(2 / 3, contentMode: .fit)
.onTapGesture {
game.choose(card)
}
}
}
}
.foregroundColor(.red)
.padding(.horizontal)
}
}
struct CardView: View {
let card: EmojiMemoryGame.Card
var body: some View {
ZStack {
let shape = RoundedRectangle(cornerRadius: 20)
if card.isFaceUp {
shape.fill().foregroundColor(.white)
shape.strokeBorder(lineWidth: 3)
Text(card.content).font(.largeTitle)
} else if card.isMatched {
shape.opacity(0)
} else {
shape.fill()
}
}
}
}
struct EmojiMemoryGameView_Previews: PreviewProvider {
static var previews: some View {
let game = EmojiMemoryGame()
EmojiMemoryGameView(game: game)
.previewInterfaceOrientation(.portrait)
EmojiMemoryGameView(game: game)
.preferredColorScheme(.dark)
}
}
Using Typealias
//
// EmojiMemoryGame.swift
// Memorize
// ViewModel of the Memorize App
// Created by Azat Yaakov on 5.09.2022.
//
import SwiftUI
class EmojiMemoryGame: ObservableObject {
// our ViewModel is going to create it's own model that will specifically to the UI.
// making our model private makes our VM as a gatekeeper
typealias Card = MemoryGame<String>.Card
private static let emojis = ["🚗", "🚕", "🚙", "🚌", "🚎", "🏎", "🚓", "🚑", "🚒", "🚐", "🛻", "🚚", "🚛", "🚜", "🦯", "🦽", "🦼", "🩼", "🛴", "🚲", "🛵", "🏍", "🛺", "🛞"]
private static func createMemoryGame() -> MemoryGame<String> {
MemoryGame<String>(numberOfPairsOfCards: 9) { pairIndex in
emojis[pairIndex]
}
}
private var model = createMemoryGame()
var cards: Array<Card> {
model.cards
}
// MARK: - Intent(s)
func choose(_ card: Card) {
// simply call our models choose. this can be a database operation or a fetch api data operation etc.
objectWillChange.send()
model.choose(card)
}
}
Property Observers
Layout
Custom Views
Make Emoji Sizes Larger
struct CardView: View {
let card: EmojiMemoryGame.Card
var body: some View {
GeometryReader(content: { geometry in
ZStack {
let shape = RoundedRectangle(cornerRadius: DrawingConstants.cornerRadius)
if card.isFaceUp {
shape.fill().foregroundColor(.white)
shape.strokeBorder(lineWidth: DrawingConstants.lineWidth)
Text(card.content).font(font(in: geometry.size))
} else if card.isMatched {
shape.opacity(0)
} else {
shape.fill()
}
}
})
}
private func font(in size:CGSize) -> Font {
Font.system(size: min(size.width, size.height) * DrawingConstants.fontScale )
}
private struct DrawingConstants {
static let cornerRadius: CGFloat = 20
static let lineWidth:CGFloat = 3
static let fontScale:CGFloat = 0.8
}
}
struct EmojiMemoryGameView_Previews: PreviewProvider {
static var previews: some View {
let game = EmojiMemoryGame()
EmojiMemoryGameView(game: game)
.previewInterfaceOrientation(.portrait)
EmojiMemoryGameView(game: game)
.preferredColorScheme(.dark)
}
}
Lecture 6: Protocol Shapes
Discussion of what is perhaps the most important type in Swift: a protocol. The demo combines the concepts of generics and protocols to make the cards better use the space available on screen. Finally the Shape protocol is explained and the pie-shaped countdown timer is added to Memorize (but not yet animated).
Watch Video
Contents
Protocol
Used for :
-
A type
-
is to specify the behaviour
-
Turning dont care to we are a little bit
-
restrict an extension
-
restrict functions
Reading Assignment 3
Programming Assignment 3
Lecture 7: ViewModifier Animation
The ViewModifier protocol is explained and then used to make it possible to turn any View into a Memorize card by “cardify-ing” it. The lecture then moves on to an in-depth look at animation and starts a comprehensive multi-lecture demonstration of animation by using implicit animations to make the emoji on a Memorize card spin around when it is matched.
Watch Video
Lecture 8: Animation Demo
The demonstration of animation continues by showing how to animate the shuffling, dealing and flipping of cards along with the cards’ appearance and disappearance. The pie-shaped countdown timer added in a previous lecture is also animated.
Watch Video
Programming Assignment 4
L7+L8 Demo Code
Lecture 9: EmojiArt Drag/Drop
New demo application: EmojiArt. Lots covered here, including enum, extensions, tuples, Drag and Drop, colors and images, and more. The Grand Central Dispatch (GCD) API is explained in preparation for a demo of multithreading in the next lecture.
Note: GCD has been mostly replaced by Swift’s new built-in async API as of WWDC 2021.
Watch Video
Lecture 10: Gestures
After demonstrating how to use GCD to keep the downloading of the background image from the internet from blocking the responsiveness of the UI, multitouch gestures are added to zoom in on and pan around in our EmojiArt document.
Note: GCD has been mostly replaced by Swift’s new built-in async API as of WWDC 2021.
Watch Video
Programming Assignment 5
L9+L10 Demo Code
Lecture 11: Persistence Error Handling
A number of persistence topics (UserDefaults, the file system, Codable archiving, JSON) as well as how errors are handled in Swift. Make changes to an EmojiArt document persist and introduce a new ViewModel to EmojiArt called PaletteStore.
Watch Video
Lecture 12: Binding Sheet navigation EditMode
The details about numerous property wrappers, including @State, @ObservedObject, @Binding, @Environment, @EnvironmentObject and @StateObject. Demo of many new SwiftUI elements, including TextField, Form, NavigationView, List, sheet, popover, Alert, EditMode and more. Enhance EmojiArt’s palettes of emoji.
Watch Video
Programming Assignment 6
L11+L12 Demo Code
Lecture 13: Publisher More Persistence
The Publisher protocol is used to implement a cleaner version of EmojiArt’s background downloading code. CloudKit and CoreData are briefly explained (but not demoed). See the bonus lecture from 2020 below (Enroute, part 2) for a demo of CoreData.
Watch Video
Lecture 14: Document Architecture
Demonstration of using SwiftUI’s Document Architecture to turn EmojiArt into a multi-document application. Includes discussion of the App and Scene protocols, WindowGroup, DocumentGroup, @SceneStorage, @ScaledMetric, and more. Along the way, we add Undo/Redo to EmojiArt.
Watch Video
L13+L14 Demo Code
Lecture 15: UIKit Integration
Get EmojiArt working on iPhone. Includes some more toolbar work as well as understanding how to integrate UIKit functionality into a SwiftUI application.
Watch Video
Lecture 16: Multiplatform (macOS)
Turn EmojiArt into a multi-platform application (i.e. both iOS and macOS). Demonstrates a variety of ways to share code across platforms.
Watch Video
L15+L16 Demo Code
Enroute Picker Coddle REST API
The first of two bonus lectures from 2020 covers Picker and creates a new demonstration application (Enroute) which pulls data from a REST API on the internet using the Codable mechanism shown earlier in the course.
Watch Video
Demo (as shown in lecture)
Demo (updated for Xcode 12)
Core Data
The second of two lectures from 2020 which use the Enroute demonstration application. Adds a CoreData database to Enroute to store historical flight data.