안녕하세요, GraceKim입니다! 🍎
이번에는 3차시로 Navigation and modal presentation과 Passing data 부분을 공부하여 정리해보려고 합니다. 주로 몰랐던 내용들만 위주로 기록하고자 하니, View를 단순히 스타일링을 위해 작성되는 부분은 조금 빠진 내용이 있을 수 있습니다.
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)을 가지기 때문에, 참여자의 정보가 초기화된 후에는 변경되지 않습니다. 이는 예측 가능하고 안전한 코드를 작성하는 데 도움이 됩니다.
이는 코드의 유지 보수성을 높이고, 확장성을 갖추며, 타입 안정성을 확보하는 데 도움이 됩니다.
이 부분은 특별히 데이터의 흐름을 관리하고 사용자 상호 작용에 대하여 앱 데이터의 현재 상태를 반영하는 방법에 대한 문서입니다. 정리해서 요약해보겠습니다.
@State
와 @Binding
를 사용하면 Source of Truths를 유지하는 데에 도움이 됩니다.@State
로 선언된 속성은 앱 전체에 공유되는 데이터의 중심이 되는 부분을 뜻합니다.@State
속성이 변경되면, 시스템은 자동으로 뷰를 다시 그려 업데이트합니다.@Binding
은 기존에 앱 전체에 공유되는 데이터의 중심이 되는 부분(ex. @State)과 연결된 속성을 나타냅니다.@Binding
은 여러 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()을 사용하는 경우는 다음과 같습니다.
- 다른 타입으로 값을 변환하여 사용해야할 때, Getter을 통해 변환된 값을 얻을 수 있습니다.
- Setter를 사용하여 값을 설정할 때, 특정 조건을 적용하거나 가공된 값을 프로퍼티에 할당할 수 있습니다.
- 일부 속성은 다른 속성들로부터 계산되어야 할 때, 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)
: 입력된 참가자 이름이 없을 경우 버튼을 비활성화합니다.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를 위해 활용하는 방법에 대하여 잘 알아야할 것 같습니다.