Standford University CS193 Walkthrough

 

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:

  1. By cliking Create a new Xcode Project
  2. By Cloning an existing project source code from any repository

We will use the first method:

  1. CleanShot 2022-09-02 at 12.08.36
  2. CleanShot 2022-09-02 at 12.10.17
  3. CleanShot 2022-09-02 at 12.12.22
    1. Do not select Use Core Data as it is not used for now.
    2. Do not select Inlcude Tests as it is not used for now.
  4. CleanShot 2022-09-02 at 12.19.23
  5. CleanShot 2022-09-02 at 12.20.26

Default Code that generated from the XCode

image-20220902123046763

Running the iOS App

There is two ways to run the iOS app:

CleanShot 2022-09-02 at 12.22.13

  1. Using the Genral iOS simulator
  2. Using the New Automatic App Preview Tool

CleanShot 2022-09-02 at 12.22.13

Preview and Inspect Elements (Coded Views)

CleanShot 2022-09-02 at 12.38.38

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)

CleanShot 2022-09-02 at 12.41.24

CleanShot 2022-09-02 at 12.42.10

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.

image-20220902182821212

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

Minecraft Oyuncakları ve Hediyeleri

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()
    }
}

CleanShot 2022-09-02 at 18.59.33

Adding color from the inspector

CleanShot 2022-09-02 at 19.01.43

ZStack

ZStack is a view builder. We can think the ZStack as an image layer on the Photoshop.

How Photoshop Layers Work

Six squares of different colors, stacked atop each other, with a 10-point

ZStack is used to place items on top of each other.

CleanShot 2022-09-02 at 19.05.30

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()
        }
    }
}

CleanShot 2022-09-02 at 19.09.53

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)
    }
}

CleanShot 2022-09-02 at 19.51.02

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)
        }
    }
}

CleanShot 2022-09-02 at 19.57.01

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)
        }
    }
}

CleanShot 2022-09-02 at 20.00.48

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()
            }
        }
    }
    
}

CleanShot 2022-09-02 at 20.06.14

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
        }
    }
    
}

image-20220902201359110

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

CleanShot 2022-09-02 at 20.17.23

CleanShot 2022-09-02 at 20.16.39

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
        }
    }
    
}

CleanShot 2022-09-02 at 20.21.40

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)
    }
}

CleanShot 2022-09-02 at 20.30.40

image-20220902203230784

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.

CleanShot 2022-09-02 at 20.33.35

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)
    }
}

CleanShot 2022-09-02 at 20.41.23

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")
            }
        })
    }
}

CleanShot 2022-09-02 at 20.49.47

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:

image-20220902205505917

CleanShot 2022-09-02 at 20.56.21

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")
        })
    }
}

CleanShot 2022-09-02 at 20.59.00

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")
        })
    }
}

CleanShot 2022-09-02 at 23.18.43

Can even fix the width

image-20220902231922524

CleanShot 2022-09-02 at 23.19.46

Note:

HStack uses all the space Vertically.

VGridStack uses all the horizontal space but uses as small as possible vertical space.

With Aspect Ration:

image-20220902232611205

CleanShot 2022-09-02 at 23.26.22

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")
        })
    }
}

CleanShot 2022-09-02 at 23.28.17

Adaptive GridItem

Fix the lanscap view

Problem:

CleanShot 2022-09-02 at 23.31.40

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")
        })
    }
}

image-20220902233611552

CleanShot 2022-09-02 at 23.36.24

Reading Assignment 1

A Swift Tour

Link to the Post

The Basics

Link to the Post

Basic Operations

Link to the Post

Programming Assignment 1

CleanShot 2022-09-05 at 23.21.20

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

The Model—View—Controller(MVC) Pattern

The Model—View—Presenter(MVP) Pattern

The Model — View — ViewModel (MVVM) Pattern

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)

mvvm1

mvvm2

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

    image-20220905143540450

    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 ALL vars
    • 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 NO vars
    • 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.

CleanShot 2022-09-05 at 16.17.04

