[iOS 16주차] 문제 해결: UIViewController에서 FamilyActivityPicker 호출하기

DoyleHWorks·2025년 2월 5일
2

문제 배경 및 파악

팀원이 구현해준 ViewController에서 SwiftUI에서만 지원하는 FamilyActivityPicker를 호출해야 하였다. ViewController 안에 HostingController를 선언함으로써 해당 UI를 띄우는 데는 성공했지만, 문제는 Picker의 '취소'와 '완료' 버튼을 눌러도 창이 닫히지 않았다. 애플 공식 문서를 봐도, 예시 코드를 봐도 SwiftUI의 View에서 FamilyActivityPicker를 불러주기만 하면 그 안의 '취소' 버튼과 '완료' 버튼은 정상적으로 작동한다. 아무래도 문제는 역시 UIKit 기반의 ViewController에서 억지로 FamilyActivityPicker를 불러와서 그런 것 같다.

import UIKit
import SwiftUI
import FamilyControls

final class StarModalViewController: UIViewController {

// ...

    private func appPicker() {
        let isPresentedBinding = Binding<Bool>(
            get: { self.isFamilyActivityPickerPresented },
            set: { newValue in
                self.isFamilyActivityPickerPresented = newValue
                // picker가 닫힐 때(newValue가 false) 필요한 작업을 추가할 수 있음
            }
        )
        
        let selectionBinding = Binding<FamilyActivitySelection>(
            get: { self.familyActivitySelection },
            set: { newSelection in
                self.familyActivitySelection = newSelection
                // 선택 결과를 viewModel이나 다른 곳에 전달할 수 있음
                print("선택된 Family Activity: \(newSelection)")
            }
        )
        
        let pickerView = FamilyActivityPickerWrapper(isPresented: isPresentedBinding, selection: selectionBinding)
        let hostingVC = UIHostingController(rootView: pickerView)
        hostingVC.modalPresentationStyle = .formSheet        
        self.isFamilyActivityPickerPresented = true
        self.present(hostingVC, animated: true, completion: nil)
    }
}
import SwiftUI
import FamilyControls

struct FamilyActivityPickerWrapper: View {
    @Binding var isPresented: Bool
    @Binding var selection: FamilyActivitySelection
    
    var body: some View {
        Color.clear
            .familyActivityPicker(isPresented: $isPresented, selection: $selection)
            .onChange(of: selection) { newSelection in
                let applications = selection.applications
                let categories = selection.categories
                let webDomains = selection.webDomains
            }
    }
}

문제 접근

명시적으로 dismiss 넣어주기

    private func appPicker() {
        let isPresentedBinding = Binding<Bool>(
            get: { self.isFamilyActivityPickerPresented },
            set: { newValue in
                self.isFamilyActivityPickerPresented = newValue
                // picker가 닫힐 때(newValue가 false) 필요한 작업을 추가할 수 있음
                hostingVC.dismiss(animated: true) // -> 컴파일 오류 발생
            }
        )
        
        let selectionBinding = Binding<FamilyActivitySelection>(
            get: { self.familyActivitySelection },
            set: { newSelection in
                self.familyActivitySelection = newSelection
                // 선택 결과를 viewModel이나 다른 곳에 전달할 수 있음
                print("선택된 Family Activity: \(newSelection)")
                hostingVC.dismiss(animated: true) // -> 컴파일 오류 발생
            }
        )
        
        let pickerView = FamilyActivityPickerWrapper(isPresented: isPresentedBinding, selection: selectionBinding)
        let hostingVC = UIHostingController(rootView: pickerView)
        hostingVC.modalPresentationStyle = .formSheet
        
        self.isFamilyActivityPickerPresented = true
        self.present(hostingVC, animated: true, completion: nil)
    }

하지만 이렇게 하니 hostingVC.dismiss 보다도 hostingVC가 선언되는 시점이 뒤이기 때문에 컴파일 오류가 발생하였다.

문제는 pickerViewBinding<Bool>selection<FamilyActivitySelection>를 필요로 하고,
hostingVC는 그런 pickerView를 필요로 하며,
hostingVC.dismissBinding<Bool>selection<FamilyActivitySelection> 안에 호출해야한다.

