LoginSignup
0
6

More than 1 year has passed since last update.

SwiftUI画面遷移をViewから分離させて書く方法

Last updated at Posted at 2021-05-28

はじめに

記事を読んでほしい対象の方

SwiftUIでライブラリを使わずに...

  • 画面遷移のコードをViewから分離 させたい方
  • UIKitのように画面遷移したい けど, せっかくなので UIKit は使いたくない方
  • アーキテクチャ に悩んでいる方

結論

ちょっと残念ですが現時点では UIKit を使わないままで理想に近づけることはほぼ不可能だと思われます。
いくつか海外の記事も見て回りましたが、同様の質問をAppleに投げた人が見つかったものの、 Appleからの正式な回答 を持ってしても要件としてはまだ不十分でした。
参考 Stack Overflow

そこで本記事では、 UIKit のクラスを使うコードを1ファイルのみに限定し、その中身は編集せずに固定、それ以外は全て純粋な SwiftUI で書く方法を紹介します。

固定ファイル

まずはこの固定コードをそのまま丸々コピーして、プロジェクトのどこかにおいてください。 Coordinator.swift

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>


ここをクリックしてCoordinator.swift をみる
Coordinator.swift
// MARK: - Coordinator

import SwiftUI

typealias Coordinator = Navigator & ViewProvider
class Navigator {
    var navigation: Navigation = UINavigation()
}
protocol ViewProvider {
    associatedtype ViewType: View
    func view() -> ViewType
}
protocol Navigation {
    func push<C: Coordinator>(_ coordinator: C, animated: Bool)
    func set(_ coordinators: [AnyCoordinator], animated: Bool)
    func pop(animated: Bool)
    func popToRoot(animated: Bool)
}
class DefaultCoordinator: Coordinator {
    let content: ViewType
    init(@ViewBuilder content: () -> ViewType) {
        self.content = content()
    }
    func view() -> AnyView {
        self.content
    }
}
extension ViewProvider {
    func anyCoordinator<V: View>(_ view: V) -> AnyCoordinator {
        return AnyCoordinator(DefaultCoordinator {AnyView(view)} )
    }
}
class AnyCoordinator : Coordinator {
    init<C: Coordinator>(_ base: C) {
        _view = { AnyView(base.view()) }
    }
    init<V: View>(_ view: V) {
        _view = { AnyView(view) }
    }
    func view() -> AnyView {
        _view()
    }
    private let _view: () -> ViewType
}

// MARK: - Wrapped UIKit

import UIKit

struct Window<C: Coordinator>: UIViewControllerRepresentable {
    var coordinator: C
    init(coordinator: C) {
        self.coordinator = coordinator
        let hosting = UIHostingController(rootView: self.coordinator.view())
        (self.coordinator.navigation as! UINavigation).controller = UINavigationController(rootViewController: hosting)
    }

    // UIViewControllerRepresentable
    typealias UIViewControllerType = UINavigationController
    func makeUIViewController(context: UIViewControllerRepresentableContext<Window>) -> Window.UIViewControllerType {
        return (self.coordinator.navigation as! UINavigation).controller!
    }
    func updateUIViewController(_ uiViewController: Window.UIViewControllerType, context: UIViewControllerRepresentableContext<Window>) {
    }
}
private class UINavigation: Navigation {
    var controller: UINavigationController?
    func push<C: Coordinator>(_ coordinator: C, animated: Bool) {
        let hosting = UIHostingController(rootView: coordinator.view())
        (coordinator.navigation as! UINavigation).controller = self.controller
        self.controller?.pushViewController(hosting, animated: animated)
    }
    func set(_ coordinators: [AnyCoordinator], animated: Bool) {
        for c in coordinators {
            (c.navigation as! UINavigation).controller = self.controller
        }
        self.controller?.setViewControllers(coordinators.map {UIHostingController(rootView: $0.view())}, animated: animated)
    }
    func pop(animated: Bool) {
        self.controller?.popViewController(animated: animated)
    }
    func popToRoot(animated: Bool) {
        self.controller?.popToRootViewController(animated: animated)
    }
}


