Share
Embed
SwiftUIとUIKitを仲良くさせる
Mike Apurin
September 11, 2022
Transcript
67 slides
-
• ΏΊΈͷ iOS ΤϯδχΞ • ࣾͰ SwiftUI ษڧձΛ։࠵͢Δ΄Ͳ SwiftUI ʹ·͍ͬͯΔ
• Ջͷͱ͖ͳΒίʔώʔΛᔸΕ͍ͯΔ ☕ Mike Apurin ΞϓϦϯɾϛϋΠϧ auramagi auramagi -
ҰͰͳ͘ڞଘ͕͍͍ • શ͘৽نͷҊ݅Ͱͳ͍ͳΒطʹ UIKit Ͱߏங͞Ε͍ͯΔ • SwiftUI Ͱख͕ಧ͔ͳ͍ͱ͜Ζ͋Δ • SwiftUI
/ UIKit ͦΕͧΕͷॴΛੜ͔͢ -
UIKitͷதͰSwiftUIΛ͏ • UIHostingController • SwiftUI ͷ View Λදࣔ͢Δ UIViewController •
UIHostingCon fi guration — 🆕 iOS 16 • UITableViewCell / UICollectionViewCell ͷ contentCon fi guration ͱͯ͠ ઃఆͯ͠ηϧ୯ҐͰ SwiftUI ͷ View Λදࣔ -
UIHostingController — جຊ • ੜͯ͠୯ಠͰදࣔ͢Δ func showSwiftUIView() { let host
= UIHostingController( rootView: Text("Hello iOSDC 2022!") ) show(host, sender: self) } -
UIHostingController — جຊ • ੜͯ͠୯ಠͰදࣔ͢Δ func showSwiftUIView() { let host
= UIHostingController( rootView: Text("Hello iOSDC 2022!") ) show(host, sender: self) } -
UIHostingController — جຊ • ੜͯ͠୯ಠͰදࣔ͢Δ func showSwiftUIView() { let host
= UIHostingController( rootView: Text("Hello iOSDC 2022!") ) show(host, sender: self) } // ... host.rootView = Text(“ϥʔϝϯ৯͍ͨ") -
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 ʹઃఆ͕ͨ͠ө͞Εͳ͍ -
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) -
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 ͕ؒҧ͍ͬͯΔ -
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 = // ... } } } -
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 = // ... } } } -
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) } } } -
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) // ... } } -
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) // ... } } -
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 Ͱઃఆ͢Δ -
1. ͕ࢠ View ʹαΠζΛఏҊ 2. ࢠ View ͕ఏҊΛݩʹͳΓ͍ͨαΠζΛܾΊΔ 3. ͕ࣗͷதͰࢠ
View Λஔ SwiftUIϨΠΞτॲཧͷ͓͞Β͍ ࢠ αΠζఏҊ ͳΓ͍ͨαΠζ -
• αΠζఏҊ struct ProposedViewSize: Equatable { var width: CGFloat? var
height: CGFloat? } • nil ͷͱ͖ʹ੍͕ͳ͍߹ͷαΠζΛܭࢉ • . fi xedSize() Λ͔͚ͨͱ͖ ScrollView ʹೖΕ ͨͱ͖ͳͲ • ϏϡʔࣗମͷαΠζͳ͍ͱ͖ϓϨΠεϗ ϧμʔͷ 10 ͕ฦΔ SwiftUIϨΠΞτॲཧͷ͓͞Β͍ ࢠ αΠζఏҊ ͳΓ͍ͨαΠζ -
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ϨΠΞτॲཧͷ͓͞Β͍ ࢠ αΠζఏҊ ͳΓ͍ͨαΠζ -
UIHostingController — ϨΠΞτ • view.intrinsicContentSize ʹதͷ SwiftUI View ͷ ideal
αΠζ͕ө͞ΕΔ • View ʹมߋ͕͍͋ͬͯͭਖ਼͍͠ʹͳ͍ͬͯΔ • ͨͩ͠σϑΥϧτͰ invalidateIntrinsicContentSize() ͕ࣗಈతʹݺΕͳ͍ -
UIHostingController — ϨΠΞτ • 🆕 iOS 16 ͔Β sizingOptions Ͱࣗಈߋ৽ͷಈ࡞ΛΧελϚΠζͰ͖Δ
• .intrinsicContentSize • ࣗಈతʹ UIHostingController.view.invalidateIntrinsicContentSize() ΛݺͿ • .preferredContentSize • ࣗಈతʹ UIHostingController.preferredContentSize ʹαΠζΛө • ྆ํΛಉ࣌ʹઃఆͰ͖Δ • υΩϡϝϯτʹɺॲཧίετ͕ߴ͍ͱҙ͞Ε͍ͯΔ -
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 } } -
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 } } -
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 } -
UIHostingConfiguration — جຊ • ηϧ͚ͩͰͳ͘୯ಠͷ UIView ͷੜͰ͑Δʢ͔ʁʣ let view =
UIHostingConfiguration { ... }.makeContentView() • UIHostingController Ͱ view Λ୯ಠͨ͠߹ͱಈ͖͕ҧ͏ • Safe Area ਖ਼͘͠өͤΕ͍ͯΔͬΆ͍ • தʹ UIViewControllerRepresentable ͕͑ͳ͍ • ύϑΥʔϚϯεʹཁҙɻreuse ͞ΕΔͱ͖ϨΠΞτΛ࠶ߏங͢Δ͔Β 1/120 ඵͰྃ͢Δඞཁ͕͋Δ -
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) } } } • جຊతʹใݯηϧҎ֎ʹஔ͘ඞཁ͕͋Δ -
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) } } } • جຊతʹใݯηϧҎ֎ʹஔ͘ඞཁ͕͋Δ -
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) } } } • جຊతʹใݯηϧҎ֎ʹஔ͘ඞཁ͕͋Δ -
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) } } } -
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 } } -
UIHostingConfiguration — ϨΠΞτ • UIHostingController ͱಉ͘͡தͷ SwiftUI ͷ View ͷ
ideal αΠζ͕ੜ͞Ε ͨ view ͷ intrinsicContentSize ʹө • invalidateIntrinsicContentSize() ࣗಈతʹݺΕͳ͍ • 🆕 iOS 16: UITableView / UICollectionView ͷ selfSizingInvalidation ϓϩύςΟ • ηϧͷαΠζ͕ߋ৽͞Εͨ͜ͱΛݕͯࣗ͠ಈө • tableView.performBatchUpdates { } ͳͲΛ͠ͳͯ͘ྑ͘ͳΔ • σϑΥϧτͰ༗ޮ • SwiftUI ʹݶΒͣಉ͡ΈͰ Auto Layout ͷ੍ߋ৽ΛϐοΫΞοϓͰ͖Δ -
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) } } } -
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) } } } -
UIViewRepresentable/UIViewControllerRepresentable — جຊ • UIViewRepresentable ϓϩτίϧ • UIView Λϥοϓͯ͠ SwiftUI
ͷதͰ͏ • UIViewControllerRepresentable ϓϩτίϧ • UIViewController Λϥοϓͯ͠ SwiftUI ͷதͰ͏ -
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") } } -
UIViewRepresentable/UIViewControllerRepresentable — جຊ • ϥΠϑαΠΫϧ ঢ়ଶߋ৽ View Tree͔Βআڈ View TreeʹՃ
makeCoordinator makeUIView updateUIView dismantleUIView -
*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() } } -
*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) } } -
*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) } } -
*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 // ... } } -
*Representable — ঢ়ଶͷ࣋ͪํ • Environment ͳͲ SwiftUI ͷঢ়ଶཧ͑·͢ struct UILabelView:
UIViewRepresentable { @Environment(\.scenePhase) var scenePhase func makeUIView(context: Context) -> UILabel { ... } func updateUIView(_ uiView: UILabel, context: Context) { uiView.text = "\(scenePhase)" } } -
*Representable — ঢ়ଶͷ࣋ͪํ • Environment ͳͲ SwiftUI ͷঢ়ଶཧ͑·͢ struct UILabelView:
UIViewRepresentable { @Environment(\.scenePhase) var scenePhase func makeUIView(context: Context) -> UILabel { ... } func updateUIView(_ uiView: UILabel, context: Context) { uiView.text = "\(scenePhase)" } } -
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? } ࢠ αΠζఏҊ ͳΓ͍ͨαΠζ -
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) ʹಛԽͨ͠ܭࢉ࣮Ͱ͖Δ ※ ίʔυΠϝʔδͰ͢ -
UIViewControllerRepresentable — iOS 15ҎԼͷϨΠΞτ • preferredContentSize ͕ ideal ͷαΠζʹͳΔ •
αΠζͷ࠷ɾ࠷ߴ͕ઃఆ͞Εͳ͍ͷͰجຊతʹͷఏҊαΠζͷ ··ʹͳΔ • . fi xedSize() Λ͔͚Δ͜ͱͰ preferredContentSize Ͱઃఆͨ͠ʹ͢Δ -
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 -
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) } } -
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) } } -
UIViewRepresentable — iOS 15ҎԼͷϨΠΞτ • σϑΥϧτͷϨΞτॲཧ 1. Auto Layout ͷϓϩύςΟ͔Β
UIView ͕ͳΕΔαΠζΛܾఆ 2. ͜ΕΛͱʹɺఏҊ͞ΕͨαΠζ͔ΒαΠζΛܾΊΔ 3. UIView ͷ invalidateIntrinsicContentSize() ͕ݺΕͨΒ࠶ܭࢉ -
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() } } -
AppϥΠϑαΠΫϧ • iOS 14͔Β৽͘͠Ճ͞ΕͨSwiftUI͚ͷΞϓϦϥΠϑαΠΫϧ • ैདྷͷ UIApplicationDelegate / UISceneDelegate ͷΘΓͱͳΔ
@main struct MyAwesomeApp: App { var body: some Scene { WindowGroup { Text("Hello iOSDC 2022") } } } -
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: // ... } } } } } -
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ϦμΠϨΫτͳͲ } } -
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 { ... } -
UIApplicationDelegateͱซ༻ͯ͠͏ • طଘͷ UIApplicationDelegate ͔ΒҠߦͨ͠ͱ͖ͷҙ • window ͷΞΫηεͰ͖ͳ͘ͳΔ • window.makeKeyAndVisible()
ͷॲཧෆཁ • UIApplication.shared.delegate ҧ͏ΫϥεʹͳΔ • ࣗͷΫϥε SwiftUI.AppDelegate ͱ͍͏ SwiftUI ෦ͷඇެ։ ϥούʔͰแ·ΕΔ͔Β -
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 } // ... } } -
UISceneDelegateͱซ༻ͯ͠͏ • ΧελϜͷ UISceneDelegate Λͬͨͱ͖ͷҙ • UISceneDelegate.scene(_:willConnectTo:options:) Ͱ Key Window
Λ ࡞Βͳ͍ • SwiftUI ଆͰࣗಈతʹ࡞ΒΕΔ • windowScene.delegate ҧ͏ΫϥεʹͳΔ • ͜Ε SwiftUI.AppSceneDelegate ͱ͍͏ඇެ։ϥούΫϥεͰแ ·ΕΔ͔Β -
iOS 13αϙʔτ͢Δ • iOS 14 ະຬͷରԠ͕ඞཁͳΒผͷܕΛ @main ʹͯ͠ذΛೖ Εͯैདྷͷ UIKit
ϥΠϑαΠΫϧͱ SwiftUI ϥΠϑαΠΫϧʹ Γସ͑Δ͜ͱ͕Մೳ • ίʔυ͕ଟগॏෳ͢Δ͕࠷ରԠόʔδϣϯΛ iOS 14 ·Ͱ্ ͛ͨͱ͖ʹαΫοͱআ͢Δ͚ͩ -
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 } } -
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() } } } -
ʢ൪֎ʣෳͷ 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() } } }