Swiftに限らずプログラミングの世界では、ユーザーが複雑な内部の処理や状態を気にせず、不変な抽象部分のみで実装を進める、いわゆる「カプセル化」というパターンがよく用いられます。
その際に重要になってくるのが、どのコード部分はアクセス可能、または不可能かという「Access Control」であり、Swiftの重要な特徴の一つに挙げられるものです。
この記事ではAccess Controlの概要やメリット、実際の使用法について紹介します。
SwiftのAccess Controlとは
クラス内で宣言する変数(プロパティ)、メソッド、イニシャライザ、内部クラスがどこからアクセス可能なものかを表す修飾子です。
Swiftでは、以下のようなアクセスレベルの異なる5種類の修飾子が用意されています。
下に行くほど制限が緩くなります。
- private
- 同一ファイルであり、かつそのクラスの中でしか使えない変数、構造体に用いる
- fileprivate
- 同一ファイルであればどこからでも使える変数、構造体に用いる
- ファイル中で密接に関係しているが、他の部分からのアクセスは制限したい場合に用いられる
- internal (default)
- 同一モジュール内であればどこでも使える変数、構造体に用いる
- public
- モジュールをimportしていればどこでも使える変数、構造体に用いる
- open
- モジュールをimportしていればどこでも使える変数、構造体に用いられ、さらに継承やoverrideなどの上書きもできるようになる
Access Controlのメリット
- 実装において余計なコードが間違った変数やクラスにアクセスするのを防ぎ、わかりやすいコンパイルエラーをうまく吐き出してくれるメリットがある
- コード同士のアクセスが適切に制限されることでコード同士が疎結合になり、メンテナンスのしやすいコードになる
実際の使用例
ここでは、実際にView + ViewModelの構成で、各ボタンを押すとアクセスレベルの違う値にlabelが置き換わるアプリの作成を通じて、使用法を見ていきたいと思います。
共通
propertyであれば、private(set) var…のように、任意でgetter, setterどちらのアクセス制限かを明示することができる。
final class AccessControlViewModel {
var internalResult = "internal" // クラス外部からアクセス可能な変数
private(set) var privateSetResult = "This is private!" // 値のgetは外部から可能だが、setはこのクラス内でしかできない。
}
1. private
例えば、ViewModelの内部の状態変数など、外部から変更させたくないコードに使います。
// AccessControlViewModel.swift
final class AccessControlViewModel {
var internalResult = "internal"
private(set) var privateSetResult = "This is private!"
}
// AccessControlViewController.swift
import UIKit
final class AccessControlViewController: UIViewController {
var viewModel: AccessControlViewModel!
@IBOutlet var resultLabel: UILabel!
@IBOutlet var privateButton: UIButton! {
didSet {
privateButton.addAction(.init {[weak self] _ in
guard let self else { return }
// ここでコンパイルエラー
self.viewModel.privateSetResult = "altered" // Cannot assign to property: 'privateSetResult' setter is inaccessible
self.resultLabel.text = self.viewModel.privateSetResult
},
for: .touchUpInside)
}
}
@IBOutlet var internalButton: UIButton! {
didSet {
internalButton.addAction(.init {[weak self] _ in
guard let self else { return }
self.resultLabel.text = self.viewModel.internalResult
},
for: .touchUpInside)
}
}
}
以下のように変更する必要があります。
// AccessControlViewModel.swift
final class AccessControlViewModel {
...
func changeResult() {
privateSetResult = "altered"
}
}
// AccessControlViewController.swift
import UIKit
final class AccessControlViewController: UIViewController {
...
@IBOutlet var privateButton: UIButton! {
didSet {
privateButton.addAction(.init {[weak self] _ in
guard let self else { return }
self.viewModel.changeResult() // <-- 変更
self.resultLabel.text = self.viewModel.privateSetResult
},
for: .touchUpInside)
}
}
}
2. fileprivate
こちらが使われるのは、例えばファイル内で共通に使われるクラス(テストでのMockクラスなど)内のメソッドなど、ファイル中では共有可能だがその他の場所では使えなくしたい場合です。
// AccessControlViewModel.swift
final class AccessControlViewModel {
...
func changeResultFromFileprivate() {
// こちらはOK
privateSetResult = SecretClass().secretFileprivate
// コンパイルエラー
privateSetResult = SecretClass().secretPrivate // 'secretPrivate' is inaccessible due to 'private' protection level
}
}
final class SecretClass {
private let secretPrivate = "private"
fileprivate let secretFileprivate = "fileprivate"
}
// AccessControlViewController.swift
final class AccessControlViewController: UIViewController {
...
@IBOutlet var fileprivateButton: UIButton! {
didSet {
fileprivateButton.addAction(.init {[weak self] _ in
guard let self else { return }
// ここでコンパイルエラー
print(SecretClass().secretFileprivate) // 'secretFileprivate' is inaccessible due to 'fileprivate' protection level
self.viewModel.changeResultFromFileprivate()
self.resultLabel.text = self.viewModel.privateSetResult
},
for: .touchUpInside)
}
}
}
fileprivateはちゃんと同じファイル内でのみアクセス可能なことがわかります。
3. internal
internalはデフォルトの状態で、宣言に何も修飾子をつけなかったら自動的にこのアクセスレベルになります。
そのため、あえて明示することはほぼ、ありません。
試しにいずれかのクラスにつけてみましょう。
// このように明示しても結果は変わらない。
internal final class AccessControlViewModel {
...
}
4. public
ここから先はmoduleを複数持つ場合に使用されるものです。
XcodeのFile > New > Packageから、ターゲットを作成したアプリに合わせて任意の名前のパッケージを作成します。ここではSecretModule
というモジュールを作成したものとします。
※TARGETS > アプリ名 > Frameworks, Libraries, and Embedded ContentへのSecret Module
の追加も忘れずに。
// SecretModule.swift
class SecretModuleClass {
var secretModuleValue: String
init() {
self.secretModuleValue = "moduleSecret"
}
}
// AccessControlViewModel.swift
import SecretModule
final class AccessControlViewModel {
...
func changeResultFromAnotherModule() {
// コンパイルエラー
privateSetResult = SecretModuleClass().secretModuleValue // Cannot find 'SecretModuleClass' in scope
}
}
// AccessControlViewController.swift
final class AccessControlViewController: UIViewController {
...
@IBOutlet var publicButton: UIButton! {
didSet {
publicButton.addAction(.init {[weak self] _ in
guard let self else { return }
self.viewModel.changeResultFromAnotherModule()
self.resultLabel.text = self.viewModel.privateSetResult
},
for: .touchUpInside)
}
}
}
このように別moduleをimportして、そこにアクセスするためには、該当箇所をpublicにする必要があります。
public class SecretModuleClass {
public var secretModuleValue: String
public init() {
self.secretModuleValue = "moduleSecret"
}
}
これでエラーが出なくなりました。
5. open
モジュールのクラスやメソッドを継承して、自分たちのアプリの仕様に合わせて上書きしたい時に使われます。
例えば、AccessControlViewModel
のSecretClass
を、SecretModuleClass
から継承したいとします。
// AccessControlViewModel.swift
import SecretModule
final class AccessControlViewModel {
...
func changeResultFromInheritedClassFromModule() {
privateSetResult = SecretClass().secretModuleValue
}
}
// コンパイルエラー
final class SecretClass: SecretModuleClass { // Cannot inherit from non-open class 'SecretModuleClass' outside of its defining module
private let secretPrivate = "private"
fileprivate let secretFileprivate = "fileprivate"
override init() {
super.init()
self.secretModuleValue = "overridden secret"
}
}
// AccessControlViewController.swift
final class AccessControlViewController: UIViewController {
...
@IBOutlet var openButton: UIButton! {
didSet {
openButton.addAction(.init {[weak self] _ in
guard let self else { return }
self.viewModel.changeResultFromInheritedClassFromModule()
self.resultLabel.text = self.viewModel.privateSetResult
},
for: .touchUpInside)
}
}
}
ここでmoduleを以下のように変更します。
open class SecretModuleClass { // <-- classの修飾子をopenに変更
public var secretModuleValue: String
public init() {
self.secretModuleValue = "moduleSecret"
}
}
これでクラスの継承ができるようになり、コンパイルエラーが出なくなりました。
以上で各ボタンを押すと、それぞれのアクセスレベルをクリアして値が取得され、labelの内容が更新されるアプリの完成です。
まとめ
- Access Controlによって、コード同士がどのように関わり合っているのか明確になり、さらにお互いが疎結合になる。
- SwiftにおけるAccess Controlは以下の5種類。
- private
- 同一ファイルであり、かつそのクラスの中でしか使えない
- fileprivate
- 同一ファイルであればどこからでも使える
- internal (default)
- 同一モジュール内であればどこでも使える
- public
- モジュールをimportしていればどこでも使える
- open
- モジュールをimportしていればどこでも使え、さらに継承やoverrideなどの上書きも行うことができる
- private
最後に
こちらは私が書籍で学んだ学習内容をアウトプットしたものです。
わかりにくい点、間違っている点等ございましたら是非ご指摘お願いいたします。