Closed15

[SwiftUI]Presenterを抽象化して外部から注入できるようにする

ほへとほへと

以下のProtocolをView側で使用しようとすると、エラーが出る。

protocol CommentDetailPresenterInput: ObservableObject {
    var comment: Proto_Comment { get }
    var goodButtonDisabled: Bool { get }

    func handleViewEvent(_ event: CommentDetailViewEvent)
}
import Foundation
import SwiftUI

final class CommentDetailHostingController: UIHostingController<CommentDetailView> {
    // ここの定義でエラーが出る
    init(presenter: CommentDetailPresenterInput) {
        super.init(
            rootView: CommentDetailView(
                presenter: presenter
            )
        )
    }

    @available(*, unavailable)
    @MainActor
    dynamic required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

Use of protocol 'CommentDetailPresenterInput' as a type must be written 'any CommentDetailPresenterInput'

ほへとほへと

ObservableObjectを継承すると表示されるエラーなので、詳しく調べてみる。

ほへとほへと

以下を読む。

https://developer.apple.com/documentation/combine/observableobject


概要

オブジェクトが変更される前に発行するパブリッシャーを持つオブジェクトの種類。

protocol ObservableObject : AnyObject

デフォルトでは、ObservableObject は、@Published プロパティが変更される前に変更された値を発行する objectWillChange パブリッシャーを合成します。

Combineフレームワークに基づいて、@Publishedプロパティラッパーで、データが変更されたときに通知を発生させ、@ObservedObject, @StateObjectプロパティラッパーでObservableObjectを監視する。

ほへとほへと

ObservableObjectの内部的な動きを知りたい。

https://rookie-programmer.jp/?p=194


概要

objectWillChangeパブリッシャーをプロパティとして持っており、
このパブリッシャーがsendされるタイミングは、@Publishedが付与されたプロパティが変更されたタイミングである。つまり、@Publishedを使用せずにコードを書くと以下のようになる。

final class SampleModel: ObservableObject {
	var value: Int = 0 { 
		willSet {
			objectWillChange.send()
		}
	}
}

https://developer.apple.com/documentation/combine/observableobject/objectwillchange-2oa5v

@ObservedObjectを付与するということはパブリッシャーを購読しているのと同じ感じ。

objectWillChange
    .sink {
        // 実行処理
    }
ほへとほへと

ObservableObjectがAnyObjectを継承しているので、これについても調べる。

ほへとほへと

以下を読む。

https://developer.apple.com/documentation/swift/anyobject


概要

すべてのクラスが暗黙的に準拠するプロトコル。
AnyObject は、型指定のないオブジェクトの柔軟性が必要な場合、または型指定のない結果を返すブリッジされた Objective-C メソッドとプロパティを使用する場合に使用します。

クラスの抽象化みたいな?
AnyObjectを継承していればクラスとしてみなされる感じもあるのかな?

ほへとほへと

AnyとAnyObjectについて以下を読む。

https://dev.classmethod.jp/articles/anyobject-and-any/


概要

Anyは、クラス、構造体、または列挙型のすべてのインスタンスを指します。これには関数型も含まれます。AnyObjectよりも広く利用できます。

AnyObjectは構造体などでの使用はできない。

ほへとほへと

AnyObjectを継承しているし、大丈夫なのでは、と思ったけど、エラーが出るのは何故だろう。
ちなみにジェネリクスを使用すれば、ObservableObjectを継承したプロトコルも使えるみたい。

そもそも、ジェネリクスとプロトコルの使い分けってなんだっけ、、

ほへとほへと

ジェネリクスとプロトコルの使い分けについてはChatGPTに聞いてみた。


Swiftのプロトコルとジェネリクスにはいくつかの違いがあります。それぞれの特徴を以下にまとめます。

プロトコル (Protocols)

  • 定義: プロトコルは、メソッド、プロパティ、その他の要件を定義するもので、クラス、構造体、列挙型がそれを採用することで、特定の機能を実装することを要求します。
  • 使用方法: プロトコルは、インターフェースを定義し、異なる型が共通の機能を持つことを保証するために使用されます。
  • :
    protocol Drawable {
        func draw()
    }
    
