일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- WidgetTree
- Masil
- colorofdays
- 새싹후기
- 프로젝트회고
- 청년취업사관학교후기
- IOS
- 스터디
- 오늘의 색상
- xml
- flutter
- ImageSlider
- 조건문
- 백준
- GIT
- UserDefault
- Swift
- MVVM
- 스위프트
- flutter #state # stateful #stateless
- process
- stanford
- 프로그래머스
- xcode
- UIKit
- 코딩테스트
- SwiftUI
- CS193p
- 알고리즘
- collectionView
- Today
- Total
개발을 시작하는 이야기
[SwiftUI] Lecture 5: Properties Layout @ViewBuilder 본문
강의 보기 : Youtube :: Stanford
@State, @Binding
@State는 단어 그대로 현재 상태를 나타내는 속성으로, 뷰의 어떤 값을 저장하는 데 사용된다. 현재 뷰 UI의 특정 상태를 저장하기 위해 만들어진 것이기 때문에 보통 Private로 지정하여 사용한다. @State 속성의 프로퍼티 값은 재 할당을 하더라도 변경되지 않는데 @Binding 변수를 통해서 변경이 가능하다. @Binding은 단어 그대로 구속력 있는, 묶여 있다는 뜻으로, @State 속성으로 선언된 프로퍼티와 묶여 있다고 생각하면 된다.
@State는 주가 되는 뷰에 선언을 해주고, 선언된 프로퍼티를 다른 뷰에서 사용하기 위해서는 @Binding을 사용한다. 그리고 사용 시에는 $를 앞에 붙여주어 Binding 변수임을 나타내 준다.
Struct ContentView: View {
@State private var test = 0
var body: some View {
BindView(test: $test)
}
}
Struct BindView: View {
@Binding var test: Int
var body: some View {
Stepper("test: \(test)", value: $test)
}
}
Property Observers
단어 그대로 프로퍼티 값의 변화를 관찰하고 이에 응답한다. 새로운 값이 현재의 값과 동일하더라도 속성의 값이 설정(Set) 될 때마다 호출된다. lazy 저장 프로퍼티를 제외하고, 정의된 저장 프로퍼티에 옵저버를 추가할 수 있다. 또한 하위 클래스 내의 프로퍼티를 재정의하여 상속된 프로퍼티 (저장 프로퍼티, 연산 프로퍼티 어느 것이든)에도 프로퍼티 옵저버를 추가할 수 있다. 프로퍼티 옵저버는 총 두 가지 옵션이 있는데, willSet과 didSet 이 있다.
willSet은 값이 저장되기 직전에 호출된다. newValue로 변경 후 값을 가져올 수 있다.
didSet은 값이 저장된 직후에 호출된다. oldValue로 변경 전 값을 가져올 수 있다.
var name: String = "Unknown" {
willSet {
print("현재이름 = \(name), 변경후 이름 = \(newValue)")
}
didSet {
print("현재 이름 = \(name), 변경전 이름 = \(oldValue)")
}
}
위에서 저장 프로퍼티에만 옵저버를 추가할 수 있다고 했지만 연산 프로퍼티에도 옵저버를 추가할 수 있다. 다만 조건이 있는데, 부모 클래스의 연산 프로퍼티를 오버라이딩 할 경우에 추가할 수 있다.
struct Person {
// 연산 프로퍼티는 이렇게 다른 저장 프로퍼티를 꼭 필요로 한다.
var name: String = "Unknown"
// 값을 저장하는 것이 아니므로, 타입 추론 불가. 따라서 타입 명시 필수!
var selfIntroduce: String {
// 접근자 getter (다른 프로퍼티의 값을 얻거나 연산하여 리턴할 때 사용)
// => 어떤 저장 프로퍼티의 값을 연산하여 반환할 것인지 return 구문 필수
get {
return "내 이름은 \(name)야??"
}
// 설정자 setter (다른 저장 프로퍼티에 값을 저장할 때 사용)
set {
self.name = "내 이름은 " + newValue + "야!!"
}
}
}
Layout
GeometryReader
View를 구성할때 VStack, HStack, ZStack들로만 적절히 섞어 사용해도 레이아웃을 구성할 수 있지만, 그 이상으로 하위 뷰들의 위치나 모양을 직접 설정해주어야 하는 경우가 있다. 그럴 때 사용해주는 Container View 가 바로 GeometryReader이다.
ChildView는 별 설정이 없다면 ParentView가 제안해준 위치를 사용한다. 하지만 ParentView가 제안한 설정이 마음에 들지 않는다면 ChildView가 자신의 위치, 모양 등을 설정할 수 있다. 이때, ChildView가 ParentView가 제안한 위치를 활용하여 재설정할 때 사용하는 것이 GeometryReader이다.
오늘은 기능을 더하기보다는 코드를 간결하게 작성하는 것에 주력했다.
Model
import Foundation
struct MemoryGame<CardContent> where CardContent: Equatable {
private(set) var cards: Array<Card>
private var indexOfTheOneAndIOnlyFaceUpCatd: Int? {
get { cards.indices.filter ({ cards[$0].isFaceUp }).oneAndOnly }
set { cards.indices.forEach { cards[$0].isFaceUp = ($0 == newValue) } }
}
mutating func choose(_ card: Card) {
if let chosenIndex = cards.firstIndex(where: { $0.id == card.id }),
!cards[chosenIndex].isFaceUp,
!cards[chosenIndex].isMatched
{
if let potentialMatchIndex = indexOfTheOneAndIOnlyFaceUpCatd {
if cards[chosenIndex].content == cards[potentialMatchIndex].content {
cards[chosenIndex].isMatched = true
cards[potentialMatchIndex].isMatched = true
}
cards[chosenIndex].isFaceUp = true
} else {
indexOfTheOneAndIOnlyFaceUpCatd = chosenIndex
}
}
}
init(numberOfPairsOfCards: Int, createCardContent: (Int) -> CardContent) {
cards = []
// add numberOfPairsOfCards x 2 cards to cards array
for pairIndex in 0..<numberOfPairsOfCards {
let content: CardContent = createCardContent(pairIndex)
cards.append(Card(content: content, id: pairIndex*2))
cards.append(Card(content: content, id: pairIndex*2+1))
}
}
struct Card: Identifiable {
var isFaceUp = false
var isMatched = false
let content: CardContent
let id: Int
}
}
extension Array {
var oneAndOnly: Element? {
if count == 1 {
return first
} else {
return nil
}
}
}
ViewModel
import SwiftUI
class EmojiMemoryGame: ObservableObject {
typealias Card = MemoryGame<String>.Card
private static let emojis = ["💡", "📋", "🖥", "😺", "🗺", "😱", "🙈", "🤔", "📪", "👨🏫", "📱", "🎉", "📄", "💁", "📞", "👨💻", "⚒", "🙋", "🤵♂️", "😀", "😃", "😄", "😁"]
private static func createMemoryGame() -> MemoryGame<String> {
MemoryGame<String>(numberOfPairsOfCards: 9) { pairIndex in
emojis[pairIndex]
}
}
@Published private var model: MemoryGame<String> = createMemoryGame()
var cards: Array<Card> {
model.cards
}
//MARK: - Intent(s)
func choose(_ card: Card) {
model.choose(card)
}
}
View
import SwiftUI
struct EmojiMemoryGameView: View {
@ObservedObject var game: EmojiMemoryGame
var body: some View {
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 60))]) {
ForEach(game.cards) { card in
CardView(card: card)
.aspectRatio(2/3, contentMode: .fit)
.onTapGesture {
game.choose(card)
}
}
}
}
.foregroundColor(/*@START_MENU_TOKEN@*/.red/*@END_MENU_TOKEN@*/)
.padding(.horizontal)
}
}
struct CardView: View {
let card: EmojiMemoryGame.Card
var body: some View {
GeometryReader { 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
}
}
commend키를 활용하여 Rename으로 해당 함수를 참조하는 모든 명령어를 한 번에 바꾸는 것은 무척 좋은 꿀팁이었다.
변수를 설정할 때 타입을 지정해주는 것이 코드를 보다 안정적으로 유지해주기 때문에 타입을 선언해주는 것이 좋다고 들었던 것 같은데 스탠퍼드 선생님은 반복할 필요가 없다고 대부분 쳐내버리고 꼭 필요한 몇 가지의 타입만 선언해주었다. 이거는 작업하는 코딩 컨벤션에 따라 달라지는 것 일수도 있겠지만 한번 찾아보고 정석적인 부분이 있다면 그쪽으로 맞춰 습관화를 들이는 게 좋겠다.
코드 단순화 작업을 하는걸 보고 있으니 부족한 점을 많이 느꼈다. 이 부분을 공부해서 출시 앱에 적용하는 것을 단기 목표 삼아 진행해봐야겠다.
참고자료 : Access Control, @state, @Binding, GeometryReader
'개발 이야기 > Swift' 카테고리의 다른 글
[SwiftUI] Lecture 6: Protocols Shapes (0) | 2022.04.18 |
---|---|
접근제어는 왜 사용해야 할까? (0) | 2022.04.15 |
[SwiftUI] Lecture 4: Memorize Game Logic (0) | 2022.04.14 |
[SwiftUI] Lecture 3: MVVM and the Swift type system (0) | 2022.04.12 |
[SwiftUI] Lecture 2: Learning more about SwiftUI (0) | 2022.04.10 |