[iOS App Dev Tutorials(3)] Navigation and modal presentation, Passing data

GraceKim·2023년 11월 21일
0

iOS-App-Dev-Tutorials

목록 보기
3/3
post-thumbnail

안녕하세요, GraceKim입니다! 🍎

이번에는 3차시로 Navigation and modal presentation과 Passing data 부분을 공부하여 정리해보려고 합니다. 주로 몰랐던 내용들만 위주로 기록하고자 하니, View를 단순히 스타일링을 위해 작성되는 부분은 조금 빠진 내용이 있을 수 있습니다.


Navigation and modal presenatation

Creating a navigation hierarchy

import SwiftUI


struct ScrumsView: View {
    let scrums: [DailyScrum]
  
    var body: some View {
        NavigationStack {
            List(scrums) { scrum in
                NavigationLink(destination: Text(scrum.title)) {
                    CardView(scrum: scrum)
                }
                .listRowBackground(scrum.theme.mainColor)
            }
            .navigationTitle("Daily Scrums")
            .toolbar {
                Button(action: {}) {
                    Image(systemName: "plus")
                }
                .accessibilityLabel("New Scrum")
            }
        }
    }
}


struct ScrumsView_Previews: PreviewProvider {
    static var previews: some View {
        ScrumsView(scrums: DailyScrum.sampleData)
    }
}

NavigationStack에 대하여 새로 다루었습니다.
NavigationStack은 SwiftUI에서 사용자의 탐색을 관리하는 뷰 스택을 나타내는 사용자 정의 뷰입니다.

import Foundation


struct DailyScrum: Identifiable {
    let id: UUID
    var title: String
    var attendees: [Attendee]
    var lengthInMinutes: Int
    var theme: Theme
    
    init(id: UUID = UUID(), title: String, attendees: [String], lengthInMinutes: Int, theme: Theme) {
        self.id = id
        self.title = title
        self.attendees = attendees.map { Attendee(name: $0) }
        self.lengthInMinutes = lengthInMinutes
        self.theme = theme
    }
}


extension DailyScrum {
    struct Attendee: Identifiable {
        let id: UUID
        var name: String
        
        init(id: UUID = UUID(), name: String) {
            self.id = id
            self.name = name
        }
    }
}


extension DailyScrum {
    static let sampleData: [DailyScrum] =
    [
        DailyScrum(title: "Design", attendees: ["Cathy", "Daisy", "Simon", "Jonathan"], lengthInMinutes: 10, theme: .yellow),
        DailyScrum(title: "App Dev", attendees: ["Katie", "Gray", "Euna", "Luis", "Darla"], lengthInMinutes: 5, theme: .orange),
        DailyScrum(title: "Web Dev", attendees: ["Chella", "Chris", "Christina", "Eden", "Karla", "Lindsey", "Aga", "Chad", "Jenn", "Sarah"], lengthInMinutes: 5, theme: .poppy)
    ]
}

기존에 attendees를 string 배열로 받았는데, 새로 Attendee를 만들어서 매핑하였습니다. 이는 Swift에서 값(value) 타입의 특성을 활용하고자 함입니다.
Attendee 구조체는 불변성(immutable)을 가지기 때문에, 참여자의 정보가 초기화된 후에는 변경되지 않습니다. 이는 예측 가능하고 안전한 코드를 작성하는 데 도움이 됩니다.
이는 코드의 유지 보수성을 높이고, 확장성을 갖추며, 타입 안정성을 확보하는 데 도움이 됩니다.

Managing data flow between views

이 부분은 특별히 데이터의 흐름을 관리하고 사용자 상호 작용에 대하여 앱 데이터의 현재 상태를 반영하는 방법에 대한 문서입니다. 정리해서 요약해보겠습니다.

