44
31

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 5 years have passed since last update.

UIKitのカスタムビュー側の値をSwiftUIのView側で取得する方法

Last updated at Posted at 2019-10-01

はじめに

SwiftUIだけでは用意されていないコンポーネントがあったりでUIKit使いたいなーという状況はよくあるかと思います。

そんな時は、SwiftUIで頑張らないで、UIKitで作ってしまえ...と思い、カスタムビューを作成したのですが、カスタムビュー側で保持している値SwiftUIのView側で使いたいという状況に遭遇したので、
今回はその方法を備忘録兼ねて残します。

また、今回は、SwiftUIの方にないSearchBarをUIKitで作り、UISearchBarのテキストフィールドに入力された値をSwiftUI側で取得するというサンプルで試してみます。

環境

Xcode11 GM SEED
SwiftUI
Mac OS 10.14.6(18G103)

まずXibを使ったカスタムビューを作成しておく

以下のようにカスタムビューをUIViewで定義しておきます。
今まで通りAutoLayoutはしっかり設定させます。

アウトレットでついているのは、UISearchBarとカスタムビュー自体のContentViewだけ。
そしてXib側ではLabelを追加しています。
また、見やすいようにUIKitで作成したカスタムビューは青色にしておきます。

import UIKit

// Define UIKitSearchView in the usual way to create custom views.

class UIKitSearchView: UIView {
    @IBOutlet weak var contentView: UIView!
    @IBOutlet weak var searchBar: UISearchBar!
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        commonInit()
    }
    
    private func commonInit() {
        Bundle.main.loadNibNamed("UIKitSearchView", owner: self, options: nil)
        contentView.frame = bounds
        addSubview(contentView)
        
        searchBar.placeholder = "UIKitSearchView's searchBar"
        searchBar.backgroundImage = UIImage()
    }
}
スクリーンショット 2019-10-01 20.51.39.png

これでカスタムビューの準備は終わりました。

UIViewRepresentableを準拠したSwiftUISearchViewを定義する

SwiftUIで宣言されたViewの中でUIViewを使う場合は、UIViewRepresentableプロトコルを使用したViewでラップすることが必要です。

このSwiftUISearchViewがSwiftUIで使う為のラップビューとなります。

import SwiftUI

struct SwiftUISearchView: UIViewRepresentable {
    
    func makeUIView(context: Context) -> UIKitSearchView {
        print("\(#function)")
        return UIKitSearchView()
    }

    func updateUIView(_ uiView: UIKitSearchView, context: Context) {
        print("\(#function)")
    }
}

シンプルにUIViewを表示させるだけであれば、このSwiftUISearchViewを宣言すれば終わりですが、このままではUISearchBarに入力された文字列を取得することができません。

UIViewRepresentableの定義を見てみる

UIKitSearchView側でクロージャを保持するプロパティを定義したり、強引にやればなんとかなりそうですが、スマートじゃないってことで
UIViewRepresentableのプロトコルの定義を調べてみます。

public protocol UIViewRepresentable : View where Self.Body == Never {

    /// The type of `UIView` to be presented.
    associatedtype UIViewType : UIView

    /// Creates a `UIView` instance to be presented.
    func makeUIView(context: Self.Context) -> Self.UIViewType

    /// Updates the presented `UIView` (and coordinator) to the latest
    /// configuration.
    func updateUIView(_ uiView: Self.UIViewType, context: Self.Context)

    /// Cleans up the presented `UIView` (and coordinator) in
    /// anticipation of their removal.
    static func dismantleUIView(_ uiView: Self.UIViewType, coordinator: Self.Coordinator)

    /// A type to coordinate with the `UIView`.
    associatedtype Coordinator = Void

    /// Creates a `Coordinator` instance to coordinate with the
    /// `UIView`.
    ///
    /// `Coordinator` can be accessed via `Context`.
    func makeCoordinator() -> Self.Coordinator

    typealias Context = UIViewRepresentableContext<Self>
}

@available(iOS 13.0, tvOS 13.0, watchOS 6.0, *)
@available(OSX, unavailable)
extension UIViewRepresentable where Self.Coordinator == Void {

    /// Creates a `Coordinator` instance to coordinate with the
    /// `UIView`.
    ///
    /// `Coordinator` can be accessed via `Context`.
    public func makeCoordinator() -> Self.Coordinator
}

@available(iOS 13.0, tvOS 13.0, watchOS 6.0, *)
@available(OSX, unavailable)
extension UIViewRepresentable {

    /// Cleans up the presented `UIView` (and coordinator) in
    /// anticipation of their removal.
    public static func dismantleUIView(_ uiView: Self.UIViewType, coordinator: Self.Coordinator)

    /// Declares the content and behavior of this view.
    public var body: Never { get }
}

ふむふむ。
なんだかUIViewRepresentableを継承すると、以下の実装が必要となるようです。

  • makeUIView(context:)
  • updateUIView(_:context:)
  • makeCoordinator()
  • dismantleUIView(_ uiView:coordinator:)

ただ、makeCoordinator()dismantleUIView(_ uiView:coordinator:)はextensionでデフォルト実装がされているので、定義不要みたいですね。

なんかmakeCoordinator()が使えそう

dismantleUIViewは調べたところ、表示されるUIKitビュー(およびコーディネーター)をクリーンアップするのに使うらしい。(よくわかりませんでした。)
どなたか教えていただけると嬉しいです。

ということでmakeCoordinatorがなんか使えそう。