>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

使用コード例

iOS界で有名な Coordinator(コーディネーター) と言うパターンで遷移を書きます。
今回は簡単なサンプルなので、 ビューモデルCoordinator の役割もさせちゃいます。

ビューA

Up ボタンでカウント数を増やす。 Go ボタンを押すと、カウント数を渡しながら ビューB へ進む。

ViewA.swift
import SwiftUI

/// ビュー A
struct ViewA: View {
    /// ビューモデル は @StateObject を付ける
    @StateObject var vm: ViewModelA
    var body: some View {
        // テキスト
        Text("A counts: \(vm.count)")
        // ボタン
        Button(action: vm.up) { Text("Up") }
        // ボタン
        Button(action: vm.go) { Text("Go") }
    }
}

/// ビューモデル A
class ViewModelA: Coordinator, ObservableObject {
    // @Published この値と一緒に ビュー A の表示が変わる
    @Published var count: Int = 0

    // カウントを 1 増やす
    func up() {
        count += 1
    }

    // カウントを渡して B へ遷移 (Push)
    func go() {
        self.navigation.push(
            ViewModelB(count),
            animated: true
        )
    }

    // Coordinator に必要なメソッド
    func view() -> ViewA {
        return ViewA(vm: self)
    }
}

ビューB

ビューA からカウント数を受け取って表示する。

ViewB.swift
import SwiftUI

/// ビュー B
struct ViewB: View {
    /// ビューモデル は @StateObject を付ける
    @StateObject var vm: ViewModelB
    var body: some View {
        // テキスト
        Text("B counts: \(vm.count)")
    }
}

/// ビューモデル B
class ViewModelB: Coordinator, ObservableObject {
    let count: Int
    init(_ count: Int) { self.count = count }

    // Coordinator に必要なメソッド
    func view() -> ViewB {
        return ViewB(vm: self)
    }
}

ビューC

ビューモデル を持たないミニマムな ビュー
あとのサンプルで使います。

ViewC.swift
import SwiftUI

/// ビュー C
struct ViewC: View {
    var body: some View {
        Text("C")
    }
}

実行

xxxApp.swift(アプリによってxxxは異なる) WindowGroup の中に
Window(coordinator: ViewModelA()) を書いておきましょう。

xxxApp.swift
import SwiftUI

@main
struct xxxApp: App {
    var body: some Scene {
        WindowGroup {
            Window(coordinator: ViewModelA()) // ここを書き換えた。
        }
    }
}

無事にやりたかったことができました。
今回のコードでは他にも以下のような遷移メソッドを提供しています。
サンプルは ViewAgo() の中を変えてみてください。

サンプル

  • push : 今の画面へ戻って来られるように状態を保ったまま次の画面へ進む


ここをクリックして サンプル1 をみる
Sample_01_push.swift
...
self.navigation.push(
    ViewModelB(count),
    animated: true
)
...

  • set : いくつかの画面を繋がった状態で用意して、その一番最後の画面を表示する


ここをクリックして サンプル2 をみる
Sample_02_set.swift
...
// set を使うときは AnyCoordinator で囲んで型消去をする
self.navigation.set(
    [
        AnyCoordinator(ViewModelB(1)),
        AnyCoordinator(ViewC()),
        AnyCoordinator(ViewModelB(2)),
        AnyCoordinator(ViewC()),
        AnyCoordinator(ViewModelB(3)),
    ],
    animated: true)
...

  • pop : 今いる画面を削除しながら、前の画面に戻る


ここをクリックして サンプル3 をみる
Sample_03_pop.swift
...
self.navigation.pop(animated: true)
...

  • popToRoot : 一番最初の画面(ルート)まで戻る。


ここをクリックして サンプル4 をみる
Sample_04_popToRoot.swift
...
self.navigation.popToRoot(animated: true)
...

さいごに

長い記事になってしまいました。最後まで読んでいただきありがとうございます。
また機能の追加とか修正ができれば、記事を更新しようかと思います。
それでは、良いSwiftUIライフを!

0
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
6