We will add new swift file to the project y:

CleanShot 2022-09-05 at 16.18.36

CleanShot 2022-09-05 at 16.19.35

CleanShot 2022-09-05 at 16.20.54

select the folder and the file name for the model to create the file.

This will create the model file

CleanShot 2022-09-05 at 16.22.24

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

CleanShot 2022-09-05 at 16.43.27

import SwiftUI

We import SwiftUI here, cause it uses both model and UI.

CleanShot 2022-09-05 at 16.46.55

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
    }
}

CleanShot 2022-09-05 at 16.51.46

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
    }
    
}

CleanShot 2022-09-05 at 16.58.25

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
    }
}

CleanShot 2022-09-05 at 17.08.42

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:

  1. write our own init func and initialize them in the right order
  2. 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
  3. 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
    }
}

image-20220905171800479

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))
        }
    }

simulator_screenshot_880E0B3A-7769-49B2-B76C-A66064EC288C

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

CleanShot 2022-09-22 at 19.48.14

//
//  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

CleanShot 2022-09-23 at 22.41.56

CleanShot 2022-09-23 at 22.42.22

CleanShot 2022-09-23 at 22.42.36

CleanShot 2022-09-23 at 22.43.07

CleanShot 2022-09-23 at 22.43.22

CleanShot 2022-09-23 at 22.43.43

CleanShot 2022-09-23 at 22.44.32

CleanShot 2022-09-23 at 22.45.04

CleanShot 2022-09-23 at 22.45.23

CleanShot 2022-09-23 at 22.46.07

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

CleanShot 2022-09-23 at 22.48.17

CleanShot 2022-09-23 at 22.49.03

CleanShot 2022-09-23 at 22.50.32

Layout

CleanShot 2022-09-23 at 22.51.09

CleanShot 2022-09-23 at 22.51.53

CleanShot 2022-09-23 at 22.52.34

CleanShot 2022-09-23 at 22.53.06

CleanShot 2022-09-23 at 22.53.54

CleanShot 2022-09-23 at 22.54.13

CleanShot 2022-09-23 at 22.54.43

CleanShot 2022-09-23 at 22.55.21

CleanShot 2022-09-23 at 22.55.40

CleanShot 2022-09-23 at 22.56.06

CleanShot 2022-09-23 at 22.57.39

CleanShot 2022-09-23 at 22.58.20

CleanShot 2022-09-23 at 22.58.39

CleanShot 2022-09-23 at 22.59.34

CleanShot 2022-09-23 at 23.00.26

CleanShot 2022-09-23 at 23.00.51

CleanShot 2022-09-23 at 23.01.19

CleanShot 2022-09-23 at 23.02.01

CleanShot 2022-09-23 at 23.02.42

CleanShot 2022-09-23 at 23.03.10

CleanShot 2022-09-23 at 23.03.41

CleanShot 2022-09-23 at 23.04.02

CleanShot 2022-09-23 at 23.05.20

CleanShot 2022-09-23 at 23.05.47

CleanShot 2022-09-23 at 23.06.04

Custom Views

CleanShot 2022-09-23 at 23.08.26

CleanShot 2022-09-23 at 23.08.54

CleanShot 2022-09-23 at 23.09.14

CleanShot 2022-09-23 at 23.09.31

CleanShot 2022-09-23 at 23.10.04

CleanShot 2022-09-23 at 23.11.11

CleanShot 2022-09-23 at 23.12.12

CleanShot 2022-09-23 at 23.12.38

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)
    }
}

CleanShot 2022-09-23 at 23.25.41

CleanShot 2022-09-23 at 23.26.03

CleanShot 2022-09-23 at 23.26.19

CleanShot 2022-09-23 at 23.26.39

CleanShot 2022-09-23 at 23.27.49

CleanShot 2022-09-23 at 23.28.11

CleanShot 2022-09-23 at 23.28.29

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

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.

Watch Video

Demo Code (as shown in lecture)

Demo Code (updated for Xcode 12)