데이터 단일 소스

  • 여러 복사본을 사용하면 데이터의 불일치 버그가 발생할 수 있습니다. 따라서 데이터의 일관성을 위해 앱의 각 데이터 요소에 대한 단일 소스를 사용하는 것이 좋습니다.
  • 동일한 정보를 다양한 뷰에서 사용할 때, 앱의 여러 부분에서 같은 부분을 표시하거나 수정하는 것에 있어서, 데이터를 사용하는 뷰가 동일하게 이 변경을 반영하고 동일한 정보를 주어야 합니다.

Swift Property Wrappers

  • 프로퍼티 래퍼(Property Wrappers)가 있어 일반적인 속성 초기화 패턴을 캡슐화하고 효율적으로 속성에 Action을 추가할 수 있는데, @State@Binding를 사용하면 Source of Truths를 유지하는 데에 도움이 됩니다.
    PropertyWrappers

@State(상태 속성)

  • @State로 선언된 속성은 앱 전체에 공유되는 데이터의 중심이 되는 부분을 뜻합니다.
  • 이 속성에 저장된 데이터는 View의 상태를 나타내며, View에 의존하는 다른 모든 구성 요소는 이 속성에 따라 업데이트됩니다.
  • 상호 작용 등으로 인해 @State 속성이 변경되면, 시스템은 자동으로 뷰를 다시 그려 업데이트합니다.

Binding&State

@Binding(바인딩)

  • @Binding은 기존에 앱 전체에 공유되는 데이터의 중심이 되는 부분(ex. @State)과 연결된 속성을 나타냅니다.
  • 데이터를 직접 저장하지 않고, 기존 데이터와 연결되어 있는 양방향 연결을 만듭니다.
  • @Binding은 여러 View 간에 데이터를 공유하고 변경 사항을 실시간으로 반영해줍니다.

Creating the edit view

import SwiftUI


struct DetailEditView: View {
    @State private var scrum = DailyScrum.emptyScrum


    var body: some View {
        Form {
            Section(header: Text("Meeting Info")) {
                TextField("Title", text: $scrum.title)
            }
        }
    }
}


struct DetailEditView_Previews: PreviewProvider {
    static var previews: some View {
        DetailEditView()
    }
}

방금 배운 내용을 적용한 것입니다. @State로 선언된 scrum은 View가 다시 그려질 때마다 계속 그 값을 유지하게 되고, 사용자에게 실시간으로 업데이트된 화면을 보여줄 수 있습니다.
즉, TextField("Title", text: $scrum.title)에 사용자가 값을 입력하면, @State의 속성인 scrum.title 속성에 바로 반영됩니다.

import Foundation


struct DailyScrum: Identifiable {
    let id: UUID
    var title: String
    var attendees: [Attendee]
    var lengthInMinutes: Int
    var lengthInMinutesAsDouble: Double {
        get {
            Double(lengthInMinutes)
        }
        set {
            lengthInMinutes = Int(newValue)
        }
    }
    var theme: Theme
    
    init(id: UUID = UUID(), title: String, attendees: [String], lengthInMinutes: Int, theme: Theme) {
        self.id = id
        self.title = title
        self.attendees = attendees.map { Attendee(name: $0) }
        self.lengthInMinutes = lengthInMinutes
        self.theme = theme
    }
}

/* 코드 생략 */

lengthInMinutesAsDouble은 슬라이더가 값을 변경하면 정수로 변환하여 속성을 업데이트 하기 위해 setter을 적용하였습니다.

보통 get()과 set()을 사용하는 경우는 다음과 같습니다.

  1. 다른 타입으로 값을 변환하여 사용해야할 때, Getter을 통해 변환된 값을 얻을 수 있습니다.
  2. Setter를 사용하여 값을 설정할 때, 특정 조건을 적용하거나 가공된 값을 프로퍼티에 할당할 수 있습니다.
  3. 일부 속성은 다른 속성들로부터 계산되어야 할 때, Getter와 Setter를 사용하여 필요한 계산을 수행할 수 있습니다.
import SwiftUI


struct DetailEditView: View {
    @State private var scrum = DailyScrum.emptyScrum


