팀원이 구현해준 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
}
}
}
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가 선언되는 시점이 뒤이기 때문에 컴파일 오류가 발생하였다.
문제는 pickerView
는 Binding<Bool>
과 selection<FamilyActivitySelection>
를 필요로 하고,
hostingVC
는 그런 pickerView
를 필요로 하며,
hostingVC.dismiss
는Binding<Bool>
과 selection<FamilyActivitySelection>
안에 호출해야한다.
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)
}
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)
}
컴파일 오류가 발생함
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
위와 같이 설정을 바꿔 시각적인 요소는 해결하였다.