1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

TCAを使ってUIKitとSwiftUIを連携させる

Last updated at Posted at 2023-05-09

背景

業務でTCAを始めて導入しました。業務アプリは全てSwiftで書かれており、合わせて新しい画面を作る必要もあったため、SwiftUI+TCAでの導入となりました。
そこで課題だったのが、UIKit×SwiftUIの連携をTCAでどう行うかでした。
具体的には、子View(SwiftUIのSheet)上でタップされたボタンに応じて、親(UIKit)でactionを受けて分岐し、親に定義されている既存メソッドを使用するということを実装しました。

備忘録 && 同じようにTCAを既存のUIKitプロジェクトに導入したいという方の一助になればと思いました。

バージョン

Swift: 5.7.2
Xcode: 14.2
TCA: 0.52.0

デモアプリ

  1. 親(UIKit)のボタンを押すと3つのボタンがある子(SwiftUI)がSheetで表示される
  2. 任意のボタンをタップ
  3. 子がdismissし、タップしたボタンに応じて親でAlertが表示される

まずは、子(SwiftUI×TCA)を作成します。
今回のサンプル実装部分では特に何も行わないのですっからかんです。

親にどのボタンがタップされたかを渡すためにButtonTypeというenumを作成し、タップされた時にボタンに応じたcaseを渡しています。

ChildView
import SwiftUI
import ComposableArchitecture

struct ChildView: View {
    let store: StoreOf<Child>

    var body: some View {
        WithViewStore(self.store) { viewStore in
            HStack {
                Button("a") {
                    viewStore.send(.buttonTapped(.a))
                }
                Button("b") {
                    viewStore.send(.buttonTapped(.b))
                }
                Button("c") {
                    viewStore.send(.buttonTapped(.c))
                }
            }
        }
    }
}


Store

Child
import ComposableArchitecture

struct Child: ReducerProtocol {
    struct State: Equatable {}

    enum Action: Equatable {
        case buttonTapped(ButtonType)
    }

    func reduce(into state: inout State, action: Action) -> EffectTask<Action> {
        switch action {
        case .buttonTapped:
            return .none
        }
    }
}

enum ButtonType: String {
    case a, b, c
}

では、肝心の親(UIKit)の方です。

子Viewのインスタンスを作る際にreducerをReduceで作成し、そこで子のstateとactionを受け取ることができます。(今回はstateは使用しないので、_にしてます)

子のactionに応じて分岐処理を書きます。

ParentViewController
import SwiftUI
import ComposableArchitecture

final class ParentViewController: UIViewController {
    
    @IBAction private func showChildView(_ sender: UIButton) {
        let childView = ChildView(
            store: .init(initialState: .init(),
                         reducer: Reduce { [weak self] _, action in
                                 .fireAndForget {
                                     switch action {
                                     case let .buttonTapped(type):
                                         self?.presentedViewController?.dismiss(animated: true) {
                                             self?.showAlert(type: type)
                                         }
                                     }
                                 }
                         }))
        let hostingController = UIHostingController(rootView: childView)
        present(hostingController, animated: true)
    }

    private func showAlert(type: ButtonType) {
        let alert = UIAlertController(title: type.rawValue, message: nil, preferredStyle: .alert)
        alert.addAction(.init(title: "OK", style: .default))
        present(alert, animated: true)
    }
}

ネストが深くなってしまうのがちょっと・・ですが、これが子のactionを受けて、親のViewController側のメソッドを実行するシンプルな例でした。

動作確認

ezgif.com-video-to-gif (1).gif

まとめ

これで既存のメソッドを使いつつ、UIKitとSwiftUIをTCAを使って一旦は連携できました。

親側でもTCA化すればいいのですが、時間的な制約もあり、TCAなしで書いてます。

こちらも少しずつSwiftUI×TCA化を進めていきます。

この記事が誰かの助けになれば幸いです!

もし間違いやもっといい方法があるよという方、ぜひコメントなどで教えてください!

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?