About Swift
MVVM
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 a combination of such things
The UI is basically just a “parametrizable” shell that the Model feeds and brings to life
Think of the UI as a visual manifestation of the Model
The Model is where things like isFaceUp. And cardCount would live (not in @state in the UI)
SwiftUI takes care of making sure the UI gets rebuilt when a Model change affects the UI
Connecting the Model to the UI
There are some choices about how to connect the Model to the UI …
1. Rarely, the Model could just be an @State in a View (this is very minimal to no separation)
2. The Model might only be accessible via a gatekeeper “View Model” class (full separation)
3. There is a View Model class, but the Model is still directly accessible (partial separation)
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
We’re going to talk now about #2 (full separation).
We call this architecture that connects the Model to the UI in the way MVVM.
Model-View-ViewModel
This is the primary architecture for any reasonably complex SwiftUI application.
You’ll also quirky see how #3 (partial separation ) is just a minor tweak to MVVM.
Swift Type System
Struct and Class
Both struct and class have …
… pretty much exactly the same syntax.
stored vars (the kind you are used to, i.e., stored in memory)
computed vars (i.e. those whose value is the result of evaluating some code)
constant lets (I.e. vars whose values never change)
functions
아래는 외부 함수 파라미터명을 내부 함수 파라미터에서 변경할 수 있다는 내용이다.
func multiply(operand: Int, by: Int) -> Int {
return operand * by
}
multiply(operand: 5, by: 6)
func multiply(_ operand: Int, by otherOperand: Int) -> Int {
operand * otherOperand
}
multiply(5, by: 6)
아래와 같이 구조체와 클래스는 원하는 선택적으로 초기화 프로그램을 가질 수 있다.
struct roundedRectangle {
init(cornerRadius: CGFloat) {
//initialize this rectangle with that cornerradius
}
init(cornerSize: CGSize)
{
//initialize this rectangle with that cornerSize
}
}
위의 캡처는 구조체와 클래스의 차이를 분명히 나타내고 있다. 여기서 우리는 ViewModel 만 클래스로 사용할 예정이다. 이 ViewModel을 gate keeper 라고 하는데 View간의 공유할 자원을 지속적으로 연결하는 역활을 한다.
이제는 Generics 에 대해 알아봅시다.
Generics
제네릭을 사용하는 실질적인 에를 들면 Array를 들수 있는데 이는 구조체로 < > 안에 어떠한 유형이 오던 상관하지 않는다는 뜻입니다. 또한 제네릭은 프로토콜과 결합하여 강력해 질 것입니다.
Protocol
프로토콜은 단지 구현이 되지 않은 함수와 변수로 정의되어 있습니다. 변수는 값을 가지고만 올 수 있는 get과 쓰고 읽을 수 있는 get, set 으로 표현됩니다.
우리가 View 프로토콜을 채택했을 때 var body 부분을 구현했던 것 처럼 위의 에제인 PortableThing 구조체에서 Moveable 프로토콜을 채택했으면 정의되어진 함수 하나와 변수 2개를 구현해야 합니다. 이 때 PortableThing 은 Moveable 처럼 작동하게 됩니다.
프로토콜은 위의 예제와 같이 완전히 상속이 허용됩니다.
또한 위의 예제처럼 다중의 프로토콜을 가질 수 있습니다. 이 또한 모든 함수와 변수를 구현해야 합니다.
프로토콜은 유형 즉 타입입니다. 우리가 자주 보게 되는 View는 프로토콜 즉 타입입니다.
View 프로토콜은 “제한과 이득”이라는 특징이 있습니다. Identifiable, Hashable, Equatable 등의 프로토콜은 조만간 다룰 예정입니다.
프로토콜의 또 다른 용도는 “조금 신경쓰세요” 도 있습니다. == 은 Equatable이라는 Swift 프로토콜의 일부입니다. 이 때의 where은 조금만 신경쓰세요 라고 하면 되겠네요.
Swift에서는 함수를 타입으로 간주한다.
위의 함수 squre을 operation이라는 타입에 assign하여 값을 얻을 수 있다.
코드를 정리하고자 합니다. 먼저 카드가 추가되고 삭제되는 기능을 없애겠습니다. 아래와 같이 주석 처리하겠습니다.
// @State var cardCount: Int = 4
var body: some View {
// VStack {
ScrollView {
cards
// }
// Spacer()
// cardCountAdjusters
}
.padding()
}
var cards: some View {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 120))]) {
ForEach(emojis.indices, id: \.self) { index in
CardView(content: emojis[index])
.aspectRatio(2/3, contentMode: .fit)
}
}
.foregroundStyle(.orange)
}
/*
var cardCountAdjusters: some View {
HStack {
cardRemover
Spacer()
cardAdder
}
.imageScale(.large)
.font(.largeTitle)
}
func cardCountAdjuster(by offset: Int, symbol: String) -> some View {
Button(action: {
cardCount += offset
}, label: {
Image(systemName: symbol)
})
.disabled(cardCount + offset < 1 || cardCount + offset > emojis.count)
}
var cardRemover: some View {
cardCountAdjuster(by: -1, symbol: "rectangle.stack.badge.minus.fill")
}
var cardAdder: some View {
cardCountAdjuster(by: +1, symbol: "rectangle.stack.badge.plus.fill")
}
*/
이제 새로운 모델을 만들기 위해 새로만들기 -> 파일로 이동해서 파일을 추가합니다.
import Foundation
struct MemoryGame<CardContent> {
var cards: Array<Card>
func choose(card: Card) {
}
struct Card {
var isFaceUp: Bool
var isMatched: Bool
var content: CardContent
}
}
MemoryGame.swift 하는 이름의 모델을 만들었습니다. 다음은 아래와 같은 viewModel을 만듭니다. 새로운 파일을 만들고 emojiMemoryGame 이라는 View와 Model의 연결 통로를 하는 역활을 합니다.
import SwiftUI
class EmojiMemoryGame {
var model: MemoryGame<String>
}