$30 off During Our Annual Pro Sale. View Details »

SwiftUIとUIKitを仲良くさせる

Mike Apurin
September 11, 2022

 SwiftUIとUIKitを仲良くさせる

Mike Apurin

September 11, 2022
Tweet

More Decks by Mike Apurin

Other Decks in Programming

Transcript

  1. • ΏΊΈͷ iOS ΤϯδχΞ • ࣾ಺Ͱ SwiftUI ษڧձΛ։࠵͢Δ΄Ͳ SwiftUI ʹ͸·͍ͬͯΔ

    • Ջͷͱ͖ͳΒίʔώʔΛᔸΕ͍ͯΔ ☕ Mike Apurin ΞϓϦϯɾϛϋΠϧ auramagi auramagi
  2. UIKitͷதͰSwiftUIΛ࢖͏ • UIHostingController • SwiftUI ͷ View Λදࣔ͢Δ UIViewController •

    UIHostingCon fi guration — 🆕 iOS 16 • UITableViewCell / UICollectionViewCell ͷ contentCon fi guration ͱͯ͠ ઃఆͯ͠ηϧ୯ҐͰ SwiftUI ͷ View Λදࣔ
  3. UIHostingController — جຊ • ੜ੒ͯ͠୯ಠͰදࣔ͢Δ func showSwiftUIView() { let host

    = UIHostingController( rootView: Text("Hello iOSDC 2022!") ) show(host, sender: self) } 􀬂
  4. UIHostingController — جຊ • ੜ੒ͯ͠୯ಠͰදࣔ͢Δ func showSwiftUIView() { let host

    = UIHostingController( rootView: Text("Hello iOSDC 2022!") ) show(host, sender: self) }
  5. UIHostingController — جຊ • ੜ੒ͯ͠୯ಠͰදࣔ͢Δ func showSwiftUIView() { let host

    = UIHostingController( rootView: Text("Hello iOSDC 2022!") ) show(host, sender: self) } // ... host.rootView = Text(“ϥʔϝϯ৯΂͍ͨ")
  6. UIHostingController — جຊ • UIViewController ͷࢠڙͱ͠දࣔ͢Δ let host = UIHostingController(rootView:

    Text("Hello iOSDC 2022!”)) host.willMove(toParent: self) addChild(host) view.addSubview(host.view) host.didMove(toParent: self) host.view.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ host.view.topAnchor.constraint(equalTo: view.topAnchor), host.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), host.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), host.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), ]) • ࢠVCͱͯ͠௥Ճ͓͔ͯ͠ͳ͍ͱϨΠΞ΢τͷ໰୊͕ൃੜ͢Δ৔߹͕͋Δ • Safe Area ͕ઃఆ͞Εͳ͍ • Environment ʹઃఆͨ͠஋͕൓ө͞Εͳ͍
  7. UIHostingController — جຊ struct EnvironmentTestView: View { @Environment(\.colorScheme) var colorScheme

    var body: some View { Text(verbatim: "\(colorScheme)") .font(.title) .padding(80) } } let host = UIHostingController( rootView: VStack { EnvironmentTestView() .border(.white) Rehost { // UIViewControllerΛ௨ͯ͠SwiftUIΛදࣔ͢Δ EnvironmentTestView() .border(.black) } } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.blue.ignoresSafeArea()) .environment(\.colorScheme, .dark) ) host.willMove(toParent: parent) addChild(host) view.addSubview(host.view) host.didMove(toParent: parent)
  8. UIHostingController — جຊ struct EnvironmentTestView: View { @Environment(\.colorScheme) var colorScheme

    var body: some View { Text(verbatim: "\(colorScheme)") .font(.title) .padding(80) } } let host = UIHostingController( rootView: VStack { EnvironmentTestView() .border(.white) Rehost { // UIViewControllerΛ௨ͯ͠SwiftUIΛදࣔ͢Δ EnvironmentTestView() .border(.black) } } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.blue.ignoresSafeArea()) .environment(\.colorScheme, .dark) ) //host.willMove(toParent: parent) //addChild(host) view.addSubview(host.view) //host.didMove(toParent: parent) ͣΕΔ Environment ͕ؒҧ͍ͬͯΔ
  9. UIHostingController — ঢ়ଶͷ࣋ͪํ • ΠϯελϯεΛอ࣋ͯ͠ rootView Λஔ͖׵͑Δ struct MessageView: View

    { let message: String let action: () -> Void var body: some View { VStack { Text(message) Button("New message (SwiftUI)", action: action) } } } class HostingViewController: UIViewController { var message: String = "Not selected" { didSet { updateHostView() } } lazy var host = UIHostingController( rootView: MessageView(message: message) { [unowned self] in message = // ... } ) func updateHostView() { host.rootView = MessageView(message: message) { [unowned self] in message = // ... } } } 􀬂 􀬂
  10. UIHostingController — ঢ়ଶͷ࣋ͪํ • ΠϯελϯεΛอ࣋ͯ͠ rootView Λஔ͖׵͑Δ struct MessageView: View

    { let message: String let action: () -> Void var body: some View { VStack { Text(message) Button("New message (SwiftUI)", action: action) } } } class HostingViewController: UIViewController { var message: String = "Not selected" { didSet { updateHostView() } } lazy var host = UIHostingController( rootView: MessageView(message: message) { [unowned self] in message = // ... } ) func updateHostView() { host.rootView = MessageView(message: message) { [unowned self] in message = // ... } } }
  11. UIHostingController — ঢ়ଶͷ࣋ͪํ • UIViewController ͱ View Ͱڞ௨ͷ ObservableObject Λ

    ௨ͯ͠ঢ়ଶΛ؅ཧ͢Δ @MainActor class SharedModel: ObservableObject { @Published var message: String = "Not selected" func action() { ... } } struct MessageView: View { @ObservedObject var model: SharedModel var body: some View { VStack { Text(model.message) Button("New message (SwiftUI)", action: model.action) } } }
  12. UIHostingController — ঢ়ଶͷ࣋ͪํ • UIViewController ͱ View Ͱڞ௨ͷ ObservableObject Λ

    ௨ͯ͠ঢ়ଶΛ؅ཧ͢Δ class HostingViewController: UIViewController { private let model = SharedModel() private var cancellables: Set<AnyCancellable> = [] override func viewDidLoad() { super.viewDidLoad() let host = UIHostingController(rootView: MessageView(model: model) ) let label = UILabel() model.$message .sink { label.text = $0 } .store(in: &cancellables) // ... } } 􀬂 􀬂
  13. UIHostingController — ঢ়ଶͷ࣋ͪํ • UIViewController ͱ View Ͱڞ௨ͷ ObservableObject Λ

    ௨ͯ͠ঢ়ଶΛ؅ཧ͢Δ class HostingViewController: UIViewController { private let model = SharedModel() private var cancellables: Set<AnyCancellable> = [] override func viewDidLoad() { super.viewDidLoad() let host = UIHostingController(rootView: MessageView(model: model) ) let label = UILabel() model.$message .sink { label.text = $0 } .store(in: &cancellables) // ... } }
  14. UIHostingController — ঢ়ଶͷ࣋ͪํ • ଞʹ΋ํ๏͸͋Γ·͢ • Combine ͷ Publisher Λ౉ͯ͠ߪಡ͢Δ

    struct MessageView: View { let publisher: AnyPublisher<String, Never> @State private var message: String = "" var body: some View { Text(message) .onReceive(publisher) { message = $0 } } } • ϓϩύςΟɾΞΫγϣϯΛ @Environment ʹೖΕΔ • ڞ௨ϞσϧΛ @EnvironmentObject Ͱઃఆ͢Δ
  15. 1. ਌͕ࢠ View ʹαΠζΛఏҊ 2. ࢠ View ͕ఏҊΛݩʹͳΓ͍ͨαΠζΛܾΊΔ 3. ਌͕ࣗ෼ͷதͰࢠ

    View Λ഑ஔ SwiftUIϨΠΞ΢τॲཧͷ͓͞Β͍ ࢠ ਌ αΠζఏҊ ͳΓ͍ͨαΠζ
  16. • αΠζఏҊ struct ProposedViewSize: Equatable { var width: CGFloat? var

    height: CGFloat? } • nil ͷͱ͖ʹ੍໿͕ͳ͍৔߹ͷαΠζΛܭࢉ • . fi xedSize() Λ͔͚ͨͱ͖΍ ScrollView ʹೖΕ ͨͱ͖ͳͲ • ϏϡʔࣗମͷαΠζ͸ͳ͍ͱ͖͸ϓϨΠεϗ ϧμʔ஋ͷ 10 ͕ฦΔ SwiftUIϨΠΞ΢τॲཧͷ͓͞Β͍ ࢠ ਌ αΠζఏҊ ͳΓ͍ͨαΠζ
  17. func frame( minWidth: CGFloat?, idealWidth: CGFloat?, maxWidth: CGFloat?, minHeight: CGFloat?,

    idealHeight: CGFloat?, maxHeight: CGFloat?, alignment: Alignment ) -> some View • min: ࠷௿஋ • ideal: ཧ૝αΠζ • max: ࠷ߴ஋ • ؔ܎Λݫक: min <= ideal <= max SwiftUIϨΠΞ΢τॲཧͷ͓͞Β͍ ࢠ ਌ αΠζఏҊ ͳΓ͍ͨαΠζ
  18. UIHostingController — ϨΠΞ΢τ • view.intrinsicContentSize ʹத਎ͷ SwiftUI View ͷ ideal

    αΠζ͕൓ө͞ΕΔ • View ʹมߋ͕͋ͬͯ΋͍ͭ΋ਖ਼͍͠஋ʹͳ͍ͬͯΔ • ͨͩ͠σϑΥϧτͰ invalidateIntrinsicContentSize() ͕ࣗಈతʹݺ͹Εͳ͍
  19. UIHostingController — ϨΠΞ΢τ • 🆕 iOS 16 ͔Β͸ sizingOptions Ͱࣗಈߋ৽ͷಈ࡞ΛΧελϚΠζͰ͖Δ

    • .intrinsicContentSize • ࣗಈతʹ UIHostingController.view.invalidateIntrinsicContentSize() ΛݺͿ • .preferredContentSize • ࣗಈతʹ UIHostingController.preferredContentSize ʹ΋αΠζΛ൓ө • ྆ํΛಉ࣌ʹઃఆͰ͖Δ • υΩϡϝϯτʹ͸ɺॲཧίετ͕ߴ͍ͱ஫ҙ͞Ε͍ͯΔ
  20. UIHostingController — ϨΠΞ΢τ class ViewController: UIViewController { func presentAsPopover() {

    let host = UIHostingController( rootView: Text("Hello iOSDC 2022!”).padding() ) host.preferredContentSize = host.view.intrinsicContentSize if #available(iOS 16, *) { host.sizingOptions = .preferredContentSize } host.modalPresentationStyle = .popover host.popoverPresentationController?.sourceView = view host.popoverPresentationController?.sourceRect = view.bounds host.popoverPresentationController?.delegate = self present(host, animated: true) } } extension HostingViewController: UIPopoverPresentationControllerDelegate { func adaptivePresentationStyle( for controller: UIPresentationController ) -> UIModalPresentationStyle { .none } } 􀬂
  21. UIHostingController — ϨΠΞ΢τ class ViewController: UIViewController { func presentAsPopover() {

    let host = UIHostingController( rootView: Text("Hello iOSDC 2022!”).padding() ) host.preferredContentSize = host.view.intrinsicContentSize if #available(iOS 16, *) { host.sizingOptions = .preferredContentSize } host.modalPresentationStyle = .popover host.popoverPresentationController?.sourceView = view host.popoverPresentationController?.sourceRect = view.bounds host.popoverPresentationController?.delegate = self present(host, animated: true) } } extension HostingViewController: UIPopoverPresentationControllerDelegate { func adaptivePresentationStyle( for controller: UIPresentationController ) -> UIModalPresentationStyle { .none } }
  22. UIHostingConfiguration — جຊ • 🆕 iOS 16: UITableViewCell ͋Δ͍͸ UICollectionViewCell

    ୯ҐͰ SwiftUI ͷ ViewΛදࣔ override func tableView( _ tableView: UITableView, cellForRowAt indexPath: IndexPath ) -> UITableViewCell { let cell = tableView.dequeueReusableCell( withIdentifier: "Cell", for: indexPath ) cell.contentConfiguration = UIHostingConfiguration { Text("Hello") } .margins(.all, 16) .background(Color.mint) return cell }
  23. UIHostingConfiguration — جຊ • ηϧ͚ͩͰ͸ͳ͘୯ಠͷ UIView ͷੜ੒Ͱ΋࢖͑Δʢ͔΋ʁʣ let view =

    UIHostingConfiguration { ... }.makeContentView() • UIHostingController Ͱ view Λ୯ಠͨ͠৔߹ͱಈ͖͕ҧ͏ • Safe Area ͸ਖ਼͘͠൓өͤΕ͍ͯΔͬΆ͍ • தʹ͸ UIViewControllerRepresentable ͕࢖͑ͳ͍ • ύϑΥʔϚϯεʹཁ஫ҙɻreuse ͞ΕΔͱ͖ϨΠΞ΢τΛ࠶ߏங͢Δ͔Β 1/120 ඵͰ׬ྃ͢Δඞཁ͕͋Δ
  24. UIHostingConfiguration — ঢ়ଶͷ࣋ͪํ • ⚠ @State / @StateObject ͷѻ͍͸ཁ஫ҙ •

    reuse ͞ΕͨޙͰ΋લʹදࣔ͠ηϧͷঢ়ଶ͕࢒Δ struct Cell: View { @State var i = 0 let indexPath: IndexPath var body: some View { VStack(alignment: .leading) { Text("Cell \(indexPath.section)-\(indexPath.row). i: \(i)") Button("i += 1") { i += 1 } .buttonStyle(.borderless) } } } • جຊతʹ৘ใݯ͸ηϧҎ֎ʹஔ͘ඞཁ͕͋Δ 􀬂
  25. UIHostingConfiguration — ঢ়ଶͷ࣋ͪํ • ⚠ @State / @StateObject ͷѻ͍͸ཁ஫ҙ •

    reuse ͞ΕͨޙͰ΋લʹදࣔ͠ηϧͷঢ়ଶ͕࢒Δ struct Cell: View { @State var i = 0 let indexPath: IndexPath var body: some View { VStack(alignment: .leading) { Text("Cell \(indexPath.section)-\(indexPath.row). i: \(i)") Button("i += 1") { i += 1 } .buttonStyle(.borderless) } } } • جຊతʹ৘ใݯ͸ηϧҎ֎ʹஔ͘ඞཁ͕͋Δ
  26. UIHostingConfiguration — ঢ়ଶͷ࣋ͪํ • ⚠ @State / @StateObject ͷѻ͍͸ཁ஫ҙ •

    reuse ͞ΕͨޙͰ΋લʹදࣔ͠ηϧͷঢ়ଶ͕࢒Δ struct Cell: View { @State var i = 0 let indexPath: IndexPath var body: some View { VStack(alignment: .leading) { Text("Cell \(indexPath.section)-\(indexPath.row). i: \(i)") Button("i += 1") { i += 1 } .buttonStyle(.borderless) } } } • جຊతʹ৘ใݯ͸ηϧҎ֎ʹஔ͘ඞཁ͕͋Δ
  27. UIHostingConfiguration — ঢ়ଶͷ࣋ͪํ • ৘ใݯΛηϧҎ֎ʹஔͨ͘Ίʹ ObservableObject Λ࢖͏ @MainActor class TableModel:

    ObservableObject { @Published var i: [IndexPath: Int] = [:] } struct Cell: View { @ObservedObject var model: TableModel let indexPath: IndexPath var body: some View { VStack(alignment: .leading) { let i = model.i[indexPath, default: 0] Text("Cell \(indexPath.section)-\(indexPath.row). i: \(i)") Button("i += 1") { model.i[indexPath, default: 0] += 1 } .buttonStyle(.borderless) } } }
  28. UIHostingConfiguration — ঢ়ଶͷ࣋ͪํ • ৘ใݯΛηϧҎ֎ʹஔͨ͘Ίʹ ObservableObject Λ࢖͏ class TableView: UITableViewController

    { let model = TableModel() override func tableView( _ tableView: UITableView, cellForRowAt indexPath: IndexPath ) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) cell.contentConfiguration = UIHostingConfiguration { Cell(model: model, indexPath: indexPath) } return cell } }
  29. UIHostingConfiguration — ϨΠΞ΢τ • UIHostingController ͱಉ͘͡த਎ͷ SwiftUI ͷ View ͷ

    ideal αΠζ͕ੜ੒͞Ε ͨ view ͷ intrinsicContentSize ʹ൓ө • invalidateIntrinsicContentSize() ͸ࣗಈతʹݺ͹Εͳ͍ • 🆕 iOS 16: UITableView / UICollectionView ͷ selfSizingInvalidation ϓϩύςΟ • ηϧͷαΠζ͕ߋ৽͞Εͨ͜ͱΛݕ஌ͯࣗ͠ಈ൓ө • tableView.performBatchUpdates { } ͳͲΛ͠ͳͯ͘΋ྑ͘ͳΔ • σϑΥϧτͰ༗ޮ • SwiftUI ʹݶΒͣಉ͡࢓૊ΈͰ Auto Layout ͷ੍໿ߋ৽ΛϐοΫΞοϓͰ͖Δ
  30. UIHostingConfiguration — ϨΠΞ΢τ @MainActor class SharedModel: ObservableObject { @Published var

    emoji: [IndexPath: [String]] = [:] } struct Cell: View { static let emojiPalette: [String] = ["🫠", ...] @ObservedObject var model: SharedModel let indexPath: IndexPath var body: some View { VStack(alignment: .leading) { var emojiList = model.emoji[indexPath, default: []] Text("Cell \(indexPath.section)-\(indexPath.row).") Text(emojiList.joined(separator: "\n")) Button("More emoji!") { emojiList.append(Self.emojiPalette.randomElement()!) model.emoji[indexPath] = emojiList } .buttonStyle(.borderless) } } } 􀬂
  31. UIHostingConfiguration — ϨΠΞ΢τ @MainActor class SharedModel: ObservableObject { @Published var

    emoji: [IndexPath: [String]] = [:] } struct Cell: View { static let emojiPalette: [String] = ["🫠", ...] @ObservedObject var model: SharedModel let indexPath: IndexPath var body: some View { VStack(alignment: .leading) { var emojiList = model.emoji[indexPath, default: []] Text("Cell \(indexPath.section)-\(indexPath.row).") Text(emojiList.joined(separator: "\n")) Button("More emoji!") { emojiList.append(Self.emojiPalette.randomElement()!) model.emoji[indexPath] = emojiList } .buttonStyle(.borderless) } } }
  32. UIViewRepresentable/UIViewControllerRepresentable — جຊ • UIViewRepresentable ϓϩτίϧ • UIView Λϥοϓͯ͠ SwiftUI

    ͷதͰ࢖͏ • UIViewControllerRepresentable ϓϩτίϧ • UIViewController Λϥοϓͯ͠ SwiftUI ͷதͰ࢖͏
  33. UIViewRepresentable/UIViewControllerRepresentable — جຊ struct UILabelView: UIViewRepresentable { let text: String

    func makeUIView(context: Context) -> UILabel { let view = UILabel() view.numberOfLines = 0 return view } func updateUIView(_ uiView: UILabel, context: Context) { uiView.text = text } } struct ContentView: View { var body: some View { UILabelView(text: "Hello World") } }
  34. *Representable — ঢ়ଶͷ࣋ͪํ • ҰํతͳόΠϯσΟϯά͸ී௨ͷϓϩύςΟͰͰ͖Δ struct UILabelView: UIViewRepresentable { let

    text: String func makeUIView(context: Context) -> UILabel { // ... } func updateUIView(_ uiView: UILabel, context: Context) { uiView.text = text } } struct ContentView: View { @State var text: String = "Hello World" var body: some View { UILabelView(text: text) .padding() } }
  35. *Representable — ঢ়ଶͷ࣋ͪํ • ૒ํతͳόΠϯσΟϯάͳΒ @Binding ͳͲΛ౉͢͜ͱͰͰ͖Δ struct UISliderView: UIViewRepresentable

    { @Binding var value: Double let range: ClosedRange<Double> let step: Double class Coordinator { @Binding var value: Double var step: Double init(value: Binding<Double>, step: Double) { self._value = value self.step = step } @objc func updateValue(sender: UISlider) { print(step) value = round(Double(sender.value) / step) * step sender.value = Float(value) } } func makeCoordinator() -> Coordinator { Coordinator(value: $value, step: step) } }
  36. *Representable — ঢ়ଶͷ࣋ͪํ • ૒ํతͳόΠϯσΟϯάͳΒ @Binding ͳͲΛ౉͢͜ͱͰ Ͱ͖Δ struct UISliderView:

    UIViewRepresentable { func makeUIView(context: Context) -> UISlider { let uiView = UISlider() uiView.addTarget( context.coordinator, action: #selector(Coordinator.updateValue(sender:)), for: .valueChanged ) return uiView } func updateUIView(_ uiView: UISlider, context: Context) { context.coordinator.step = step uiView.minimumValue = Float(range.lowerBound) uiView.maximumValue = Float(range.upperBound) uiView.value = Float(value) } }
  37. *Representable — ঢ়ଶͷ࣋ͪํ • Coordinator ͷϓϩύςΟͱͯ͠ Representable ͷߏ଄ମΛอ࣋͢Δͱศར struct UISliderView:

    UIViewRepresentable { class Coordinator { var parent: UISliderView init(_ parent: UISliderView) { self.parent = parent } @objc func updateValue(sender: UISlider) { print(parent.step) parent.value = round(Double(sender.value) / parent.step) * parent.step sender.value = Float(parent.value) } } func makeCoordinator() -> Coordinator { Coordinator(self) } func updateUIView(_ uiView: UISlider, context: Context) { context.coordinator.parent = self // ... } }
  38. *Representable — ঢ়ଶͷ࣋ͪํ • Environment ͳͲ SwiftUI ͷঢ়ଶ؅ཧ΋࢖͑·͢ struct UILabelView:

    UIViewRepresentable { @Environment(\.scenePhase) var scenePhase func makeUIView(context: Context) -> UILabel { ... } func updateUIView(_ uiView: UILabel, context: Context) { uiView.text = "\(scenePhase)" } }
  39. *Representable — ঢ়ଶͷ࣋ͪํ • Environment ͳͲ SwiftUI ͷঢ়ଶ؅ཧ΋࢖͑·͢ struct UILabelView:

    UIViewRepresentable { @Environment(\.scenePhase) var scenePhase func makeUIView(context: Context) -> UILabel { ... } func updateUIView(_ uiView: UILabel, context: Context) { uiView.text = "\(scenePhase)" } }
  40. UIViewRepresentable — ϨΠΞ΢τ • iOS 16 ͔Β͸ఏҊαΠζ͔Βࣗ෼ͷαΠζΛܭࢉ͢Δؔ਺͕௥Ճ͞Ε͍ͯΔ protocol UIViewControllerRepresentable {

    func sizeThatFits( _ proposal: ProposedViewSize, uiViewController: UIViewControllerType, context: Context ) -> CGSize? } protocol UIViewRepresentable { func sizeThatFits( _ proposal: ProposedViewSize, uiView: UIViewType, context: Context ) -> CGSize? } struct ProposedViewSize: Equatable { var width: CGFloat? var height: CGFloat? } ࢠ ਌ αΠζఏҊ ͳΓ͍ͨαΠζ
  41. UIViewRepresentable — ϨΠΞ΢τ • UIView ͷ systemLayoutSizeFitting ͳͲΛ࢖ͬͯ൚༻తͳαΠζܭࢉΛ࣮૷Ͱ͖Δ func sizeThatFits(_

    proposal: ProposedViewSize, uiView: UIViewType, context: Context) -> CGSize? { let widthTarget = target(for: proposal.width) let heightTarget = target(for: proposal.height) return uiView.systemLayoutSizeFitting( CGSize(width: widthTarget.0, height: heightTarget.0), withHorizontalFittingPriority: widthTarget.1, verticalFittingPriority: heightTarget.1 ) } func target(for proposal: CGFloat?) -> (CGFloat, UILayoutPriority) { switch proposal { case .none: return (UIView.layoutFittingCompressedSize.width, .fittingSizeLevel) case .some(.zero): return (UIView.layoutFittingCompressedSize.width, .defaultHigh) case .some(.infinity): return (UIView.layoutFittingExpandedSize.width, .defaultLow) case let .some(value): return (value, .defaultHigh) } } • ܕ৘ใ͕͋Δ͔Βɺͦͷ UIView (UIViewController) ʹಛԽͨ͠ܭࢉ΋࣮૷Ͱ͖Δ ※ ίʔυ͸ΠϝʔδͰ͢
  42. UIViewControllerRepresentable — iOS 15ҎԼͷϨΠΞ΢τ • preferredContentSize ͕ ideal ͷαΠζʹͳΔ •

    αΠζͷ࠷௿஋ɾ࠷ߴ஋͕ઃఆ͞Εͳ͍ͷͰجຊతʹ਌ͷఏҊαΠζͷ ··ʹͳΔ • . fi xedSize() Λ͔͚Δ͜ͱͰ preferredContentSize Ͱઃఆͨ͠஋ʹ͢Δ
  43. UIViewRepresentable — iOS 15ҎԼͷϨΠΞ΢τ • AutoLayoutͷ intrinsicContentSize ͱ priority Λݩʹͯ͠αΠζΛܭࢉ

    Intrinsic size Compression Resistance Hugging ݁Ռ
 (min … ideal … max) (-1) - - 0 … 0 … ∞ x < 750 < 750 0 … x … ∞ x < 750 >= 750 0 … x … x x < 750 < 750 x … x … ∞ x >= 750 >= 750 x … x … x -1 = UIView.noIntrinsicMetric 750 = UILayoutPriority.defaultHigh
  44. UIHostingController — جຊ struct UILabelView: UIViewRepresentable { let text: String

    func makeUIView(context: Context) -> UILabel { let view = UILabel() view.numberOfLines = 0 return view } func updateUIView(_ uiView: UILabel, context: Context) { uiView.text = text } } struct ContentView: View { @State var text: String = "Hello iOSDC 2022" var body: some View { UILabelView(text: text) .padding() .border(.red) } }
  45. UIHostingController — جຊ struct UILabelView: UIViewRepresentable { let text: String

    func makeUIView(context: Context) -> UILabel { let view = UILabel() view.numberOfLines = 0 view.setContentHuggingPriority(.defaultHigh, for: .horizontal) view.setContentHuggingPriority(.defaultHigh, for: .vertical) view.setContentCompressionResistancePriority( .defaultLow, for: .horizontal ) return view } func updateUIView(_ uiView: UILabel, context: Context) { uiView.text = text } } struct ContentView: View { @State var text: String = "Hello iOSDC 2022" var body: some View { UILabelView(text: text) .padding() .border(.red) } }
  46. UIViewRepresentable — iOS 15ҎԼͷϨΠΞ΢τ • σϑΥϧτͷϨΞ΢τॲཧ 1. Auto Layout ͷϓϩύςΟ͔Β

    UIView ͕ͳΕΔαΠζΛܾఆ 2. ͜ΕΛ΋ͱʹɺఏҊ͞ΕͨαΠζ͔ΒαΠζΛܾΊΔ 3. UIView ͷ invalidateIntrinsicContentSize() ͕ݺ͹ΕͨΒ࠶ܭࢉ
  47. RepresentableKit • Auto Layout ϓϩύςΟ͔ΒαΠζΛܭࢉ͢Δ UIViewRepresentable ͷಈ࡞Λந৅Խͯ͠ UIView Λ SwiftUI

    Ͱදࣔ͢ΔͨΊͷϛχϥΠϒϥϦΛ࣮૷ • UIViewAdaptor ʹೖΕΔ͚ͩʂ • github.com/yumemi-inc/RepresentableKit struct UILabel_Previews: PreviewProvider { static var previews: some View { UIViewAdaptor { let view = UILabel() view.numberOfLines = 0 view.text = "To love the journey is to accept no such end. I have found, through painful experience, that the most important step a person can take is always the next one." return view } .padding() } }
  48. AppϥΠϑαΠΫϧ • iOS 14͔Β৽͘͠௥Ճ͞ΕͨSwiftUI޲͚ͷΞϓϦϥΠϑαΠΫϧ • ैདྷͷ UIApplicationDelegate / UISceneDelegate ͷ୅ΘΓͱͳΔ

    @main struct MyAwesomeApp: App { var body: some Scene { WindowGroup { Text("Hello iOSDC 2022") } } }
  49. AppϥΠϑαΠΫϧ • όοΫάϥ΢ϯυʹҠߦɾ෮ؼΛݕ஌ struct MyAwesomeApp: App { @Environment(\.scenePhase) var scenePhase

    var body: some Scene { WindowGroup { Text("Hello iOSDC 2022") .onChange(of: scenePhase) { scenePhase in switch scenePhase { case .background: // ... case .inactive: // ... case .active: // ... @unknown default: // ... } } } } }
  50. AppϥΠϑαΠΫϧ • ࠶ىಈ࣌ͷঢ়ଶ෮ݩ @SceneStorage("info") var info = "" • Deep

    Linkingʹ؆୯ʹରԠ WindowGroup { Text("Hello iOSDC 2022") .onOpenURL { url in // ϦϯΫλοϓɺ௨஌։෧ɺWidgetsͷλοϓͳͲ } .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { userActivity in guard let url = userActivity.webpageURL else { return } // SafariϦμΠϨΫτͳͲ } }
  51. UIApplicationDelegateͱซ༻ͯ͠࢖͏ • UIApplicationDelegate ͷશ෦ͷػೳʹରԠ͍ͯ͠ΔΘ͚Ͱ΋ͳ͍ • ྫ͑͹ɺϓογϡ௨஌ͷσόΠετʔΫϯΛऔಘ͢Δؔ਺૬౰ͷ΋ͷ͕ͳ͍ • application(_, didRegisterForRemoteNoti fi

    cationsWithDeviceToken:) • Ҿ͖ଓ͖ซ༻Ͱ͖ΔΑ͏ʹ͸ϓϩύςΟϥούʔ͕༻ҙ͞Ε͍ͯΔ @main struct MyAwesomeApp: App { @UIApplicationDelegateAdaptor var delegate: AppDelegate var body: some Scene { WindowGroup { Text("Hello iOSDC 2022") } } } class AppDelegate: NSObject, UIApplicationDelegate { ... }
  52. UIApplicationDelegateͱซ༻ͯ͠࢖͏ • طଘͷ UIApplicationDelegate ͔ΒҠߦͨ͠ͱ͖ͷ஫ҙ఺ • window ͷΞΫηεͰ͖ͳ͘ͳΔ • window.makeKeyAndVisible()

    ͷॲཧ΋ෆཁ • UIApplication.shared.delegate ͸ҧ͏ΫϥεʹͳΔ • ࣗ෼ͷΫϥε͸ SwiftUI.AppDelegate ͱ͍͏ SwiftUI ಺෦ͷඇެ։ ϥούʔͰแ·ΕΔ͔Β
  53. UISceneDelegateͱซ༻ͯ͠࢖͏ • ެࣜͷϓϩύςΟͳͲ͕༻ҙ͞Ε͍ͯͳ͍͚Ͳࣗલͷ UISceneDelegate ΋ ซ༻Ͱ͖Δ class AppDelegate: NSObject, UIApplicationDelegate

    { func application( _ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions ) -> UISceneConfiguration { let configuration = UISceneConfiguration( name: "MainScene", sessionRole: connectingSceneSession.role ) configuration.delegateClass = SceneDelegate.self return configuration } } class SceneDelegate: NSObject, UIWindowSceneDelegate { func scene( _ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions ) { guard let scene = scene as? UIWindowScene else { return } // ... } }
  54. UISceneDelegateͱซ༻ͯ͠࢖͏ • ΧελϜͷ UISceneDelegate Λ࢖ͬͨͱ͖ͷ஫ҙ఺ • UISceneDelegate.scene(_:willConnectTo:options:) Ͱ Key Window

    Λ ࡞Βͳ͍ • SwiftUI ଆͰࣗಈతʹ࡞ΒΕΔ • windowScene.delegate ͸ҧ͏ΫϥεʹͳΔ • ͜Ε΋ SwiftUI.AppSceneDelegate ͱ͍͏ඇެ։ϥούΫϥεͰแ ·ΕΔ͔Β
  55. iOS 13΋αϙʔτ͢Δ • iOS 14 ະຬͷରԠ͕ඞཁͳΒผͷܕΛ @main ʹͯ͠෼ذΛೖ Εͯैདྷͷ UIKit

    ϥΠϑαΠΫϧͱ SwiftUI ϥΠϑαΠΫϧʹ੾ Γସ͑Δ͜ͱ͕Մೳ • ίʔυ͕ଟগॏෳ͢Δ͕࠷௿ରԠόʔδϣϯΛ iOS 14 ·Ͱ্ ͛ͨͱ͖ʹ͸αΫοͱ࡟আ͢Δ͚ͩ
  56. iOS 13΋αϙʔτ͢Δ @MainActor class AppContainer: NSObject, ObservableObject { @ViewBuilder var

    rootView: some View { ... } @available(iOS, deprecated: 14) func launch(window: UIWindow?) { window?.rootViewController = UIHostingController( rootView: rootView ) window?.makeKeyAndVisible() } } class AppDelegate: NSObject, UIApplicationDelegate { let container = AppContainer() @available(iOS, deprecated: 14) var window: UIWindow? @available(iOS, deprecated: 14) func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { if #unavailable(iOS 14) { container.launch(window: window) } return true } }
  57. iOS 13΋αϙʔτ͢Δ struct MyAwesomeApp: App { @UIApplicationDelegateAdaptor var delegate: AppDelegate

    var body: some Scene { WindowGroup { delegate.container.rootView } } } @main @MainActor struct AppLauncher { static func main() { if #available(iOS 14, *) { MyAwesomeApp.main() } else { AppDelegate.main() } } }
  58. ʢ൪֎ʣෳ਺ͷ UIApplicationDelegate ʹରԠ • ΞϓϦͷීஈͷىಈॲཧΛεΩοϓ͍ͨ͠ͱ͖ͳͲෳ਺ͷ UIApplicationDelegate ͕࣮૷͞Ε͍ͯΔ৔߹͕͋Δ • @main ʹͳΔܕΛࣗલͰ࣮૷ͯ͠ىಈ࣌ʹ੾Γସ͑Δ͜ͱ͕Մೳ

    @main @MainActor struct AppLauncher { static func main() { func delegateClass(_ string: String) -> UIApplicationDelegate.Type? { NSClassFromString(string) as? UIApplicationDelegate.Type } if let appDelegate = delegateClass("MyAwesomeAppTests.TestAppDelegate") { appDelegate.main() } else { MyAwesomeApp.main() } } }