Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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. SwiftUIͱUIKitΛ஥ྑͤ͘͞Δ
    Mike Apurin / @auramagi

    View full-size slide

  2. • ΏΊΈͷ iOS ΤϯδχΞ

    • ࣾ಺Ͱ SwiftUI ษڧձΛ։࠵͢Δ΄Ͳ
    SwiftUI ʹ͸·͍ͬͯΔ

    • Ջͷͱ͖ͳΒίʔώʔΛᔸΕ͍ͯΔ ☕
    Mike Apurin ΞϓϦϯɾϛϋΠϧ
    auramagi
    auramagi

    View full-size slide

  3. SwiftUIΛͱʹ͔͘࢖͍͍ͨཧ༝
    • SwiftUI ͸ൃද͔Β΋͏3೥͕ܦ͔ͬͯͳΓ੒ख़͍ͯ͠Δҹ৅

    • SwiftUI Ͱ͔͠࢖͑ͳ͍෦඼ɾٕज़͕͋Δ

    • UI ߏஙͷεϐʔυΛ্͍͛ͨ

    • কདྷʹඋ͑Δ

    • ౳ʑ

    View full-size slide

  4. WWDC 2022 Platforms State of The Union
    ʮΞϓϦΛ࡞Δʹ͸SwiftͱSwiftUI͸࠷దͩʯ

    View full-size slide

  5. Ұ୒Ͱ͸ͳ͘ڞଘ͕͍͍
    • શ͘৽نͷҊ݅Ͱ͸ͳ͍ͳΒطʹ UIKit Ͱߏங͞Ε͍ͯΔ

    • SwiftUI Ͱ͸ख͕ಧ͔ͳ͍ͱ͜Ζ΋͋Δ

    • SwiftUI / UIKit ͦΕͧΕͷ௕ॴΛੜ͔͢

    View full-size slide

  6. UIKitͷதͰSwiftUIΛ࢖͏

    View full-size slide

  7. UIKitͷதͰSwiftUIΛ࢖͏
    • UIHostingController

    • SwiftUI ͷ View Λදࣔ͢Δ UIViewController

    • UIHostingCon
    fi
    guration — 🆕 iOS 16

    • UITableViewCell / UICollectionViewCell ͷ contentCon
    fi
    guration ͱͯ͠
    ઃఆͯ͠ηϧ୯ҐͰ SwiftUI ͷ View Λදࣔ

    View full-size slide

  8. UIHostingController — جຊ
    • ੜ੒ͯ͠୯ಠͰදࣔ͢Δ

    func showSwiftUIView() {


    let host = UIHostingController(


    rootView: Text("Hello iOSDC 2022!")


    )


    show(host, sender: self)


    }


    􀬂

    View full-size slide

  9. UIHostingController — جຊ
    • ੜ੒ͯ͠୯ಠͰදࣔ͢Δ

    func showSwiftUIView() {


    let host = UIHostingController(


    rootView: Text("Hello iOSDC 2022!")


    )


    show(host, sender: self)


    }


    View full-size slide

  10. UIHostingController — جຊ
    • ੜ੒ͯ͠୯ಠͰදࣔ͢Δ

    func showSwiftUIView() {


    let host = UIHostingController(


    rootView: Text("Hello iOSDC 2022!")


    )


    show(host, sender: self)


    }


    // ...


    host.rootView = Text(“ϥʔϝϯ৯΂͍ͨ")

    View full-size slide

  11. 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 ʹઃఆͨ͠஋͕൓ө͞Εͳ͍

    View full-size slide

  12. 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)


    View full-size slide

  13. 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

    ͕ؒҧ͍ͬͯΔ

    View full-size slide

  14. 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 = // ...


    }


    }


    }


    􀬂
    􀬂

    View full-size slide

  15. 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 = // ...


    }


    }


    }


    View full-size slide

  16. 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)


    }


    }


    }


    View full-size slide

  17. UIHostingController — ঢ়ଶͷ࣋ͪํ
    • UIViewController ͱ View Ͱڞ௨ͷ ObservableObject Λ
    ௨ͯ͠ঢ়ଶΛ؅ཧ͢Δ

    class HostingViewController: UIViewController {


    private let model = SharedModel()




    private var cancellables: Set = []




    override func viewDidLoad() {


    super.viewDidLoad()




    let host = UIHostingController(rootView:


    MessageView(model: model)


    )




    let label = UILabel()


    model.$message


    .sink { label.text = $0 }


    .store(in: &cancellables)




    // ...


    }


    }
    􀬂
    􀬂

    View full-size slide

  18. UIHostingController — ঢ়ଶͷ࣋ͪํ
    • UIViewController ͱ View Ͱڞ௨ͷ ObservableObject Λ
    ௨ͯ͠ঢ়ଶΛ؅ཧ͢Δ

    class HostingViewController: UIViewController {


    private let model = SharedModel()




    private var cancellables: Set = []




    override func viewDidLoad() {


    super.viewDidLoad()




    let host = UIHostingController(rootView:


    MessageView(model: model)


    )




    let label = UILabel()


    model.$message


    .sink { label.text = $0 }


    .store(in: &cancellables)




    // ...


    }


    }

    View full-size slide

  19. UIHostingController — ঢ়ଶͷ࣋ͪํ
    • ଞʹ΋ํ๏͸͋Γ·͢

    • Combine ͷ Publisher Λ౉ͯ͠ߪಡ͢Δ

    struct MessageView: View {


    let publisher: AnyPublisher


    @State private var message: String = ""


    var body: some View {


    Text(message)


    .onReceive(publisher) { message = $0 }


    }


    }


    • ϓϩύςΟɾΞΫγϣϯΛ @Environment ʹೖΕΔ

    • ڞ௨ϞσϧΛ @EnvironmentObject Ͱઃఆ͢Δ

    View full-size slide

  20. 1. ਌͕ࢠ View ʹαΠζΛఏҊ

    2. ࢠ View ͕ఏҊΛݩʹͳΓ͍ͨαΠζΛܾΊΔ

    3. ਌͕ࣗ෼ͷதͰࢠ View Λ഑ஔ

    SwiftUIϨΠΞ΢τॲཧͷ͓͞Β͍


    αΠζఏҊ ͳΓ͍ͨαΠζ

    View full-size slide

  21. • αΠζఏҊ


    struct ProposedViewSize: Equatable {


    var width: CGFloat?




    var height: CGFloat?


    }


    • nil ͷͱ͖ʹ੍໿͕ͳ͍৔߹ͷαΠζΛܭࢉ

    • .
    fi
    xedSize() Λ͔͚ͨͱ͖΍ ScrollView ʹೖΕ
    ͨͱ͖ͳͲ

    • ϏϡʔࣗମͷαΠζ͸ͳ͍ͱ͖͸ϓϨΠεϗ
    ϧμʔ஋ͷ 10 ͕ฦΔ

    SwiftUIϨΠΞ΢τॲཧͷ͓͞Β͍


    αΠζఏҊ ͳΓ͍ͨαΠζ

    View full-size slide

  22. 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ϨΠΞ΢τॲཧͷ͓͞Β͍


    αΠζఏҊ ͳΓ͍ͨαΠζ

    View full-size slide

  23. UIHostingController — ϨΠΞ΢τ
    • view.intrinsicContentSize ʹத਎ͷ SwiftUI View ͷ ideal αΠζ͕൓ө͞ΕΔ

    • View ʹมߋ͕͋ͬͯ΋͍ͭ΋ਖ਼͍͠஋ʹͳ͍ͬͯΔ

    • ͨͩ͠σϑΥϧτͰ invalidateIntrinsicContentSize() ͕ࣗಈతʹݺ͹Εͳ͍

    View full-size slide

  24. UIHostingController — ϨΠΞ΢τ
    • 🆕 iOS 16 ͔Β͸ sizingOptions Ͱࣗಈߋ৽ͷಈ࡞ΛΧελϚΠζͰ͖Δ

    • .intrinsicContentSize

    • ࣗಈతʹ UIHostingController.view.invalidateIntrinsicContentSize() ΛݺͿ

    • .preferredContentSize

    • ࣗಈతʹ UIHostingController.preferredContentSize ʹ΋αΠζΛ൓ө

    • ྆ํΛಉ࣌ʹઃఆͰ͖Δ

    • υΩϡϝϯτʹ͸ɺॲཧίετ͕ߴ͍ͱ஫ҙ͞Ε͍ͯΔ

    View full-size slide

  25. 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


    }


    }


    􀬂

    View full-size slide

  26. 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


    }


    }


    View full-size slide

  27. 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


    }

    View full-size slide

  28. UIHostingConfiguration — جຊ
    • ηϧ͚ͩͰ͸ͳ͘୯ಠͷ UIView ͷੜ੒Ͱ΋࢖͑Δʢ͔΋ʁʣ

    let view = UIHostingConfiguration { ... }.makeContentView()


    • UIHostingController Ͱ view Λ୯ಠͨ͠৔߹ͱಈ͖͕ҧ͏

    • Safe Area ͸ਖ਼͘͠൓өͤΕ͍ͯΔͬΆ͍

    • தʹ͸ UIViewControllerRepresentable ͕࢖͑ͳ͍

    • ύϑΥʔϚϯεʹཁ஫ҙɻreuse ͞ΕΔͱ͖ϨΠΞ΢τΛ࠶ߏங͢Δ͔Β 1/120
    ඵͰ׬ྃ͢Δඞཁ͕͋Δ

    View full-size slide

  29. 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)


    }


    }


    }


    • جຊతʹ৘ใݯ͸ηϧҎ֎ʹஔ͘ඞཁ͕͋Δ
    􀬂

    View full-size slide

  30. 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)


    }


    }


    }


    • جຊతʹ৘ใݯ͸ηϧҎ֎ʹஔ͘ඞཁ͕͋Δ

    View full-size slide

  31. 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)


    }


    }


    }


    • جຊతʹ৘ใݯ͸ηϧҎ֎ʹஔ͘ඞཁ͕͋Δ

    View full-size slide

  32. 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)


    }


    }


    }

    View full-size slide

  33. 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


    }


    }


    View full-size slide

  34. UIHostingConfiguration — ϨΠΞ΢τ
    • UIHostingController ͱಉ͘͡த਎ͷ SwiftUI ͷ View ͷ ideal αΠζ͕ੜ੒͞Ε
    ͨ view ͷ intrinsicContentSize ʹ൓ө

    • invalidateIntrinsicContentSize() ͸ࣗಈతʹݺ͹Εͳ͍

    • 🆕 iOS 16: UITableView / UICollectionView ͷ selfSizingInvalidation ϓϩύςΟ

    • ηϧͷαΠζ͕ߋ৽͞Εͨ͜ͱΛݕ஌ͯࣗ͠ಈ൓ө

    • tableView.performBatchUpdates { } ͳͲΛ͠ͳͯ͘΋ྑ͘ͳΔ

    • σϑΥϧτͰ༗ޮ

    • SwiftUI ʹݶΒͣಉ͡࢓૊ΈͰ Auto Layout ͷ੍໿ߋ৽ΛϐοΫΞοϓͰ͖Δ

    View full-size slide

  35. 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)


    }


    }


    }
    􀬂

    View full-size slide

  36. 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)


    }


    }


    }

    View full-size slide

  37. SwiftUIͷதͰUIKitΛ࢖͏

    View full-size slide

  38. UIViewRepresentable/UIViewControllerRepresentable — جຊ
    • UIViewRepresentable ϓϩτίϧ

    • UIView Λϥοϓͯ͠ SwiftUI ͷதͰ࢖͏

    • UIViewControllerRepresentable ϓϩτίϧ

    • UIViewController Λϥοϓͯ͠ SwiftUI ͷதͰ࢖͏

    View full-size slide

  39. 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")


    }


    }

    View full-size slide

  40. UIViewRepresentable/UIViewControllerRepresentable — جຊ
    • ϥΠϑαΠΫϧ
    ঢ়ଶߋ৽
    View Tree͔Βআڈ
    View Treeʹ௥Ճ
    makeCoordinator
    makeUIView
    updateUIView
    dismantleUIView

    View full-size slide

  41. *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()


    }


    }

    View full-size slide

  42. *Representable — ঢ়ଶͷ࣋ͪํ
    • ૒ํతͳόΠϯσΟϯάͳΒ @Binding ͳͲΛ౉͢͜ͱͰͰ͖Δ

    struct UISliderView: UIViewRepresentable {


    @Binding var value: Double


    let range: ClosedRange


    let step: Double


    class Coordinator {


    @Binding var value: Double


    var step: Double


    init(value: Binding, 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)


    }


    }

    View full-size slide

  43. *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)


    }


    }

    View full-size slide

  44. *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


    // ...


    }


    }

    View full-size slide

  45. *Representable — ঢ়ଶͷ࣋ͪํ
    • Environment ͳͲ SwiftUI ͷঢ়ଶ؅ཧ΋࢖͑·͢

    struct UILabelView: UIViewRepresentable {


    @Environment(\.scenePhase) var scenePhase


    func makeUIView(context: Context) -> UILabel { ... }


    func updateUIView(_ uiView: UILabel, context: Context) {


    uiView.text = "\(scenePhase)"


    }


    }

    View full-size slide

  46. *Representable — ঢ়ଶͷ࣋ͪํ
    • Environment ͳͲ SwiftUI ͷঢ়ଶ؅ཧ΋࢖͑·͢

    struct UILabelView: UIViewRepresentable {


    @Environment(\.scenePhase) var scenePhase


    func makeUIView(context: Context) -> UILabel { ... }


    func updateUIView(_ uiView: UILabel, context: Context) {


    uiView.text = "\(scenePhase)"


    }


    }

    View full-size slide

  47. 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?


    }


    αΠζఏҊ ͳΓ͍ͨαΠζ

    View full-size slide

  48. 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) ʹಛԽͨ͠ܭࢉ΋࣮૷Ͱ͖Δ
    ※ ίʔυ͸ΠϝʔδͰ͢

    View full-size slide

  49. UIViewControllerRepresentable — iOS 15ҎԼͷϨΠΞ΢τ
    • preferredContentSize ͕ ideal ͷαΠζʹͳΔ

    • αΠζͷ࠷௿஋ɾ࠷ߴ஋͕ઃఆ͞Εͳ͍ͷͰجຊతʹ਌ͷఏҊαΠζͷ
    ··ʹͳΔ

    • .
    fi
    xedSize() Λ͔͚Δ͜ͱͰ preferredContentSize Ͱઃఆͨ͠஋ʹ͢Δ

    View full-size slide

  50. 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

    View full-size slide

  51. 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)


    }


    }

    View full-size slide

  52. 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)


    }


    }

    View full-size slide

  53. UIViewRepresentable — iOS 15ҎԼͷϨΠΞ΢τ
    • σϑΥϧτͷϨΞ΢τॲཧ

    1. Auto Layout ͷϓϩύςΟ͔Β UIView ͕ͳΕΔαΠζΛܾఆ

    2. ͜ΕΛ΋ͱʹɺఏҊ͞ΕͨαΠζ͔ΒαΠζΛܾΊΔ

    3. UIView ͷ invalidateIntrinsicContentSize() ͕ݺ͹ΕͨΒ࠶ܭࢉ

    View full-size slide

  54. 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()


    }


    }


    View full-size slide

  55. AppϥΠϑαΠΫϧ

    View full-size slide

  56. AppϥΠϑαΠΫϧ
    • iOS 14͔Β৽͘͠௥Ճ͞ΕͨSwiftUI޲͚ͷΞϓϦϥΠϑαΠΫϧ

    • ैདྷͷ UIApplicationDelegate / UISceneDelegate ͷ୅ΘΓͱͳΔ

    @main


    struct MyAwesomeApp: App {


    var body: some Scene {


    WindowGroup {


    Text("Hello iOSDC 2022")


    }


    }


    }


    View full-size slide

  57. 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:


    // ...


    }


    }


    }


    }


    }

    View full-size slide

  58. 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ϦμΠϨΫτͳͲ


    }


    }

    View full-size slide

  59. 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 { ... }

    View full-size slide

  60. UIApplicationDelegateͱซ༻ͯ͠࢖͏
    • طଘͷ UIApplicationDelegate ͔ΒҠߦͨ͠ͱ͖ͷ஫ҙ఺

    • window ͷΞΫηεͰ͖ͳ͘ͳΔ

    • window.makeKeyAndVisible() ͷॲཧ΋ෆཁ

    • UIApplication.shared.delegate ͸ҧ͏ΫϥεʹͳΔ

    • ࣗ෼ͷΫϥε͸ SwiftUI.AppDelegate ͱ͍͏ SwiftUI ಺෦ͷඇެ։
    ϥούʔͰแ·ΕΔ͔Β

    View full-size slide

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


    // ...


    }


    }

    View full-size slide

  62. UISceneDelegateͱซ༻ͯ͠࢖͏
    • ΧελϜͷ UISceneDelegate Λ࢖ͬͨͱ͖ͷ஫ҙ఺

    • UISceneDelegate.scene(_:willConnectTo:options:) Ͱ Key Window Λ
    ࡞Βͳ͍

    • SwiftUI ଆͰࣗಈతʹ࡞ΒΕΔ

    • windowScene.delegate ͸ҧ͏ΫϥεʹͳΔ

    • ͜Ε΋ SwiftUI.AppSceneDelegate ͱ͍͏ඇެ։ϥούΫϥεͰแ
    ·ΕΔ͔Β

    View full-size slide

  63. iOS 13΋αϙʔτ͢Δ
    • iOS 14 ະຬͷରԠ͕ඞཁͳΒผͷܕΛ @main ʹͯ͠෼ذΛೖ
    Εͯैདྷͷ UIKit ϥΠϑαΠΫϧͱ SwiftUI ϥΠϑαΠΫϧʹ੾
    Γସ͑Δ͜ͱ͕Մೳ

    • ίʔυ͕ଟগॏෳ͢Δ͕࠷௿ରԠόʔδϣϯΛ iOS 14 ·Ͱ্
    ͛ͨͱ͖ʹ͸αΫοͱ࡟আ͢Δ͚ͩ

    View full-size slide

  64. 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


    }


    }

    View full-size slide

  65. 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()


    }


    }


    }


    View full-size slide

  66. ʢ൪֎ʣෳ਺ͷ 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()


    }


    }


    }

    View full-size slide

  67. ͝ਗ਼ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠

    View full-size slide