hostingVC의 rootView에 임시 View 넣어 미리 선언하기

#1

    private func appPicker() {
	    let hostingVC = UIHostingController(rootView: View()) // -> 컴파일 오류: any View - 프로토콜이라서 Initializer 지원 안 함
        
        // isPresentedBinding 구현부
        // selectionBinding 구현부
        
        let pickerView = FamilyActivityPickerWrapper(isPresented: isPresentedBinding, selection: selectionBinding)
        hostingVC.rootView = pickerView
        hostingVC.modalPresentationStyle = .formSheet
        
        self.isFamilyActivityPickerPresented = true
        self.present(hostingVC, animated: true, completion: nil)
    }

#2

    private func appPicker() {
    	let testView = TestView()
	    let hostingVC = UIHostingController(rootView: testView) 
        
        // isPresentedBinding 구현부
        // selectionBinding 구현부
        
        let pickerView = FamilyActivityPickerWrapper(isPresented: isPresentedBinding, selection: selectionBinding)
        hostingVC.rootView = pickerView // 컴파일 오류: 'TestView'라는 타입만 받음
        hostingVC.modalPresentationStyle = .formSheet
        
        self.isFamilyActivityPickerPresented = true
        self.present(hostingVC, animated: true, completion: nil)
    }

컴파일 오류가 발생함

문제 해결

+) pickerView가 받는 Initializer 변수를 임시 변수로 선언

#3

    private func appPicker() {
        let tempIsPresentedBinding = Binding<Bool>(
            get: { self.isFamilyActivityPickerPresented },
            set: { self.isFamilyActivityPickerPresented = $0 }
        )
        
        let tempSelectionBinding = Binding<FamilyActivitySelection>(
            get: { self.familyActivitySelection },
            set: { self.familyActivitySelection = $0 }
        )
        
        let hostingVC = UIHostingController( // hostingVC를 먼저 선언하여 아래 구현부에서 dismiss를 호출할 수 있게 함
            rootView: FamilyActivityPickerWrapper(isPresented: tempIsPresentedBinding,
                                                  selection: tempSelectionBinding)
        )
        
        hostingVC.modalPresentationStyle = .formSheet

        let isPresentedBinding = Binding<Bool>(
            get: { self.isFamilyActivityPickerPresented },
            set: { newValue in
                self.isFamilyActivityPickerPresented = newValue
                // picker가 닫힐 때(newValue가 false) 필요한 작업을 추가할 수 있음
                hostingVC.dismiss(animated: true)
            }
        )
        
        let selectionBinding = Binding<FamilyActivitySelection>(
            get: { self.familyActivitySelection },
            set: { newSelection in
                self.familyActivitySelection = newSelection
                // 선택 결과를 viewModel이나 다른 곳에 전달할 수 있음
                print("선택된 Family Activity: \(newSelection)")
                hostingVC.dismiss(animated: true)
            }
        )
        
        let pickerView = FamilyActivityPickerWrapper(isPresented: isPresentedBinding, selection: selectionBinding)
        hostingVC.rootView = pickerView // rootView를 갱신
        
        self.isFamilyActivityPickerPresented = true
        self.present(hostingVC, animated: true, completion: nil)
    }

위와 같은 코드로 문제를 해결할 수 있었다.
UIKit 기반의 코드에서 SwiftUI 기반인 FamilyActivityPicker를 여는 것만으로도 코드가 이렇게나 길어졌다..

이제 selectionBinding의 set 블록에서 데이터를 전달해주고, '없음 >' 라벨도 간단하게 업데이트되게 하면 될 것 같다..

추가 문제 해결

FamilyActivityPickerWrapper를 띄울 때 hostingVC를 경유해서 그런지 formSheet 형식의 Present가 두 번 발생하게 되는데,

        hostingVC.modalPresentationStyle = .overFullScreen
        hostingVC.view.backgroundColor = .clear

위와 같이 설정을 바꿔 시각적인 요소는 해결하였다.

profile
Reciprocity lies in knowing enough

0개의 댓글

관련 채용 정보