    var body: some View {
        Form {
            Section(header: Text("Meeting Info")) {
                TextField("Title", text: $scrum.title)
                HStack {
                    Slider(value: $scrum.lengthInMinutesAsDouble, in: 5...30, step: 1) {
                        Text("Length")
                    }
                    Spacer()
                    Text("\(scrum.lengthInMinutes) minutes")
                }
            }
        }
    }
}

/* 코드 생략 */

즉, Slider(value: $scrum.lengthInMinutesAsDouble, in: 5...30, step: 1) 에서 값이 변경이 되면, Double형으로 값을 받지만, Setter을 통해 Int형의 값으로 변환이 이루어집니다.

import SwiftUI


struct DetailEditView: View {
    @State private var scrum = DailyScrum.emptyScrum
    @State private var newAttendeeName = ""
    
    var body: some View {
        Form {
            Section(header: Text("Meeting Info")) {
                TextField("Title", text: $scrum.title)
                HStack {
                    Slider(value: $scrum.lengthInMinutesAsDouble, in: 5...30, step: 1) {
                        Text("Length")
                    }
                    Spacer()
                    Text("\(scrum.lengthInMinutes) minutes")
                }
            }
            Section(header: Text("Attendees")) {
                ForEach(scrum.attendees) { attendee in
                    Text(attendee.name)
                }
                .onDelete { indices in
                    scrum.attendees.remove(atOffsets: indices)
                }
                HStack {
                    TextField("New Attendee", text: $newAttendeeName)
                    Button(action: {
                        withAnimation {
                            let attendee = DailyScrum.Attendee(name: newAttendeeName)
                            scrum.attendees.append(attendee)
                            newAttendeeName = ""
                        }
                    }) {
                        Image(systemName: "plus.circle.fill")
                    }
                    .disabled(newAttendeeName.isEmpty)
                }
            }
        }
    }
}
/* 코드 생략 */

이 코드에서 주목할 부분을 정리해보았습니다.

  • ForEach(scrum.attendees): 스크럼의 참가자 목록을 나타내기 위해 scrums.attendees를 순회하고 각 참가자의 이름을 표시합니다.
  • .onDelete: 참가자 목록에서 항목을 삭제할 수 있는 기능을 추가합니다.
  • .disabled(newAttendeeName.isEmpty): 입력된 참가자 이름이 없을 경우 버튼을 비활성화합니다.

Passing data

Passing data with bindings

import SwiftUI


struct ThemePicker: View {
    @Binding var selection: Theme
    
    var body: some View {
        Picker("Theme", selection: $selection) {
            ForEach(Theme.allCases) { theme in
                ThemeView(theme: theme)
                    .tag(theme)
            }
        }
        .pickerStyle(.navigationLink)
    }
}


struct ThemePicker_Previews: PreviewProvider {
    static var previews: some View {
        ThemePicker(selection: .constant(.periwinkle))
    }
}

@Binding var selection: Theme 는 외부에서 주어진 Theme의 값을 바인딩 받았습니다. 외부에서 변경되는 값에 View가 업데이트되어야 하기 때문입니다.

import SwiftUI


struct ScrumsView: View {
    @Binding var scrums: [DailyScrum]
  
    var body: some View {
        NavigationStack {
            List($scrums) { $scrum in
                NavigationLink(destination: DetailView(scrum: $scrum)) {
                    CardView(scrum: scrum)
                }
                .listRowBackground(scrum.theme.mainColor)
            }
            .navigationTitle("Daily Scrums")
            .toolbar {
                Button(action: {}) {
                    Image(systemName: "plus")
                }
                .accessibilityLabel("New Scrum")
            }
        }
    }
}

/* 코드 생략 */

scrums는 지속적으로 업데이트 되어야 하는데, 그래서 DailyScrum 배열을 바인딩하여 사용하였습니다.


이번 차시에는 @Binding과 @State에 대하여 주로 공부하였습니다.
Swift에서 지켜야하는 Source Of Truth를 위해 활용하는 방법에 대하여 잘 알아야할 것 같습니다.

profile
T자형 개발자가 되고 싶은 GraceKim입니다 / 구름톤 유니브 Leader

0개의 댓글