[SwiftUI] Date Planner 제작기 2

Valse·2022년 10월 30일
1

DatePlannerDiary

목록 보기
2/3
post-thumbnail

Date Planner

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
		}
}
 

Form의 장난질 해결하기

  • 저번 글에서 작성했던 버튼 추가로 인한 폼 전체가 터치 영역이 된 문제는 의외로 매우 간단하게 해결했다.
    버튼의 스타일을 설정해서 버튼의 터치 영역을 제한할 수 있는 것으로 보인다.
Button("+ Add Task") {
					taskViewList.append(TaskView(taskDescriptionArray: $taskDescriptionArray))
				}.buttonStyle(.borderless)

동적으로 UI를 추가하기

  • 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
}

viewDidLoad() 가 그립다.

  • SwiftUI의 라이프사이클을 내가 아직 잘 이해하지 못하고 있어서 그런지, onAppear의 동작에 의문이 많이 들었다.

  • onAppear는 뷰마다 여러 차례 호출될 수 있기 때문에 나는 메모리에 등록되는 시점인 viewDidLoad의 역할을 해줄 메소드를 찾아다녔다.

  • ViewModifierView 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

profile
🦶🏻🦉(발새 아님)

0개의 댓글