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

既存アプリにSwiftUIをどう組み込んでいくか

 既存アプリにSwiftUIをどう組み込んでいくか

WWDC Extended Tokyo 2021 #wwdctokyo
https://yj-meetup.connpass.com/event/211321

Tatsuya Tanaka

June 08, 2021
Tweet

More Decks by Tatsuya Tanaka

Other Decks in Programming

Transcript

  1. ©︎
    2021 Yahoo Japan Corporation All rights reserved.
    طଘΞϓϦʹSwiftUIΛ

    Ͳ͏૊ΈࠐΜͰ͍͔͘
    ాதୡ໵ (@tattn)
    WWDC Extended 21 (#wwdctokyo)

    View Slide

  2. ©︎
    2021 Yahoo Japan Corporation All rights reserved.
    ాத ୡ໵ / ͨͳͨͭ (@tattn)
    • CTOࣨΞϓϦ౷ׅ෦
    @tattn
    @tanakasan2525
    @tattn

    View Slide

  3. ©︎
    2021 Yahoo Japan Corporation All rights reserved.
    WWDC21

    View Slide

  4. ©︎
    2021 Yahoo Japan Corporation All rights reserved.
    ͦΖͦΖiOS 12ͷαϙʔτΛ੾ΔλΠϛϯά
    ྫ೥௨ΓͳΒWWDC21ͰiOS 15͕ൃද͞Εɺ


    9݄ࠒʹϦϦʔε͞ΕΔ
    iOS 12ͷαϙʔτΛ੾ΔΞϓϦ΋গͣͭ͠૿͍͑ͯ͘

    View Slide

  5. ©︎
    2021 Yahoo Japan Corporation All rights reserved.
    OSͷਁಁ཰
    iOS 14
    IUUQTEFWFMPQFSBQQMFDPNKQTVQQPSUBQQTUPSF
    2021೥2݄24೔ݱࡏ
    iOS 13 ͦΕҎલ
    > iOS 14 ͸ɺաڈ4೥ؒʹಋೖ͞ΕͨσόΠεͷ86%Ͱ࢖༻͞Ε͍ͯ·͢ɻ
    86% 12% 2%

    View Slide

  6. ©︎
    2021 Yahoo Japan Corporation All rights reserved.
    SwiftUIͷ࣌୅౸དྷʁ
    ɾiOS 12Λ੾ΔͱSwiftUI͕࢖͑Δʂ

    ɾଞʹ΋ศརͳAPI͕࢖͑Δ (࠷ޙͷLTͰ঺հ)
    طଘͷΞϓϦʹ΋SwiftUIΛऔΓೖΕ͍ͯ͘͜ͱ͕૿͑Δ

    Ͳ͏΍ͬͯ૊ΈࠐΜͰ͍͔͕͘՝୊

    View Slide

  7. ©︎
    2021 Yahoo Japan Corporation All rights reserved.
    SwiftUIͷҠߦํ਑
    ɾ͢΂ͯSwiftUIͰ࡞Γ௚ͯ͠ϦχϡʔΞϧ

    ɹɹˠΞϓϦͷن໛ײʹΑͬͯ͸ίετେ

    ɹɹɹ(Ծʹͦ͏͢ΔͳΒiOS 13ͷαϙʔτΛ੾ΕΔλΠϛϯά͕͓͢͢Ί)


    ɾ৽نͷXib΍Storyboard͸࡞Βͳ͍

    ɹɾ৽نը໘͸SwiftUIͰ࡞Δ


    ɹɾطଘͷը໘վम࣌ʹͦͷ෦඼ΛSwiftUIͰ࡞Γ௚͢


    ɹɹˠஈ֊తʹҠߦͭͭ͠ϝϦοτΛڗडͰ͖Δ

    View Slide

  8. ©︎
    2021 Yahoo Japan Corporation All rights reserved.
    ݸਓత͓͢͢ΊͷҠߦํ਑ (iOS 13αϙʔτ͢Δ৔߹)
    ɾ৽͍͠ը໘΍UI෦඼վम࣌ʹSwiftUIΛ࢖͏

    ɹɹɾxib΍Storyboard͸ࠓޙෛ࠴ʹͳΔ


    ɾSwiftUIͷը໘ભҠ͸Ͱ͖Δ͚ͩSwiftUIͷ࢓૊ΈΛ࢖͏

    ɹɹɾࠓޙɺSwiftUIͷ࢓૊Έʹ׬શҠߦ͢ΔίετΛݮΒ͢



    ɾSwiftUIͰ࣮ݱͰ͖ͳ͍࣌͸UIKitΛ"ہॴར༻"͢Δ


    ɾiOS 13Ͱ࢖͑ͳ͍ػೳ͸࢖͑ΔػೳͰ୅༻͢Δ

    View Slide

  9. ©︎
    2021 Yahoo Japan Corporation All rights reserved.
    ݸਓత͓͢͢ΊͷҠߦํ਑ (iOS 13αϙʔτ͢Δ৔߹)
    ɾ৽͍͠ը໘΍UI෦඼վम࣌ʹSwiftUIΛ࢖͏

    ɹɹɾxib΍Storyboard͸ࠓޙෛ࠴ʹͳΔ


    ɾSwiftUIͷը໘ભҠ͸Ͱ͖Δ͚ͩSwiftUIͷ࢓૊ΈΛ࢖͏

    ɹɹɾࠓޙɺSwiftUIͷ࢓૊Έʹ׬શҠߦ͢ΔίετΛݮΒ͢



    ɾSwiftUIͰ࣮ݱͰ͖ͳ͍࣌͸UIKitΛ"ہॴར༻"͢Δ


    ɾiOS 13Ͱ࢖͑ͳ͍ػೳ͸࢖͑ΔػೳͰ୅༻͢Δ

    View Slide

  10. ©︎
    2021 Yahoo Japan Corporation All rights reserved.
    ৽نը໘͸SwiftUIͰ࡞Δ: UIKit→SwiftUI
    let hostingController = UIHostingController(rootView: NewView())


    present(hostingController, animated: true, completion: nil)
    let hostingController = UIHostingController(rootView: NewView())


    navigationController?.pushViewController(hostingController, animated: true)
    SwiftUIͷը໘Λpresent
    SwiftUIͷը໘Λpush
    UIKit SwiftUI
    let hostingController = UIHostingController(rootView: NewView())


    self.addChild(hostingController)


    stackView.addArrangedSubview(hostingController.view)


    hostingController.didMove(toParent: self)
    UIStackViewʹSwiftUIͷUIίϯϙʔωϯτΛຒΊࠐΈ
    SwiftUI
    UIKit
    UIKit

    View Slide

  11. ©︎
    2021 Yahoo Japan Corporation All rights reserved.
    ݸਓత͓͢͢ΊͷҠߦํ਑ (iOS 13αϙʔτ͢Δ৔߹)
    ɾ৽͍͠ը໘΍UI෦඼վम࣌ʹSwiftUIΛ࢖͏

    ɹɹɾxib΍Storyboard͸ࠓޙෛ࠴ʹͳΔ


    ɾSwiftUIͷը໘ભҠ͸Ͱ͖Δ͚ͩSwiftUIͷ࢓૊ΈΛ࢖͏

    ɹɹɾࠓޙɺSwiftUIͷ࢓૊Έʹ׬શҠߦ͢ΔίετΛݮΒ͢



    ɾSwiftUIͰ࣮ݱͰ͖ͳ͍࣌͸UIKitΛ"ہॴར༻"͢Δ


    ɾiOS 13Ͱ࢖͑ͳ͍ػೳ͸࢖͑ΔػೳͰ୅༻͢Δ

    View Slide

  12. ©︎
    2021 Yahoo Japan Corporation All rights reserved.
    ݸਓత͓͢͢ΊͷҠߦํ਑ (iOS 13αϙʔτ͢Δ৔߹)
    ɾ৽͍͠ը໘΍UI෦඼վम࣌ʹSwiftUIΛ࢖͏

    ɹɹɾxib΍Storyboard͸ࠓޙෛ࠴ʹͳΔ


    ɾSwiftUIͷը໘ભҠ͸Ͱ͖Δ͚ͩSwiftUIͷ࢓૊ΈΛ࢖͏

    ɹɹɾࠓޙɺSwiftUIͷ࢓૊Έʹ׬શҠߦ͢ΔίετΛݮΒ͢



    ɾSwiftUIͰ࣮ݱͰ͖ͳ͍࣌͸UIKitΛ"ہॴར༻"͢Δ


    ɾiOS 13Ͱ࢖͑ͳ͍ػೳ͸࢖͑ΔػೳͰ୅༻͢Δ

    View Slide

  13. ©︎
    2021 Yahoo Japan Corporation All rights reserved.
    SwiftUIͷView֊૚
    struct ContentView: View {


    var body: some View {


    NavigationView {


    Button("Hello, world") {


    // viewController͕΄͍͠


    }


    }


    .navigationViewStyle(StackNavigationViewStyle())


    }


    }
    UINavigationController

    View Slide

  14. ©︎
    2021 Yahoo Japan Corporation All rights reserved.
    SwiftUIͷView֊૚
    struct ContentView: View {


    @State var viewController: UIViewController?


    var body: some View {


    NavigationView {


    Button("Hello, world") {


    // UIViewController΍UINavigationController͕औΕΔ


    _ = viewController


    _ = viewController?.navigationController


    }


    .lookUp($viewController)


    }


    .navigationViewStyle(StackNavigationViewStyle())


    }


    }


    extension View {


    func lookUp(_ value: Binding) -> some View {


    LookUpViewController(content: self) { value.wrappedValue = $0 }


    }


    }


    struct LookUpViewController: UIViewControllerRepresentable {


    let content: Content


    let lookUp: (UIViewController) -> Void


    func makeUIViewController(context: Context) -> UIViewController {


    UIHostingController(rootView: content)


    }


    func updateUIViewController(_ viewController: UIViewController, context: Context) {


    DispatchQueue.main.async {


    lookUp(viewController)


    }


    }


    }
    View֊૚ʹࣗલͷViewControllerΛࠩ͠ࠐΉͱ͋Δఔ౓ࣗ༝ʹૢΕΔ
    ※࣮ࡍʹ࢖͏৔߹͸Stateͷແବͳߋ৽Λ΋͏গ͠཈੍͢Δͱྑ͍

    View Slide

  15. ©︎
    2021 Yahoo Japan Corporation All rights reserved.
    iOS 13༻ʹϑϧεΫϦʔϯϞʔμϧ΋࣮૷Ͱ͖Δ
    struct ContentView: View {


    @State var isPresented = false


    var body: some View {


    Button("Hello, world") {


    isPresented = true


    }


    .myFullScreenCover(isPresented: $isPresented) {


    NextView()


    }


    }


    }


    struct NextView: View {


    @Environment(\.presentationMode) var presentationMode


    var body: some View {


    Button("Dismiss") {


    presentationMode.wrappedValue.dismiss()


    }


    }


    }
    extension View {


    @ViewBuilder func myFullScreenCover(


    isPresented: Binding, @ViewBuilder content: @escaping () -> Content) -> some View {


    if #available(iOS 14, *) {


    fullScreenCover(isPresented: isPresented, content: content)


    } else {


    FullScreenCover(isPresented: isPresented, content: self, destination: content)


    }


    }


    }


    struct FullScreenCover: View {


    var isPresented: Binding


    let content: Content


    let destination: () -> Destination


    @State var viewController: UIViewController? = nil


    var body: some View {


    content


    .lookUp($viewController)


    .onReceive(Just(isPresented.wrappedValue)) { isPresented in


    if isPresented {


    let controller = UIHostingController(rootView: destination())


    controller.modalPresentationStyle = .fullScreen


    viewController?.present(controller, animated: true, completion: nil)


    } else {


    viewController?.dismiss(animated: true, completion: nil)


    }


    }


    }


    }
    Ͱ͖Δ͚ͩUIKitײΛग़ͣ͞ɺࠓޙϐϡΞͳSwiftUIʹҠߦ͠΍͍͢I/Fʹ͢Δͱྑ͍

    View Slide

  16. ©︎
    2021 Yahoo Japan Corporation All rights reserved.
    Lazyܥͷ୅༻
    UITableView/UICollectionViewΛ࢖ͬͯLazyԽɺCell͸SwiftUIΛ࢖͏

    View Slide

  17. ©︎
    2021 Yahoo Japan Corporation All rights reserved.
    Lazyܥͷ୅༻
    SwiftUIͰCellͷத਎Λ࣮૷
    struct CellContent: View {


    let index: Int


    var color: Color {


    Color(hue: (1 + cos(Double(index * 10) * .pi / 180)) / 2, saturation: 0.5, brightness: 1)


    }


    var body: some View {


    VStack {


    Image(systemName: "swift")


    .padding()


    Text("SwiftUI: \(index)")


    .bold()


    .frame(width: 100)


    }


    .padding(4)


    .background(color)


    .padding(.horizontal, 4)


    }


    }

    View Slide

  18. ©︎
    2021 Yahoo Japan Corporation All rights reserved.
    Lazyܥͷ୅༻
    SwiftUIΛUICollectionViewCellͰϥοϓ
    class Cell: UICollectionViewCell {


    private weak var hostingController: UIHostingController?


    func configure(content: Content, parent: UIViewController) {


    if let controller = hostingController {


    controller.rootView = content


    return


    }


    let controller = UIHostingController(rootView: content)


    controller.view.backgroundColor = .clear


    parent.addChild(controller)


    contentView.addSubview(controller.view)


    controller.didMove(toParent: parent)


    controller.view.translatesAutoresizingMaskIntoConstraints = false


    NSLayoutConstraint.activate([


    contentView.topAnchor.constraint(equalTo: controller.view.topAnchor),


    contentView.bottomAnchor.constraint(equalTo: controller.view.bottomAnchor),


    contentView.leadingAnchor.constraint(equalTo: controller.view.leadingAnchor),


    contentView.trailingAnchor.constraint(equalTo: controller.view.trailingAnchor),


    ])


    self.hostingController = controller


    }


    }
    UICollectionViewCell

    View Slide

  19. ©︎
    2021 Yahoo Japan Corporation All rights reserved.
    Lazyܥͷ୅༻
    UICollectionViewͰSwiftUIΛϥοϓͨ͠CellΛදࣔ
    private let reuseIdentifier = "CollectionViewItem"


    final class CollectionViewController: UIViewController {


    @IBOutlet private weak var collectionView: UICollectionView!


    private lazy var dataSource = UICollectionViewDiffableDataSource(


    collectionView: collectionView


    ) { collectionView, indexPath, item in


    let cell = collectionView.dequeueReusableCell(


    withReuseIdentifier: reuseIdentifier,


    for: indexPath) as! Cell


    cell.configure(content: .init(index: item), parent: self)


    return cell


    }


    override func viewDidLoad() {


    super.viewDidLoad()


    collectionView.register(


    Cell.self,


    forCellWithReuseIdentifier: reuseIdentifier


    )


    var snapshot = NSDiffableDataSourceSnapshot()


    snapshot.appendSections([0])


    snapshot.appendItems(Array(0..<100))


    dataSource.apply(snapshot)


    }


    }

    View Slide

  20. ©︎
    2021 Yahoo Japan Corporation All rights reserved.
    ݸਓత͓͢͢ΊͷҠߦํ਑ (iOS 13αϙʔτ͢Δ৔߹)
    ɾ৽͍͠ը໘΍UI෦඼վम࣌ʹSwiftUIΛ࢖͏

    ɹɹɾxib΍Storyboard͸ࠓޙෛ࠴ʹͳΔ


    ɾSwiftUIͷը໘ભҠ͸Ͱ͖Δ͚ͩSwiftUIͷ࢓૊ΈΛ࢖͏

    ɹɹɾࠓޙɺSwiftUIͷ࢓૊Έʹ׬શҠߦ͢ΔίετΛݮΒ͢



    ɾSwiftUIͰ࣮ݱͰ͖ͳ͍࣌͸UIKitΛ"ہॴར༻"͢Δ


    ɾiOS 13Ͱ࢖͑ͳ͍ػೳ͸࢖͑ΔػೳͰ୅༻͢Δ

    View Slide

  21. ©︎
    2021 Yahoo Japan Corporation All rights reserved.
    State / StateObject / ObservedObject
    ஋มߋ࣌ʹbodyͷ࠶࣮ߦ & View͕࠶ੜ੒͞Εͯ΋஋Λอ࣋


    →View͕࠶ੜ੒͞Εͯ΋ӈล஋͸࠶ੜ੒͞Εͳ͍ ※1
    @State @State var flag = true
    @StateObject @StateObject var model = ViewModel()
    objectWillChange࣌ʹbodyͷ࠶࣮ߦ & View͕࠶ੜ੒͞Εͯ΋஋Λอ࣋


    →View͕࠶ੜ੒͞Εͯ΋ӈล஋͸࠶ੜ੒͞Εͳ͍ ※1
    ※1 ViewͷinitͰ஋Λࢀর͍ͯ͠ΔͱView࠶ੜ੒࣌ʹӈล஋ͰॳظԽޙɺ஋͕෮ݩ͞ΕΔಈ࡞ʹͳͬͯ͠·͏ͷͰ஫ҙ
    @ObservedObject @ObservedObject var model = ViewModel()
    objectWillChange࣌ʹbodyͷ࠶࣮ߦ & View͕࠶ੜ੒͞ΕΔͱ஋͕ফ͑Δ

    →View͕࠶ੜ੒͞ΕΔͱӈล஋ͷ࠶ධՁͱ୅ೖ͕ߦΘΕΔ
    (iOS 13+)
    (iOS 14+)
    (iOS 13+)

    View Slide

  22. ©︎
    2021 Yahoo Japan Corporation All rights reserved.
    State / StateObject / ObservedObject
    ஋มߋ࣌ʹbodyͷ࠶࣮ߦ & View͕࠶ੜ੒͞Εͯ΋஋Λอ࣋


    →View͕࠶ੜ੒͞Εͯ΋ӈล஋͸࠶ੜ੒͞Εͳ͍ ※1
    @State @State var flag = true
    @StateObject @StateObject var model = ViewModel()
    objectWillChange࣌ʹbodyͷ࠶࣮ߦ & View͕࠶ੜ੒͞Εͯ΋஋Λอ࣋


    →View͕࠶ੜ੒͞Εͯ΋ӈล஋͸࠶ੜ੒͞Εͳ͍ ※1
    ※1 ViewͷinitͰ஋Λࢀর͍ͯ͠ΔͱView࠶ੜ੒࣌ʹӈล஋ͰॳظԽޙɺ஋͕෮ݩ͞ΕΔಈ࡞ʹͳͬͯ͠·͏ͷͰ஫ҙ
    @ObservedObject @ObservedObject var model = ViewModel()
    objectWillChange࣌ʹbodyͷ࠶࣮ߦ & View͕࠶ੜ੒͞ΕΔͱ஋͕ফ͑Δ

    →View͕࠶ੜ੒͞ΕΔͱӈล஋ͷ࠶ධՁͱ୅ೖ͕ߦΘΕΔ
    (iOS 13+)
    (iOS 14+)
    (iOS 13+)

    View Slide

  23. ©︎
    2021 Yahoo Japan Corporation All rights reserved.
    ObservedObjectʹΑΔStateObjectͷ୅༻
    struct FooView: View {


    @State var model = ViewModel()


    var body: some View {


    FooContentView(model: model)


    }


    }


    struct FooContentView: View {


    @ObservedObject var model: ViewModel


    var body: some View {


    Text(model.value)


    }


    }
    @StateͰObservableObjectΛอ࣋
    @ObservedObjectͰมߋݕ஌

    View Slide

  24. ©︎
    2021 Yahoo Japan Corporation All rights reserved.
    StateObjectͷΑ͏ʹ࢖͑ΔDynamicProperty΋࡞ΕΔ
    @propertyWrapper


    public struct StateObservedObject: DynamicProperty {


    @State @Lazy private var object: T


    @ObservedObject private var updater = StateObservedObjectUpdater()


    @dynamicMemberLookup public struct Wrapper {


    let value: T


    let update: () -> Void


    public subscript(


    dynamicMember keyPath: ReferenceWritableKeyPath


    ) -> Binding {


    .init(


    get: { value[keyPath: keyPath] },


    set: {


    value[keyPath: keyPath] = $0


    update()


    }


    )


    }


    }


    public var projectedValue: Wrapper {


    .init(value: object, update: _updater.wrappedValue.objectWillChange.send)


    }


    public var wrappedValue: T {


    get { object }


    set { object = newValue }


    }


    public init(wrappedValue: @autoclosure @escaping () -> T) {


    self._object = State(wrappedValue: Lazy(wrappedValue))


    }


    }


    private class StateObservedObjectUpdater: ObservableObject {}
    extension StateObservedObject {


    @propertyWrapper


    private class Lazy {


    let lazyValue: () -> T


    var cached: T?


    init(_ value: @escaping () -> T) {


    lazyValue = value


    }


    var wrappedValue: T {


    get {


    if let cached = cached {


    return cached


    }


    cached = lazyValue()


    return cached!


    }


    set {


    cached = newValue


    }


    }


    }


    }
    @StateObservedObject var viewModel = ViewModel()

    View Slide

  25. ©︎
    2021 Yahoo Japan Corporation All rights reserved.
    ·ͱΊ

    View Slide

  26. ©︎
    2021 Yahoo Japan Corporation All rights reserved.
    ·ͱΊ
    ɾSwiftUIΛطଘͷΞϓϦʹ΋૊ΈࠐΜͰ͍ͬͯ஌ݟΛͨΊ·͠ΐ͏

    ɾiOS 13Λαϙʔτ͢Δ৔߹͸͔ͳΓ޻෉͕ඞཁ


    ɹɹϙΠϯτ͸ɹɾUIKit͸ہॴతʹ

    ɹɹɹɹɹɹɹɹɾI/F͸iOS 14ͷAPIʹҠߦ͠΍͘͢


    ɾWWDC21ͰSwiftUI͕Ͳ͏ਐԽ͢Δָ͔͠ΈͰ͢Ͷʂʂ

    View Slide