0
1

【SwiftUI】UIKitのdelegateを利用する方法

Posted at

はじめに

iOSアプリ開発未経験でしたが、仕事でiOSアプリ開発プロジェクトに参画したので少しずつ勉強をやっています。
Swiftでdelegateを使う方法はなんとなく理解していたのですが、SwiftUIだとどのように使うのか苦戦したのでまとめてみました。

今回の実装内容

  1. UICalendarViewを利用してカレンダーを表示する
  2. カレンダーの日付ごとに特定の文言や画像を表示する
  3. 日付ボタンをタップした際に独自処理を行う

Simulator起動すると以下のような画面表示になる。
実装イメージ

そもそもdelegateとは何か?

あるクラスから他のクラスに処理を移譲(delegate)すること
親子関係のあるClassを例にすると、ClassA(親クラス)にメソッドだけ準備しておき、処理の詳細はClassB(子クラス)で定義することで、親クラスは子クラスを意識することなく実装でき、各子クラスごとに処理をカスタムできるということ。

delegateは本筋ではないので、またの機会に記事を書きたいと思います。

実装方法とその詳細

SwiftUIで、UIKitを利用するにはUIViewRepresentableプロトコルに準拠する必要がある。

import Foundation
import SwiftUI
import UIKit

struct CalendarView: UIViewRepresentable {
// ここに処理内容を記載
}

UIViewRepresentableプロトコルを覗いてみる

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

    /// The type of view to present.
    associatedtype UIViewType : UIView

    /// Creates the view object and configures its initial state.
    ///
    /// You must implement this method and use it to create your view object.
    /// Configure the view using your app's current data and contents of the
    /// `context` parameter. The system calls this method only once, when it
    /// creates your view for the first time. For all subsequent updates, the
    /// system calls the ``UIViewRepresentable/updateUIView(_:context:)``
    /// method.
    ///
    /// - Parameter context: A context structure containing information about
    ///   the current state of the system.
    ///
    /// - Returns: Your UIKit view configured with the provided information.
    @MainActor func makeUIView(context: Self.Context) -> Self.UIViewType

    /// Updates the state of the specified view with new information from
    /// SwiftUI.
    ///
    /// When the state of your app changes, SwiftUI updates the portions of your
    /// interface affected by those changes. SwiftUI calls this method for any
    /// changes affecting the corresponding UIKit view. Use this method to
    /// update the configuration of your view to match the new state information
    /// provided in the `context` parameter.
    ///
    /// - Parameters:
    ///   - uiView: Your custom view object.
    ///   - context: A context structure containing information about the current
    ///     state of the system.
    @MainActor func updateUIView(_ uiView: Self.UIViewType, context: Self.Context)

    /// Cleans up the presented UIKit view (and coordinator) in anticipation of
    /// their removal.
    ///
    /// Use this method to perform additional clean-up work related to your
    /// custom view. For example, you might use this method to remove observers
    /// or update other parts of your SwiftUI interface.
    ///
    /// - Parameters:
    ///   - uiView: Your custom view object.
    ///   - coordinator: The custom coordinator instance you use to communicate
    ///     changes back to SwiftUI. If you do not use a custom coordinator, the
    ///     system provides a default instance.
    @MainActor static func dismantleUIView(_ uiView: Self.UIViewType, coordinator: Self.Coordinator)

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

    /// Creates the custom instance that you use to communicate changes from
    /// your view to other parts of your SwiftUI interface.
    ///
    /// Implement this method if changes to your view might affect other parts
    /// of your app. In your implementation, create a custom Swift instance that
    /// can communicate with other parts of your interface. For example, you
    /// might provide an instance that binds its variables to SwiftUI
    /// properties, causing the two to remain synchronized. If your view doesn't
    /// interact with other parts of your app, providing a coordinator is
    /// unnecessary.
    ///
    /// SwiftUI calls this method before calling the
    /// ``UIViewRepresentable/makeUIView(context:)`` method. The system provides
    /// your coordinator either directly or as part of a context structure when
    /// calling the other methods of your representable instance.
    @MainActor func makeCoordinator() -> Self.Coordinator

    /// Given a proposed size, returns the preferred size of the composite view.
    ///
    /// This method may be called more than once with different proposed sizes
    /// during the same layout pass. SwiftUI views choose their own size, so one
    /// of the values returned from this function will always be used as the
    /// actual size of the composite view.
    ///
    /// - Parameters:
    ///   - proposal: The proposed size for the view.
    ///   - uiView: Your custom view object.
    ///   - context: A context structure containing information about the
    ///     current state of the system.
    ///
    /// - Returns: The composite size of the represented view controller.
    ///   Returning a value of `nil` indicates that the system should use the
    ///   default sizing algorithm.
    @available(iOS 16.0, tvOS 16.0, *)
    @MainActor func sizeThatFits(_ proposal: ProposedViewSize, uiView: Self.UIViewType, context: Self.Context) -> CGSize?

    typealias Context = UIViewRepresentableContext<Self>

    @available(iOS 17.0, tvOS 17.0, *)
    typealias LayoutOptions
}

