プロトコルで関数の戻り値にsomeを使う方法(Swift)

## はじめに

あるプロトコル（ここでは `View` ）を返す関数を持つプロトコルを定義したいとします。

素直に書くとビルドエラーとなってしまいます。

```swift:SakatsuRouterProtocol.swift
// ❌: ビルドエラーになる
protocol SakatsuRouterProtocol {
  func settingsScreen() -> some View
}
```

ビルドエラーは以下の通りです。

```
'some' type cannot be the return type of a protocol requirement; did you mean to add an associated type?
（「some」はプロトコルの戻り値の型にできません。associated typeを追加するということですか？）
```

このビルドエラーを解決して、プロトコルで関数の戻り値に `some` を使う方法を紹介します。

## 環境

- OS：macOS Ventura 13.2
- Xcode：14.2 (14C18)
- Swift：5.7.2

## 結論

ビルドエラーに記載されている通り、 `associatedtype` を使うことで解決できます。

```diff_swift:SakatsuRouterProtocol.swift
protocol SakatsuRouterProtocol {
-   func settingsScreen() -> some View
+   associatedtype SettingsScreenType: View
+   func settingsScreen() -> Self.SettingsScreenType
}
```

このプロトコルに準拠すると、 `some View` を戻り値にできます。

```swift:SakatsuRouter.swift
extension SakatsuRouter: SakatsuRouterProtocol {
  func settingsScreen() -> some View // ⭕: `some View` を戻り値にできる
}
```

## 原因

Opaque Result Type（ORT、 `some` を戻り値としている型のこと）は、ビルド時に型が一意に決まる必要があるためです。
プロトコルは準拠する型によって戻り値の型も変わるため、使えないということです。

https://twitter.com/omochimetaru/status/1623549714720505857

## 気をつけること

`associatedtype` を使う際に気をつけるべきことを紹介します。

### 違う型になるときは同じassociatedtypeを使わない

当たり前かもしれませんが、2つ以上関数があるときに、戻り値が異なる場合は同じ `associatedtype` を使ってはいけません。

```swift:SakatsuRouterProtocol.swift
// ❌: 2つ以上の関数の戻り値に同じ `associatedtype` を使っている
protocol SakatsuRouterProtocol {
  associatedtype Screen: View
  func settingsScreen() -> Self.Screen
  func testScreen() -> Self.Screen
}
```

プロトコルの準拠時に、以下のビルドエラーが発生します。

```
Type 'SakatsuRouter' does not conform to protocol 'SakatsuRouterProtocol'
```

関数ごとに `associatedtype` を作ると解決できます。

```diff_swift:SakatsuRouterProtocol.swift
protocol SakatsuRouterProtocol {
-   associatedtype Screen: View
+   associatedtype SettingsScreen: View
+   associatedtype TestScreen: View
-   func settingsScreen() -> Self.Screen
-   func testScreen() -> Self.Screen
+   func settingsScreen() -> Self.SettingsScreen
+   func testScreen() -> Self.TestScreen
}
```

### associatedtypeの名前が重複してはいけない

`associatedtype` の名前が重複している別々のプロトコルがあるとします。

```swift
// `Screen` が重複している
protocol SakatsuRouterProtocol {
  associatedtype Screen: View
  func settingsScreen() -> Self.Screen
}

protocol TestRouterProtocol {
  associatedtype Screen: View
  func testScreen() -> Self.Screen
}
```

両方のプロトコルを1つのクラスで準拠すると、ビルドエラーとなります。

```swift:FooRouter.swift
// ❌: `associatedtype` 名が重複しているプロトコルに準拠している
extension FooRouter: SakatsuRouterProtocol {
  func settingsScreen() -> some View
}

extension FooRouter: TestRouterProtocol {
  func testScreen() -> some View
}
```

ビルドエラーの内容から原因が気づきにくいので注意です。

```
Type 'SakatsuRouter' does not conform to protocol 'SakatsuRouterProtocol'
Do you want to add protocol stubs?

Cannot find type 'view' in scope
```

`associatedtype` の名前を一意にすることで解決できます。

```diff_swift
protocol SakatsuRouterProtocol {
-   associatedtype Screen: View
-   func settingsScreen() -> Self.Screen
+   associatedtype SettingsScreen: View
+   func settingsScreen() -> Self.SettingsScreen
}

protocol TestRouterProtocol {
-   associatedtype Screen: View
-   func testScreen() -> Self.Screen
+   associatedtype TestScreen: View
+   func testScreen() -> Self.TestScreen
}
```

### associatedtypeと他の型の名前が重複してはいけない

先ほどとほぼ同じですが、 `associatedtype` と他の型で名前が重複してもビルドエラーが発生します。

```swift
// ❌: `SettingsScreen` という名前が重複している
protocol SakatsuRouterProtocol {
  associatedtype SettingsScreen: View
  func settingsScreen() -> Self.SettingsScreen
}

struct SettingsScreen: View {
  // ...
}
```

ビルドエラーの内容は先ほどと同様なので省略します。

私は `associatedtype` の末尾に `Type` を付けることで重複を回避しています。

```diff_swift
protocol SakatsuRouterProtocol {
-   associatedtype SettingsScreen: View
-   func settingsScreen() -> Self.SettingsScreen
+   associatedtype SettingsScreenType: View
+   func settingsScreen() -> Self.SettingsScreenType
}

struct SettingsScreen: View {
  // ...
}
```

## おわりに

これでプロトコルの関数の戻り値に `some` が使えるようになりました :relaxed:

この手法はSwiftUIの `View` プロトコルの `body` の戻り値で実際に使われており、それを参考にしました。

## 参考リンク

- https://twitter.com/the_uhooi/status/1623535882233073665
- [Add App module by uhooi · Pull Request #137 · uhooi/Loki](https://github.com/uhooi/Loki/pull/137)
- [Refactor router by uhooi · Pull Request #138 · uhooi/Loki](https://github.com/uhooi/Loki/pull/138)
- [[Swift] 「publicなプロトコルへの準拠」だけを外部に公開する](https://zenn.dev/en3_hcl/articles/377e9d15e323be)