iOS/App

Scrumdinger 개발 03 - Managing data flow between views ~ Creating the edit view

태애니 2025. 4. 2. 20:53
728x90

 

 

Managing data flow between views

Source of truth

Swift property wrappers @State, @Binding

The system establishes dependencies between the data in @State and the child view that contains the @Binding.

 

-> the system automatically updates the relevant views

 

 

 

Properties that you label @State are mutable, and the system observes these properties for changes in the view where you define them.

@State 로 지정 되면 속성이 변경되고, 이를 자동으로 감지한다.

 

 

 

 

 

 

 

 

Creating the edit view

 

Add an edit view for scrum details

import Foundation
import ThemeKit

struct DailyScrum: Identifiable {
    let id: UUID
    var title: String
    var attendees: [Attendee]
    var lengthInMinutes: Int
    
    // slider 조절을 위해 계산할 수 있는 property 를 설정한다
    var lengthInMinutesAsDouble: Double {
        // lengthInMinutes
        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
    }
}


extension DailyScrum {
    struct Attendee: Identifiable {
        let id: UUID
        var name: String
        //Add the required id property and a variable for name.
        init(id: UUID = UUID(), name: String) {
            self.id = id
            self.name = name
        }
    }
    
    // 이런 식으로 빈 값을 설정해둘 수 있구나. 새롭게 배움**
    static var emptyScrum: DailyScrum {
        DailyScrum(title: "", attendees: [], lengthInMinutes: 5, theme: .poppy)
    }
}

 

 

extension 을 잘 활용할 줄 몰랐는데

여러 방법으로 활용할 수 있을 것 같아서 많이 배운 것 같다.

 

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)
                /**
                 TextField takes a binding to a String. You can use the $ syntax to create a binding to scrum.title. The current view manages the state of the data property.
                 */
                HStack {
                    Slider(value: $scrum.lengthInMinutesAsDouble, in: 5...30, step: 1) {
                        Text("Length")
                        //the slider and the label stay in sync.
                    }
                    Spacer()
                    Text("\(scrum.lengthInMinutes) minutes")
                }
            }
        }
    }
}

#Preview {
    DetailEditView()
}

 

 

 

Display attendees in the edit view

                HStack {
                    TextField("새로운 참석자", text: $attendeeName)
                    Button(action: {
                        withAnimation {
                            //setting the value to the empty string also clears the contents of the text field.
                            let attendee = DailyScrum.Attendee(name: attendeeName)
                            scrum.attendees.append(attendee)
                            
                            // binding to attendeeName, setting the value to the empty string also clears the contents of the text field
                            // animation block 안에서 처리!
                            attendeeName = ""
                        }
                    }) {
                        Image(systemName: "plus.circle.fill")
                    }
                    .disabled(attendeeName.isEmpty)
                }

 

Add accessibility modifiers & Present the edit view

 

// Form 코드 수정

            Section(header: Text("Meeting Info")) {
                TextField("회의명", text: $scrum.title)
                /**
                 TextField takes a binding to a String. You can use the $ syntax to create a binding to scrum.title. The current view manages the state of the data property.
                 */
                HStack {
                    Slider(value: $scrum.lengthInMinutesAsDouble, in: 5...30, step: 1) {
                        Text("Length")
                        //the slider and the label stay in sync.
                    }
                    .accessibilityValue("\(scrum.lengthInMinutes) minutes") // 접근성
                    Spacer()
                    Text("\(scrum.lengthInMinutes) minutes")
                        .accessibilityValue("\(scrum.lengthInMinutes) minutes")
                        .accessibilityHidden(true) //중복이므로 hidden
                }
            }

 

import SwiftUI

struct DetailView: View {
    let scrum: DailyScrum
    
    @State private var isPresentingEditView = false //add a Boolean @State property named isPresentingEditView.
    
    
    var body: some View {
        List {
            Section(header: Text("Meeting Info")) {
                //                Label("Start Meeting", systemImage: "timer")
                //                    .font(.headline)
                //                    .foregroundColor(.accentColor)
                
                // 3depth
                NavigationLink(destination: MeetingView()) {
                    Label("Start Meeting", systemImage: "timer")
                        .font(.headline)
                        .foregroundColor(.accentColor)
                }
                
                HStack {
                    Label("Length", systemImage: "clock")
                    Spacer()
                    Text("\(scrum.lengthInMinutes) minutes")
                }
                .accessibilityElement(children: .combine)
                /**
                 VoiceOver 사용 시 설정
                 
                 .accessibilityElement
                 .accessibilityLabel("내 맘대로 정의") // 사용자가 정의한 라벨 적용
                 SwiftUI에서 접근성(VoiceOver 등)이 특정 UI 요소를 어떻게 인식할지 정의
                 
                 .ignore : 접근성 요소로 인식하지 않음
                 .combine : 하나의 요소로 읽음
                 .contain : 개별로 읽음
                 */
                
                HStack {
                    Label("Theme", systemImage: "paintpalette")
                    Spacer()
                    Text(scrum.theme.name)
                        .padding(4)
                        .foregroundColor(scrum.theme.accentColor)
                        .background(scrum.theme.mainColor)
                        .cornerRadius(4)
                }
                .accessibilityElement(children: .combine)
            }
            
            // 참석자
            Section(header: Text("Attendees")) {
                ForEach(scrum.attendees) { attendee in
                    Label(attendee.name, systemImage: "person")
                }
            }
        }
        .navigationTitle(scrum.title)
        .toolbar {
            Button("수정") {
                isPresentingEditView = true
            }
        }
        .sheet(isPresented: $isPresentingEditView) {
            NavigationStack{
                DetailEditView() // 3depth
                // sheet 에 보여질 내용들을 아래에 구성한다
                    .navigationTitle(scrum.title)
                    .toolbar{
                        ToolbarItem(placement: .cancellationAction) {
                            Button("취소") {
                                isPresentingEditView = false
                            }
                        }
                        ToolbarItem(placement: .confirmationAction) {
                            Button("저장") {
                                isPresentingEditView = false
                            }
                        }
                    }
            }
        }
    }
}

#Preview {
    //Wrap DetailView in a NavigationStack to preview navigation elements on the canvas.
    NavigationStack {
        DetailView(scrum: DailyScrum.sampleData[0])
    }
}

 

 

Button(action: { isPresented = true }) { Text("Edit")
}.sheet(isPresented: $isPresented) {
	EditView()
}
//The sheet modifier takes a binding to a Bool and a view builder.

 

 

 

 ForEach(names, id: \.self) { name in
     Text(name)
 }
 .onDelete { indices in
    names.remove(atOffsets: indices)
 }
The method remove(atOffsets:) uses the IndexSet to remove elements from the array.
indices
onDelete 클로저의 매개변수로 전달되는 값.
삭제할 항목의 인덱스들을 담고 있는 IndexSet 타입의 값이다.
사용자가 삭제한 리스트 항목들의 위치 정보(인덱스 번호)를 포함한다.
728x90