    class Circle: Drawable {
        func draw() {
            print("Drawing a circle")
        }
    }
    
    class Square: Drawable {
        func draw() {
            print("Drawing a square")
        }
    }
    
  • 適用範囲: どの型がそのプロトコルを採用しているかはコンパイル時に決まります。プロトコルを採用することで、異なる型が同じメソッドやプロパティを持つことを保証します。

ジェネリクス (Generics)

  • 定義: ジェネリクスは、関数、クラス、列挙型、構造体の動作を型に依存させずに記述するためのものです。どの型でも扱える汎用的なコードを作成するのに使用されます。
  • 使用方法: ジェネリクスは、型に依存しない再利用可能なコードを作成するために使用されます。
  • :
    func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
        let temporaryA = a
        a = b
        b = temporaryA
    }
    
    var x = 5
    var y = 10
    swapTwoValues(&x, &y)
    // x is now 10, y is now 5
    
    var a = "Hello"
    var b = "World"
    swapTwoValues(&a, &b)
    // a is now "World", b is now "Hello"
    
  • 適用範囲: ジェネリクスを使用すると、同じ関数やクラスを異なる型で使用することができます。ジェネリクスによって、型の安全性を保ちながら、コードの再利用性を高めることができます。

違いのまとめ

  1. 目的:

    • プロトコルは、異なる型が同じインターフェースを持つことを保証するために使用されます。
    • ジェネリクスは、型に依存しない再利用可能なコードを作成するために使用されます。
  2. 使用方法:

    • プロトコルは、メソッドやプロパティのセットを定義し、それを採用する型がその要件を満たすようにします。
    • ジェネリクスは、型をパラメータとして受け取り、その型に依存しない汎用的なロジックを実装します。
  3. 柔軟性:

    • プロトコルは、異なる型が同じ機能を持つことを保証するために使用されるため、異なる型間の共通インターフェースを提供します。
    • ジェネリクスは、型に依存しない汎用的なコードを作成するために使用され、同じコードが異なる型で動作することを可能にします。

これらの違いを理解することで、Swiftでのコーディングがより効果的かつ効率的になります。

ほへとほへと

ジェネリクスでプロトコルを使用するケースについても聞いてみた。


はい、ジェネリクスとプロトコルは一緒に使うことができ、特に共通のインターフェースを保証しつつ型に依存しない汎用的なコードを作成する際に役立ちます。プロトコルとジェネリクスを組み合わせることで、より強力で柔軟な設計が可能になります。

プロトコルとジェネリクスの組み合わせ

プロトコルを使ったジェネリック関数

例えば、以下のようにプロトコルを使って、共通のインターフェースを持つ型に対してジェネリックな操作を定義することができます。

protocol Drawable {
    func draw()
}

class Circle: Drawable {
    func draw() {
        print("Drawing a circle")
    }
}

class Square: Drawable {
    func draw() {
        print("Drawing a square")
    }
}

func drawShape<T: Drawable>(_ shape: T) {
    shape.draw()
}

let circle = Circle()
let square = Square()

drawShape(circle) // "Drawing a circle"
drawShape(square) // "Drawing a square"

この例では、Drawableプロトコルを定義し、それを採用したクラスCircleSquareを作成しました。ジェネリック関数drawShapeは、Drawableプロトコルに準拠した任意の型を受け取ることができます。

プロトコルとジェネリッククラス

プロトコルを使ってジェネリッククラスの要件を定義することも可能です。例えば、以下のようにジェネリッククラスを定義します。

protocol Identifiable {
    var id: String { get }
}

class User: Identifiable {
    var id: String
    var name: String

    init(id: String, name: String) {
        self.id = id
        self.name = name
    }
}

class Item<T: Identifiable> {
    var value: T

    init(value: T) {
        self.value = value
    }

    func printID() {
        print("ID: \(value.id)")
    }
}

let user = User(id: "123", name: "Alice")
let item = Item(value: user)
item.printID() // "ID: 123"

この例では、Identifiableプロトコルを定義し、それを採用したUserクラスを作成しました。ジェネリッククラスItemは、Identifiableプロトコルに準拠した型を受け取ることができ、その型のidプロパティにアクセスできます。

