Swiftのextensionを利用してクラス名の衝突を解決する

iOSアプリのコードをいくつか見ていて、名前空間の扱いがKotlinとSwiftで異なり困惑する場面があったので、その解決方法に関する備忘録です。


名前空間とは

名前空間についてはe-wordsの説明が非常に分かりやすかったので、そのまま引用させていただきます。

名前空間とは、各要素に一意の異なる名前をつけなければ識別できない範囲のこと。 また、名前の集合全体を小さな空間に区切り、それぞれに異なる識別名を与えることで、その空間内では他の空間に含まれる名前の競合・衝突を意識しなくて良いようにしたもの。


名前空間の例として「中央区」が挙げられていました。こちらもそのまま引用させていただきます。

例えば、「中央区」という行政区名は全国のいくつかの自治体に存在し、それだけではどこを指すのか分からないが、「東京都中央区」「大阪市中央区」と表記すればそれぞれを識別することができる。 この「東京都」や「大阪市」が「中央区」に対する名前空間の役割を果たしている

自治体や住所のように階層的な名前空間によって全体が区切られていれば、大阪市に文脈が限られている場合は「大阪府大阪市中央区」とすべて書き下さなくても「中央区」のみでこれを指し示すことができ、東京の中央区を示したければ「東京都中央区」と名前空間を指定して対象を表すこともできる。


Swiftではグループによるクラス名の識別はできない

以降では、ViewModelのように同一名で何度も宣言するであろうクラス名の衝突について考えます。
以下のようなファイル構成のプロジェクトがあるとします。

project
├ hoge
│  │ HogeView.kt (or HogeView.swift)
│  └ HogeViewModel.kt (or HogeViewModel.swift)
└ fuga
   │ FugaView.kt (or FugaView.swift)
   └ FugaViewModel.kt (or FugaViewModel.swift)


Kotlinでは、パッケージというファイル管理の仕組みによってクラス名を識別することができます。

// HogeViewModel.kt
package project.hoge

data class ViewModel(...)
// FugaViewModel.kt
package project.fuga

data class ViewModel(...)


このとき、HogeViewFugaView側でそれぞれ使用したいViewModelの所属するパッケージ名をimportで指定することで、クラス名を識別することができます。
(今回の例ではデフォルトで追加されるパッケージ名を変更しない限りは同一のパッケージに所属することになるので、importを明示的に指定する必要はありません。)

// HogeView.kt
package project.hoge

import android.content.Context
import android.util.AttributeSet
import android.view.View

class HogeView @JvmOverloads constructor(
  context: Context, 
  attrs: AttributeSet? = null, 
  defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

  ...

  // HogeView.ViewModelと同義
  fun configure(item: ViewModel) {
    ...
  }

  ...

}


Swiftでは、ファイル管理の仕組みとしてグループというものがありますが、グループを利用してもKotlinのような方法でクラス名の識別をすることはできません。

// HogeViewModel.swift
struct ViewModel {
  ...
}
// FugaViewModel.swift
struct ViewModel {
  ...
}


上記のような構成にしようとすると、いずれかのViewModelに対してInvalid redeclaration of 'ViewModel'と表示され、コンパイルエラーとなります。


Nested Typeによる解決

このようなとき、SwiftではNested Type、つまり型を入れ子にして宣言することでクラス名を識別することができます。

// HogeView.swift
import UIKit

class HogeView: UIView {

  ...

  // HogeView.ViewModelと同義
  func configure(_ item: ViewModel) {
    ...
  }

  ...

  struct ViewModel {
    ...
  }

}


しかし、上記の方法では大元となるHogeViewFugaViewの中に入れ子にしてクラスを追加していくことになるので、場合によっては1ファイルあたりのコード量がどんどん増えてしまいます。


extensionの利用による解決

この問題はextensionを利用することで解決できます。
以下のようにすることで、大元となるHogeViewFugaViewとは別のファイルの中にクラスを追加することができます。

// HogeViewModel.swift
extension HogeView {
  struct ViewModel {
    ...
  }
}
// FugaViewModel.swift
extension FugaView {
  struct ViewModel {
    ...
  }
}


上記のクラスは先ほどのNested Typeと同じ形で扱うことができます。
extensionを利用したNested Typeの宣言については、公式ドキュメントにも記載があります。

docs.swift.org

// HogeView.swift
import UIKit

class HogeView: UIView {

  ...

  // HogeView.ViewModelと同義
  func configure(_ item: ViewModel) {
    ...
  }

  ...

}