長いのでまとめるとUIViewRepresentableには以下が定義されている。
@MainActor funcとして定義されている1~3は必須で実装が必要になる。

  1. makeUIView(context:)
    Viewが生成される時に実行される
  2. updateUIView(_:context:)
    Viewが更新される時に実行される
  3. makeCoordinator()
    詳細は下記に記載
  4. dismantleUIView(_ uiView:coordinator:)
    この部分は今回関係なさそうだったのでまた今度

makeCoordinator()でdelegateの処理を実現

makeCoordinator()とは

Coordinatorを作成するためのメソッド
①makeUIView()や②updateUIView()より先に呼び出されるため、ここでCoordinatorを作成することで①と②に渡されるcontextにCoordinatorが含まれる。
それを利用すればcoordinatorとやりとりが可能になる。

Coordinatorとは

UIKitで発生するイベントやデータの受け渡しをSwiftUIで管理するためのクラス

UIKitで用意されているdelegateやtarget actionを各クラスでハンドリングしたい場合、structではハンドリングができないため、このCoordinatorクラスでハンドリングを実装する。
Coordinatorクラスはデフォルトでは未実装なので必要な場合は独自実装が必要になる。

実装サンプル

UIViewRepresentableを継承した構造体は以下のような構成になる。

struct CalendarView: UIViewRepresentable {
    // 最初に呼び出される(makeUIViewより前)
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    // Viewが生成される時に実行される
    func makeUIView(context: Context) -> some UIView {
        // 処理記載
    }

    // Viewが更新される時に実行される
    func updateUIView(_ uiView: UIViewType, context: Context) {
        // 処理記載
    }

    // makeCoordinatorから呼び出される
    class Coordinator: NSObject {
        // 処理記載
    }
}

delegateなどイベントハンドリングは以下のようにCoordinatorに実装する。
今回は以下2つを実装している。

  1. CalenderViewの日付をタップした際の挙動
  2. カレンダーの日付ごとのデコレーション

delegateのハンドリングをしたい場合はNSObjectに続けて、必要なDelegateクラスを明記する。
今回はUICalendarSelectionSingleDateDelegateUICalendarViewDelegateを利用。

    // makeCoordinatorから呼び出される
    class Coordinator: NSObject, UICalendarSelectionSingleDateDelegate, UICalendarViewDelegate {
        private let parent: CalendarView

        init(_ parent: CalendarView) {
            self.parent = parent
        }
        
        // 日付ボタンを押下した時の処理
        // UICalendarSelectionSingleDateDelegateのメソッド
        func dateSelection(_ selection: UICalendarSelectionSingleDate, didSelectDate dateComponents: DateComponents?) {
            guard let dateComponents else {return}
            
            let commonUtils = CommonUtils()
            let month = commonUtils.leftZeroPadding(String(dateComponents.month!))
            let day = commonUtils.leftZeroPadding(String(dateComponents.day!))
            
            parent.selectedDate = "\(dateComponents.year!)-\(month)-\(day)"
            parent.isDisplay = true
        }
        
        // 日付ごとのカスタマイズ
        // UICalendarViewDelegateのメソッド
        func calendarView(_ calendarView: UICalendarView, decorationFor dateComponents: DateComponents) -> UICalendarView.Decoration? {
            // カレンダーの日付数分このメソッドが実行される
            // switchで日付ごとの処理をカスタムすることで日付ごとに異なった内容をカレンダーに表示できる
            print("日付:\(dateComponents.month!)-\(dateComponents.day!)")
            switch dateComponents.day {
            case 1: return .default()
            case 2: return .default(color: .red)
            case 3: return .default(color: .blue)
            case 4: return .default(color: .yellow)
            case 5: return .default(color: .green)
            case 10: return .customView {
                let label = UILabel()
                label.text = "サンプルです"
                label.font = UIFont.systemFont(ofSize: 8)
                return label
            }
            case 11: return .customView {
                let label = UILabel()
                label.text = "サンプルです"
                label.font = UIFont.systemFont(ofSize: 8)
                label.textColor = .green
                return label
            }
            default: return nil
            }
        }
    }