ジェネリクスとプロトコルの利点

  • 型安全性: プロトコルとジェネリクスを組み合わせることで、コンパイル時に型の整合性を保証し、ランタイムエラーを減らすことができます。
  • 柔軟性と再利用性: 共通のインターフェースを持つ複数の型に対して、同じジェネリックコードを再利用することができます。
  • 明確なコード: プロトコルを使用することで、コードの意図や設計が明確になり、保守性が向上します。

これらの利点により、プロトコルとジェネリクスを組み合わせて使用することは、Swiftでの柔軟かつ強力な設計パターンとなります。

ほへとほへと

わざわざジェネリクスを使わなくても、プロトコル型を使用すれば良いのでは?と聞いてみた。

protocol Identifiable {

}

class Item {
    var value: Identifiable

    init(value: Identifiable) {
        self.value = value
    }

    func printID() {
        print("ID: \(value.id)")
    }
}

確かに、プロトコルを直接型として使用することは、特にシンプルなケースでは有効です。しかし、ジェネリクスを使用することで得られる利点もあります。以下にそれぞれのアプローチの違いや利点をまとめます。

プロトコルをそのまま型として使用

protocol Identifiable {
    var id: String { get }
}

class Item {
    var value: Identifiable

    init(value: Identifiable) {
        self.value = value
    }

    func printID() {
        print("ID: \(value.id)")
    }
}

class User: Identifiable {
    var id: String
    var name: String

    init(id: String, name: String) {
        self.id = id
        self.name = name
    }
}

let user = User(id: "123", name: "Alice")
let item = Item(value: user)
item.printID() // "ID: 123"

ジェネリクスを使用

protocol Identifiable {
    var id: String { get }
}

class Item<T: Identifiable> {
    var value: T

    init(value: T) {
        self.value = value
    }

    func printID() {
        print("ID: \(value.id)")
    }
}

class User: Identifiable {
    var id: String
    var name: String

    init(id: String, name: String) {
        self.id = id
        self.name = name
    }
}

let user = User(id: "123", name: "Alice")
let item = Item(value: user)
item.printID() // "ID: 123"

プロトコル型とジェネリクスの違いと利点

プロトコル型として使用する場合の利点

  1. シンプル: シンプルなケースでは、コードが簡潔になります。
  2. 柔軟性: 異なる型のオブジェクトを一つのコンテナに簡単に入れることができます。

ジェネリクスを使用する場合の利点

  1. コンパイル時の型チェック: ジェネリクスを使用することで、コンパイル時に型が確定するため、型の安全性が向上します。プロトコル型を使う場合、動的ディスパッチが発生し、ランタイム時に型のチェックが行われるため、若干のオーバーヘッドがあります。
  2. パフォーマンス: ジェネリクスを使用することで、コンパイル時に型が具体的になるため、最適化が容易になり、パフォーマンスが向上することがあります。
  3. 型の具体性: ジェネリクスを使用することで、特定の型に対してより具体的な制約や操作を定義することができます。プロトコル型を使用する場合、型の具体的な情報が失われるため、特定の型に対する操作が難しくなることがあります。

結論

どちらのアプローチを選ぶかは、具体的な状況とニーズに依存します。簡単なケースや異なる型のオブジェクトを扱う必要がある場合は、プロトコル型を使用するのが適しているかもしれません。一方で、型の安全性やパフォーマンスを重視する場合、または特定の型に対してより具体的な操作を行いたい場合は、ジェネリクスを使用することが推奨されます。

プロトコル型とジェネリクスはどちらも有用なツールであり、適切に使い分けることで、より効果的なSwiftのコードを記述することができます。

ほへとほへと

ジェネリクスは、型を失っているわけではなくて、具体的なものは持ち続けているのか。
逆にプロトコルは具体的なものまで削ぎ落としているのか。

ObservableObjectには、削ぎ落として欲しくない理由があるのか。

ほへとほへと

