TIL(Today I Learned)
11월 21일(일)
학습내용
- SwiftUI(CS193p Lecture 4)
-
아래의 EmojiMemoryGame 타입은 class로 정의 되어있다. class 이므로 game은 포인터이며, let이므로 이 포인터가 지닌 주소값이 변경되지 않는다. 따라서 객체 내부의 프로퍼티 값은 변할 수 있다. 만약 struct이라면 game 변수의 값 자체가 변경되지 않음을 의미할 것이다. 이 경우 game내의 프로퍼티가 변할 수 없다.
let game = EmojiMemoryGame()
- Identifiable 프로토콜을 채택하면 id라는 프로퍼티를 정의해야한다.
-
private + computed property vs private(set)
// private + computed property private var model: MemoryGame<String> = createMemoryGame() var cards: Array<MemoryGame<String>.Card> { model.cards } // private(set) private(set) var model: MemoryGame<String> = createMemoryGame()
- private + computed property은 cards를 접근하기 위해서 model을 드러내지 않는다는 장점이 있다. private(set)은 computed property을 정의하지 않아도 된다는 장점이 있다.
- CardView의 프로퍼티
- CardView는 카드 내용 한개만 표시하므로 cards가 아닌 card 하나만 프로퍼티로 전달해주어야 한다. 뷰가 필요한 최소한의 것만 전달해주는 것이 좋다. (cards 전달 X, ViewModel 전달 X)
-
또한 View는 read-only이므로 전달받은 데이터를 let으로 해주는게 좋다.(var X)
struct CardView: View { let card: MemoryGame<String>.Card ... }
- User Intent는 ViewModel에 구현한다.
- User Intent는 사용자의 입력
- ViewModel은 ObservableObject를 채택해야 View에게 값의 변화를 알려줄 수 있다.
- 수동으로 값을 알려주기 위해서는
var objectWillChange: ObservableObjectPublisher
을 정의하고 objectWillChange.send()을 호출함으로써 변화를 publish할 수 있다. - 하지만 @Published를 이용하면 이를 자동으로 가능하게 해준다.
-
또한 ViewModel을 참조하는 View에서는 @ObservedObject라고 프로퍼티 앞에 붙여주어야 한다.
class EmojiMemoryGame: ObservableObject { @Published private var model: MemoryGame<String> = createMemoryGame() ... } struct ContentView: View { @ObservedObject var viewModel: EmojiMemoryGame }
- model이 변화하면 ViewModel이 publish하고, View는 그때마다 재생성된다.
- 수동으로 값을 알려주기 위해서는
- Enum
- It can only have discrete states
- Value Type (값 복사해서 전달)
- case마다 associated data를 가질 수 있다.
case .hamburger(let pattyCount)
- switch로 각각의 case을 구분해서 분기할 수 있고, default로 나머지 case를 분기 받을 수 있다.
- 연산 프로퍼티를 정의할 수 있지만, 저장프로퍼티는 불가능, 또한 메서드도 정의할 수 있다.
- CaseIterable을 채택하면 allCases 라는 static 변수가 생성되는데, 이 변수를 이용해 모든 케이스를 순회할 수 있다.
-
GridItem도 아래와 같이 enum을 이용한다.
struct GridItem { enum Size { case adaptive(minimum: CGFloat, maximum: CGFloat = .infinity) case fixed(CGFloat) case flexible(minimum: CGFloat = 10, maximum: CGFloat = .infinity) } }
- Optionals
-
Optional도 enum이다. 두가지 상태 is set(some), 또는 not set(none)이 가능하다.
enum Optional<T> { case none case some(T) // T 타입의 associated Value를 갖는 것이다. }
-
Optional은 값의 “not set”, “unspecified “, “undetermined “을 위해서 사용한다.
var hello: String? var hello: Optional<String> = .none var hello: String? = "hello" var hello: Optional<String> = .some("hello") var hello: String? = nil var hello: Optional<String> = .none
if let
을 이용하여 안전하게 언래핑할 수 있다. (!
, exclamination point은 크래쉬가 날 위험이 있다.)??
을 이용하여 nil일 경우의 기본 값을 정할수 있다. Optional defaulting이라고 하며??
을 nil-coalescing operator라고 한다.- Optional Chaining
let y = x?.foo()?.bar?.z
중간에 nil이 있으면 y도 nil이 할당
-
- Tip
- 타입에서 중요하지 않은 프로퍼티는 맨 아래쪽에 둔다.
- Preview 하단의 pin을 활성화하면 탭을 이동해도 preview가 사라지지 않고 창에 고정된다.
-
cards(카드 배열)의 indices 프로퍼티를 이용해서 0…<cards.count를 대체하여 순회할 수 있다.
~~for index in 0...<cards.count~~ for index in cards.indices
- ViewModel이 모델(프로퍼티) 변화를 감지 할 수 있는 이유는 Swift가 struct의 변화를 감지할 수 있기 때문이다. 하지만 프로퍼티가 class일 경우는 불가능하다. (영상 1:27:30 부근)
-
문제점/고민한점
- 왜 SwiftUI는 ForEach의 파라미터는 Idetifiable을 해야하는가?
해결방법
- View를 id로 구분하기 위하여.
- 왜 View를 id로 구분해야하지?
- 특정 값에 해당하는 View를 찾아내기 위해
- 만약 같은 값을 View로 나타낼 경우 어떤 View를 변화시킬건지 구분할 수 가 없게됨
- 값의 index를 뷰의 index와 비교해서 구분할 수도 있잖아?
- 만약에 collection에 값의 추가와 삭제가 동시에 여러번 일어났는데 이때는 그럼 index를 어떻게 판단할 건데?
- 그렇다고 index가 바뀐 것만 뷰에 알려줄수 있는 것도 아니 잖아? 뷰는 read only이니깐.
- 그래서 id를 통해 해당 값의 id에 해당하는 뷰를 수정하는거지.
- 값의 index를 뷰의 index와 비교해서 구분할 수도 있잖아?
- 왜 View를 id로 구분해야하지?
참고
- Working with Identifiable items in SwiftUI: https://www.hackingwithswift.com/books/ios-swiftui/working-with-identifiable-items-in-swiftui