Stanford cs193p MVVM
This article is related notes for the 3rd episode of Stanford University's CS193p course.
cs193p class:
The lectures for the Spring 2023 version of Stanford University's course CS193p (Developing Applications for iOS using SwiftUI) were given in person but, unfortunately, were not video recorded. However, we did capture the laptop screen of the presentations and demos as well as the associated audio. You can watch these screen captures using the links below. You'll also find links to supporting material that was distributed to students during the quarter (homework, demo code, etc.).
cs193p class website: https://cs193p.sites.stanford.edu/2023
Today's class outline
MVVM
- Design paradigm
Swift Type System
struct
class
protocol
(part 1)- "don't care" type (aka
generics
) enum
functions
Back to demo!
- Apply MVVM to Memorize
Model and UI
Separating "Logic and Data" from "UI"
- SwiftUI is very serious about the separation of application logic & data from the UI
We call this logic and data our "Model" - It could be a
struct
or an SQL database or some machine learning code or many other things Or any combination of such things - The UI is basically just a "parametrizable" shell that Model feeds and brings to life
Think of the UI as a visual manifestation of the Model - The model is where things like
isFaceUp
andcardCount
would live (not in@State
in the UI) - SwiftUI takes care of making sure that UI gets rebuilt when a Model change affects the UI
- SwiftUI is very serious about the separation of application logic & data from the UI
Connecting the Model to the UI
There are some choices about how to connect the Model to the UI ...
- Rarely, the Model could just be an
@State
in a View (this is very minimal to no separation) - The Model might only be accessible via a gatekeeper "View Model"
class
(full separation) - There is a View Model
class
, but the Model is still directly accessible (partial separation)
- Rarely, the Model could just be an
Mostly this choice depends on the complexity of the Model ...
- A Model that is made up of SQL +
struct
(s) + something else will likely opt for #2 - A Model that is just a simple piece of data and little to no logic would likely opt for #1
- Something in-between might be interested in option #3
- A Model that is made up of SQL +
- We're going to talk now about #2 (full separation)
We call this architecture that connects the Model to the UI in this way MVVM.
Model-View-ViewModel - This is the primary architecture for any resonably complex SwiftUI application.
You'll also quickly see how #3 (partial seperation is just a minor tweak to MVVM)
MVVM
Varienties of Types
struct and class
Both
struct
andclass
have ...- ... pretty much exactly the same syntax.
stored vars (the kind you are used to, i.e., stored in memory)
var isFaceUp: Bool
computed vars (i.e. those whose value is the result of evaluating some code)
var body: some View { return Text("Hello World") }
constant lets (i.e. vars whose values never change)
let defaultColor = Color.orange ... CardView().foregroundColor(defaultColor)
functions
func multiply(operand: Int, by: Int) -> Int { return operand * by } multiply(operand: 5, by: 6)
func multiply(_ operand: Int, by otherOperand: Int) -> Int { return operand * otherOperand } multiply(5, by: 6)
initializers (i.e. special functions that are called when creating a
struct
orclass
)struct RoundedRectangle { init(cornerRadius: CGFloat) { // initialize this rectangle with that cornerRadius } init(cornerSize: CGSize) { // initialize this rectangle with that cornerSize } }
Difference between struct and class
struct | class |
---|---|
Value type | Reference type |
Copied when passed or assigned | Passed around via pointers |
Copy on write (not actually copied unless being modified) | Automatically reference counted |
Functional programming | Object-oriented Programming |
No inheritance | Inheritance (single) |
"Free" init initializes ALL vars | "Free" init initializes NO vars |
Mutability is explicit (var vs let) | Always mutable |
Your "go to" data structure | Used in specific circumstances |
Generics
Sometimes we just don't care
- We may want to manipulate data structures that we are "type agnostic" about. In another words, we don't know what type something is and we don't care.
- But Swift is a strongly-typed language, so we don't use variables and such that are "untyped".
- So how do we specify the type of something when we don't care what type it is?
- We use a "don't care" type (we call this feature "generics") ...
Example of a user of a "don't care" type:
Array
- An
Array
contains a bunch of things an it doesn't care at all what type they are! - But inside Array's code, it has to have variables for the things it contains. They need types. And it needs types for the arguments to
Array
functions that do things like adding items to it.
- An
How
Array
uses a "don't care" typeArray's declaration looks something like this ...
struct Array<Element> { ... func append(_ element: Element) { ... } }
- The type of the argument to append is Element. A "don't care" type.
- Array's implementation of append knows nothing about that argument and it does not care.
- Elemtent is not any known
struct
orclass
orprotocol
, It's just a placeholder for a type. The code for using an Array looks something like this ...
var a = Array<Int>() a.append(5) a.append(22)
- When someone uses Array, that's when Element gets determined (by
Array<Int>
)
- It is perfectly legal to have mutiple "don't care" types in the above (e.g.
<Element, Foo>
) - The actual name of "don't care" is Type Parameter.
- Other languages most of you may know (e.g. Java) have a similar feature. However, Swift combines this with
protocols
to take it all to the next level.
protocol (part 1)
- It has functions and vars, but no implementation (or storage)!
Declaring a protocol looks vary similar to
struct
orclass
(just w/o implementation) ...protocol Moveable { func move(by: Int) var hasMoved: Bool { get } var distanceFromStart: Int { get set } }
- The
{ }
on the vars just say whether it's read only or a var whose value can also be set. Any type can now claim to implement Moveable ...
struct PortableThing: Moveable { // must implement move(by:), hasMoved and distanceFromStart here }
- PortableThing now conforms to (aka "behaves like a") Moveable
... and this is also legal (this is called "protocol inheritance") ...
protocol Vehicle: Moveable { var passengerCount: Int { get set } } class Car: Vehicle { // must implement move(by:), hasMoved, distanceFromStart and passengerCount here }
... and you can claim to implement multiple protocols ...
class Car: Vehicle, Impoundable, Leasable { // must implement move(by:), hasMoved, distanceFromStart and passengerCount here // and must implement any funcs/vars in Impoundable and Leasable }
What is a protocol used for?
A protocol is a type.
- So (with certain restrictions), it can be used in the normal places you might see a type. (especially with the additional of the keyword
some
andany
) - For example, it can be the type of a var or a
return
type (like var body's return type). - We are not going to talk about that sort of use right now.
- So (with certain restrictions), it can be used in the normal places you might see a type. (especially with the additional of the keyword
Specifying the behavior of a struct, class or enum
struct ContentView: View
- Just by doing this,
ContentView
became a vary powerfulstruct
! Of course,ContentView
did have to implement var body to participate in being a View, but still. We call this process "constrains and gains".
- A
protocol
can constrain another type (for example, a View has to implement var body). - But a protocol can also supply huge gains (e.g. the 100's of functions a View gets for free).
- A
We'll see quite a variety of protocols in the comming weeks.
- Examples:
Identifiable
,Hashable
,Equatable
,CustomStringConvertible
. And more speciablized ones likeAnimatable
.
- Examples:
Anothe use we'll see is turning "don't cares" into "somewhat cares".
struct Array<Element> where Element: Equatable
- If Array were declared this way, then only things that are "equatable" could be put in Arrays.
- This is at the heart of "protocol-oriented programming".
- We'll be doing this in the demo today.
More about Protocols in part 2
- A protocol becomes massively more powerful via something called an extension
- We'll cover how to combine protocols and extensions in "part two" of protocols.
- This will explain how the "gains" part of "constrains and gains" is implemented.
- We'll also learn more about using protocols as types in the same ways we use any other type. And how the
some
andany
keywords help us do that.
Why protocols?
- It is a way for types (
struct
/classes
/other protocols) to say what they are capable of. And also for other code to demand certain behavior out of another type. - But neither side has to reveal what sort of
struct
orclass
they are. - It's also a way to add a lot of functionality (via extension) based on a protocol's primitives. This is what "functional (or protocol-oriented) programming" is all about.
- It's about formalizing how data structures in our application function.
- Even when we talk about vars in the context of
protocol
s, we don't define how they're stored. We focus on the functionality and hide the implementation details behind it.
- It is a way for types (
Functions as Types
You can declare a variable (or parameter to a func or whatever) to be of type "function". The syntax for this includes the types of the arguments and return value. You can do this anywhere any other type is allowed.
Examples:
(Int, Int) -> Bool // takes two Ints and return a Bool
(Double) -> Void // takes a Double and returns nothing
() -> Array<String> // takes no arguments and returns an Array of Strings
() -> Void // takes no argumentsand returns nothing (this is a common one)
All of the above are just types. No different than Bool
or View
or Array<Int>
. All are types.
var operation: (Double) -> Double
This is a var called operation
.
It's type is "function that takes a Double and returns a Double".
Here is a simple function that takes a Double and returns a Double ...
func square(operand: Double) -> Double {
return operand * operand
}
operation = square // just assigning a value to the operation vars, nothing more
let result1 = operation(4) // result1 would equal 16
Note that we don't use argument labels (e.g. operand:
) when executing function types.
operation = sqrt // sqrt is a built-in function which happens to take and return a Double
let result2 = operation(4) // result2 would be 2
We'll soon see an example of using a function type for a parameter to a function in our demo.
Closures
- It's so common to pass functions around that we are vary often "inlining" them.
- We call such an inlined function a "closure" and there's special language support for it. We've already using these a lot (
@ViewBuilders
are closures, so isonTapGesture
's action). We'll peel back the layers on this in the demo and again later in the quarter. - Remember that we are mostly doing "functional programming" in SwiftUI. As the vary name implies, "functions as types" is a vary important concept in Swift. Very.
Back to Demo
Since the demo is too short (only about ~15 minutes), I will merge the demo notes in this class to the next post (4th episode).
Comments are disabled.