Date Planner 라는 플레이그라운드 앱이 있습니다.
고 녀석을 만드는 중이고 3일이 지났군요.
어제 기록하지 못했던 내용도 함께 기록할 예정입니다.
보통 modal로 띄워진 View가 있다면 'add' 버튼을 눌러서 데이터를 저장하고 전달할 것 같다고 생각한다.
가장 직관적이고 상식적이라고 해야 하나?
그런데 내가 작성하고 있던 로직에서는 버튼을 누르면 데이터가 휘발되어 사라져있고 onDisappear
로 전달할 때에야 제대로 데이터가 기록되었다.
내가 판단한 문제 상황은 이렇다.
Binding
된 하위 뷰의 정보가 뷰에서 사라지면서 휘발되고, 화면의 초기로 돌아오면서 @State
의 초기값으로 설정된 다음에 add
가 작동하는 로직을 벗어나지 못한 것 같다.
여차하면 Add 버튼 동작 전까지는 구조체만 만들어 두고 환경객체에는 Add 버튼을 눌러야 등록되도록 하는 방법도 괜찮겠다고 생각하고 있다.
기능상 차이는 없을 것 같지만 더 직관적인 코드를 작성할 수 있을 것 같아서.
내 로직에서는 ForEach
가 돌면서 State를 받기 때문인지 onDisappear
도 여러차례 호출되는 걸 볼 수 있다.
id
로 구별하거나 Set
을 써서 후처리를 해야 할 것 같다.
// MARK: 하위 TaskView
let id = UUID().uuidString
@State private var subEventFlag: Bool = false
@State private var subTaskDescription: String = ""
typealias TaskDescriptionType = (eventFlag: Bool, TaskDescription: String)
@Binding var taskDescriptionArray: [TaskDescriptionType]
var body: some View {
VStack {
HStack {
Toggle("", isOn: $subEventFlag).labelsHidden()
TextField("Task Description", text: $subTaskDescription)
.autocorrectionDisabled(true)
}
Divider()
}.onDisappear {
// 바인딩 배열에 먼저 어펜드 한 후에, 상위 뷰에서 다시 한 번 어펜드하여 데이터 전달.
// 아마 리팩토링을 하게 되면 더 큰 상위의 데이터 모델에서 어펜드할 예정
taskDescriptionArray.append((subEventFlag, subTaskDescription))
}
}
/*-------------------------------------------------------*/
// MARK: 상위 MakeNewEventView
ForEach(taskViewList, id: \.id) { taskListView in
taskListView
.onDisappear {
taskListView.taskDescriptionArray)
let newEvent = EachEvent(
eventName: eventName,
eventIconName: eventIconName,
eventDate: eventDate,
eventDescription: taskDescriptionArray)
eventEnvObj.allEventDictionary[newEvent.eventId] = newEvent
}
}
Button("+ Add Task") {
taskViewList.append(TaskView(taskDescriptionArray: $taskDescriptionArray))
}.buttonStyle(.borderless)
Date Planner
는 + Add Task
버튼을 누를 때 하단에 텍스트필드와 토글을 추가하는 기능이 있다.
처음에는 @ViewBuilder
를 사용해서 동적으로 추가할 수 있겠다고 생각했는데, 반복적으로 UI를 다시 그려야 하기 때문에 ForEach
와 배열을 활용했다.
문제는 @ViewBuilder
로 생성하는 오팩타입 some View
를 꺼내서 쓰려면 AnyView
를 써야 하는데 이러면 ForEach
사용도 복잡해질 뿐더러 공식적으로 AnyView
사용은 권장되지 않기 때문에 내리막길을 걷는 코드를 쓰게 된다.
결국 TaskView
구조체를 받는 배열로 따로 정리해서 문제를 해결했다.
위의 코드가 바로 이 상황을 대변하는 코드라 할 수 있다.
@State private var taskViewList: [TaskView] = []
ForEach(taskViewList, id: \.id) { taskListView in
taskListView
// code
}
SwiftUI의 라이프사이클을 내가 아직 잘 이해하지 못하고 있어서 그런지, onAppear
의 동작에 의문이 많이 들었다.
onAppear
는 뷰마다 여러 차례 호출될 수 있기 때문에 나는 메모리에 등록되는 시점인 viewDidLoad
의 역할을 해줄 메소드를 찾아다녔다.
ViewModifier
와 View extension
으로 수식어를 만들어서 활용하기로 했다.
내용은 스택오버플로우를 활용했다.
didLoad
의 상태가 false
일 때 호출되며, 내가 따로 true
로 바꾸지 않는 이상 이 메소드는 단 한번만 호출될 것이다.
extension View {
public func onLoad(perform action: (() -> Void)? = nil) -> some View {
modifier(ViewDidLoadModifier(perform: action))
}
}
struct ViewDidLoadModifier: ViewModifier {
@State private var didLoad = false
private let action: (() -> Void)?
init(perform action: (() -> Void)? = nil) {
self.action = action
}
public func body(content: Content) -> some View {
content.onAppear {
if didLoad == false {
didLoad = true
action?()
}
}
}
}
Binding<T>
제너릭 타입의 State 변수는 프로퍼티 감시자로 확인할 수 없다.
실제로 변하는 것은 변수 자체가 아니라 State의 wrappedValue
이기 때문.
번거롭지만 그 내용을 확인하고 싶다면 따로 꺼내서 확인하거나 onChanged
를 활용해야 한다.
내부 로직을 더 깔끔하게 쓰는 방법을 고민해야겠다.
스파게티 투성이다.
221030