
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