ObservableObjectの中身はこんな感じ。

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol ObservableObject : AnyObject {

    /// The type of publisher that emits before the object has changed.
    associatedtype ObjectWillChangePublisher : Publisher = ObservableObjectPublisher where Self.ObjectWillChangePublisher.Failure == Never

    /// A publisher that emits before the object has changed.
    var objectWillChange: Self.ObjectWillChangePublisher { get }
}

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension ObservableObject where Self.ObjectWillChangePublisher == ObservableObjectPublisher {

    /// A publisher that emits before the object has changed.
    public var objectWillChange: ObservableObjectPublisher { get }
}

/// A publisher that publishes changes from observable objects.
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
final public class ObservableObjectPublisher : Publisher {

    /// The kind of values published by this publisher.
    public typealias Output = Void

    /// The kind of errors this publisher might publish.
    ///
    /// Use `Never` if this `Publisher` does not publish errors.
    public typealias Failure = Never

    /// Creates an observable object publisher instance.
    public init()

    /// Attaches the specified subscriber to this publisher.
    ///
    /// Implementations of ``Publisher`` must implement this method.
    ///
    /// The provided implementation of ``Publisher/subscribe(_:)-4u8kn``calls this method.
    ///
    /// - Parameter subscriber: The subscriber to attach to this ``Publisher``, after which it can receive values.
    final public func receive<S>(subscriber: S) where S : Subscriber, S.Failure == Never, S.Input == ()

    /// Sends the changed value to the downstream subscriber.
    final public func send()
}
ほへとほへと

Selfを持っていたりして、具体的な型を必要としている。
なので、プロトコルとして持つことは、解決できなくなる可能性があるのでダメなのかな。

ChatGPTにも聞いてみた。


この場合、プロトコル CommentDetailPresenterInputObservableObject プロトコルを継承しているため、ジェネリクスを使用する必要があります。その理由を詳しく説明します。

ObservableObject と型の制約

ObservableObject は、SwiftUIのためのプロトコルであり、クラスに準拠したオブジェクトが変更を通知できるようにするものです。ObservableObject プロトコルに準拠するクラスは、プロパティラッパー @Published を使用してプロパティの変更を監視します。

このようなプロトコルをプロトコル型として使用する場合、以下のようなエラーが発生することがあります:

Protocol 'CommentDetailPresenterInput' can only be used as a generic constraint because it has Self or associated type requirements

これは、ObservableObject@Published を使うために Self 型や関連型(associated type)を持っているためです。これにより、プロトコルを具体的な型として直接使用することができなくなります。

ジェネリクスを使用する理由

ジェネリクスを使用すると、コンパイル時に型が決定されるため、Self 型や関連型の問題を回避できます。具体的な型が決まることで、プロトコルの要件が正しく満たされ、型安全性が保証されます。

解決策

プロトコルをジェネリクスを使って定義することで、この問題を回避できます。以下に例を示します。

プロトコルの定義

protocol CommentDetailPresenterInput: ObservableObject {
    var comment: Proto_Comment { get }
    var goodButtonDisabled: Bool { get }

    func handleViewEvent(_ event: CommentDetailViewEvent)
}

ジェネリクスを使用したクラスの定義

class CommentDetailPresenter<T: CommentDetailPresenterInput>: ObservableObject {
    @Published var presenter: T

    init(presenter: T) {
        self.presenter = presenter
    }
}

使用例

class MyPresenter: CommentDetailPresenterInput {
    @Published var comment: Proto_Comment
    @Published var goodButtonDisabled: Bool

    init(comment: Proto_Comment, goodButtonDisabled: Bool) {
        self.comment = comment
        self.goodButtonDisabled = goodButtonDisabled
    }

    func handleViewEvent(_ event: CommentDetailViewEvent) {
        // Handle the event
    }
}

let myPresenter = MyPresenter(comment: /* Proto_Comment */, goodButtonDisabled: false)
let commentDetailPresenter = CommentDetailPresenter(presenter: myPresenter)

結論

ObservableObject プロトコルに準拠するプロトコルを使用する場合、そのプロトコルは Self 型や関連型を持つため、プロトコル型として直接使用することはできません。ジェネリクスを使用することで、これらの制約を回避し、型安全なコードを実現することができます。

ほへとほへと

ObservableObjectをジェネリクスで書く理由がとりあえずわかったので良かった🍔

このスクラップは4ヶ月前にクローズされました