上記Coordinatorの実装だと以下のようなログが出力される。
Simulatorの画像キャプチャ(こちら)と照らしあわせてみて欲しいのですが、7月のカレンダーでも6月30日と8月1日~3日もちゃんと表示されていますね。

日付:6-30
日付:7-1
日付:7-2
日付:7-3
...
日付:7-31
日付:8-1
日付:8-2
日付:8-3

最後にサンプルクラスの全量を記載しておきます。
参考になれば幸いです。

import Foundation
import SwiftUI
import UIKit

struct CalendarView: UIViewRepresentable {
    @Binding var selectedDate: String
    @Binding var isDisplay: Bool
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    func makeUIView(context: Context) -> some UIView {
        let view = UICalendarView()
        // カレンダーに日付ごとのカスタマイズ設定(calendarViewメソッド実行)
        view.delegate = context.coordinator

        // 日付選択できるようにする
        let dateSelection = UICalendarSelectionSingleDate(delegate: context.coordinator)

        view.selectionBehavior = dateSelection
        return view
    }
    
    func updateUIView(_ uiView: UIViewType, context: Context) {
    }
    
    // MARK: - Coordinator
    class Coordinator: NSObject, UICalendarSelectionSingleDateDelegate, UICalendarViewDelegate {
        private let parent: CalendarView

        init(_ parent: CalendarView) {
            self.parent = parent
        }
        
        // 日付ボタンを押下した時の処理
        func dateSelection(_ selection: UICalendarSelectionSingleDate, didSelectDate dateComponents: DateComponents?) {
            guard let dateComponents else {return}
            
            let commonUtils = CommonUtils()
            let month = commonUtils.leftZeroPadding(String(dateComponents.month!))
            let day = commonUtils.leftZeroPadding(String(dateComponents.day!))
            
            parent.selectedDate = "\(dateComponents.year!)-\(month)-\(day)"
            parent.isDisplay = true
            // 日付を押下した時のカスタム処理
            print("selected date:\(dateComponents.year!)-\(month)-\(day)")
        }
        
        // カレンダーに日付ごとのカスタマイズ
        func calendarView(_ calendarView: UICalendarView, decorationFor dateComponents: DateComponents) -> UICalendarView.Decoration? {
            print("日付:\(dateComponents.day)")
            switch dateComponents.day {
            case 1: return .default()
            case 2: return .default(color: .red)
            case 3: return .default(color: .blue)
            case 4: return .default(color: .yellow)
            case 5: return .default(color: .green)
            case 10: return .customView {
                let label = UILabel()
                label.text = "サンプルです"
                label.font = UIFont.systemFont(ofSize: 8)
                return label
            }
            case 11: return .customView {
                let label = UILabel()
                label.text = "サンプルです"
                label.font = UIFont.systemFont(ofSize: 8)
                label.textColor = .green
                return label
            }
            default: return nil
            }
        }
    }
    
}

まとめ

最後にこれまで書いてきた内容を超簡単にまとめると以下のような感じ。

  1. SwiftUIでUIKitを利用するにはUIViewRepresentableプロトコルに準拠
  2. UIViewRepresentableに準拠する場合は以下の実装が必須
    makeUIView(context:)
    updateUIView(_:context:)
    makeCoordinator()
  3. delegate等のイベントハンドリングはmakeCoordinator()Coordinatorで実装

最後に

Qiita初投稿でしたが結構楽しく書くことができましたし、書いてまとめることで理解が深まると感じました。
学習も兼ねて定期的に投稿を続けていこうと思います。

参考記事

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