ググってみると
なんでも、このCoordinatorを使用するとデリゲートやデータソース、ユーザーの操作に対するイベントハンドリングのようなUIKitなどで実装していた一般的な処理をSwiftUIでも実装できるらしい。

extension UIViewRepresentable where Self.Coordinator == Void {

    /// Creates a `Coordinator` instance to coordinate with the
    /// `UIView`.
    ///
    /// `Coordinator` can be accessed via `Context`.
    public func makeCoordinator() -> Self.Coordinator
}

UIViewRepresentableを継承したSelfにあるCoordinatorがなければ、デフォルト実装をするようになっているみたいなので、
つまりは、さきほど作ったSwiftUISearchViewの中にCoordinatorを作ればいいっぽい。
そして、このCoordinatorUISearchBarDelegateをつけちゃいます。

Coordinatorを作ったらmakeCoordinatorも追加して、さらにdelegateも指定させます。

struct SwiftUISearchView: UIViewRepresentable {

    func makeCoordinator() -> SwiftUISearchView.Coordinator {
        return Coordinator()
    }

    func makeUIView(context: Context) -> UIKitSearchView {
        print("\(#function)")
        let view = UIKitSearchView()
        view.searchBar.delegate = context.coordinator
        return view
    }

    func updateUIView(_ uiView: UIKitSearchView, context: Context) {
        print("\(#function)")
    }
}
extension SwiftUISearchView {
    class Coordinator: NSObject, UISearchBarDelegate {
        func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
            print("\(#function)")
        }
        
        func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
            print("\(#function)")
        }
    }
}

おー、これでCoodinatorで通知を受け取れるようになりました。

UISearchBarのテキストに入力した値を取得する

@EnvironmentObjectを使えば簡単ですが、それだと設計上良くなさそう。

なので、今回は、@State + @Bindingで実現させてみます。

おさらい

@Stateとは?

SwiftUIだとViewはstructなので、値の更新ができないけれども
@Stateを宣言すると、値を更新することができるようになるバインド用の修飾子。

@Bindingとは?

親Viewの@State attributeがついた変数などの値の更新通知を受け取れるようにするバインド修飾子のようです。(なんか使えそう...)
僕の中でこいつはまだふわっとしています。アドバイスお待ちしております

バインドさせるイメージ

1.親のContentViewでtextを保持
2.子のSwiftUISearchViewに親のContentViewで定義したtextを渡して同期させる
3.UISearchBarの変更通知を受けるのはCoordinatorなので、SwiftUISearchViewからCoordinatorへもtextを渡して同期させる

こんな感じで行けるんじゃないかと思っています。
それでは、残りをやっていきます。

親ビューを作ってtextをバインドさせる

まず親ViewであるContentView作ります。
そこで@StateでUISearchBarからの入力値を保持しておく受け皿を用意しておきます。

struct ContentView: View {
    // 親ビューで値が知りたいので、@Stateでtextを定義しておく
    @State private var text: String = DEFAULT_TEXT
    
    var body: some View {
        VStack(alignment: .center) {
            // バインドさせたいのでSwiftUISearchViewの初期化処理で突っ込む
            SwiftUISearchView(text: $text)
                .frame(height: 100.0)
            Spacer()
            VStack {
                Text("SwiftUI View")
                    .font(.title)
                    .padding(.bottom, 20)
                Text(text)
            }
            .multilineTextAlignment(.center)
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .background(Color(.blue).opacity(0.5))
            .padding()
            Spacer()
        }
    }
}

SwiftUISearchViewとCoordinatorにもtextを保持するようにプロパティと処理を追加

親ビューに続いて、SwiftUISearchView、Coordinatorにもtextをバインドさせる為に、@Bindingでプロパティを追加し、さらに初期化やデリゲート通知を受けた時の処理を追加しておきます。

struct SwiftUISearchView: UIViewRepresentable {
    @Binding var text: String
    
    func makeCoordinator() -> SwiftUISearchView.Coordinator {
        return Coordinator(text: $text)
    }
    
    func makeUIView(context: Context) -> UIKitSearchView {
        print("\(#function)")
        let view = UIKitSearchView()
        view.searchBar.delegate = context.coordinator
        return view
    }

    func updateUIView(_ uiView: UIKitSearchView, context: Context) {
        print("\(#function)")
    }
}

extension SwiftUISearchView {
    class Coordinator: NSObject, UISearchBarDelegate {
        @Binding var text: String
        
        init(text: Binding<String>) {
            _text = text
        }
        
        func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
            print("\(#function)")
            text = searchText.isEmpty ? DEFAULT_TEXT: searchText
        }
        
        func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
            print("\(#function)")
            text = DEFAULT_TEXT
            searchBar.text = nil
        }
    }
}

ここまでで一旦完成しました。
あとは実行して確認します。

結果

実行してみると無事値がSwiftUI側の親ビュー側に同期されていました。
イメージ通りです。
これは@Binding便利です。

swiftui_sample_gif

github

まとめ

今回は、UIKitのカスタムビュー側の値SwiftUIのView側で取得するという方法をやってみましたが、なんとかイメージ通りに機能したので、すっきりしました。

ただ、これが正しいのかが正直良くわかっていませんので
アドバイスやご指摘などあれば、ぜひぜひコメントお願いいたします。

SwiftUI関連は、これから色々最適解が出てくると思いますので、それまではトライアンドエラーでやってみたいと思います。

それでは、最後までお読みいただきありがとうございました。

44
31
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
44
31

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?