Swiftでは、クラス、構造体、列挙型にプロパティを持たせることができます。プロパティとはこれらの型に関連づけられた属性のことです。
プロパティには、保持型プロパティ(Stored Properties)と計算型プロパティ(Computed Properties)の2種類があります。保持型プロパティは、プロパティ値そのものを値として保持するもので、計算型プロパティは、アクセスされた時に計算した結果を返すものです。
保持型プロパティ
次の例は、クラスに保持型プロパティを持たせたものです。変数、定数どちらでも持たせることができます。
class Person {
let name: String // 名前
var age: Int // 年齢
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
定数のプロパティは、初期値を設定した後は変更することはできません。
var p = Person(name: "山田太郎", age: 25)
p.age = 30
p.name = "鈴木花子" // エラー
保持型プロパティの宣言の前にlazyというキーワードをつけると、遅延評価させることができます。
例えば、lazy var hoge: Hoge と宣言すると、hogeというプロパティに実際にアクセスがあるまで、hogeは生成されません。これは、生成にコストがかかるようなインスタンスで、実際にそれが使用されるとは限らないような場合に指定すると、パフォーマンスの向上が期待できます。
class Hoge {
var text: String
init() {
// ファイルを読んだり、ネットワークを調べたりコストのかかる処理
text = "HogeHoge"
}
}
class Fuga {
lazy var hoge = Hoge()
}
let fuga = Fuga() // この時点ではまだfuga.hogeは生成されない
print(fuga.hoge.text) // ここでfuga.hogeが生成される
定数プロパティにlazyを指定することできません。定数はイニシャライザの完了までに値が設定されている必要があるので、lazyを指定する意味がありません。
Swiftでは関数も型を持ったオブジェクトなのでプロパティとして代入することができます。(このようなオブジェクトを第一級オブジェクトと呼びます。)
次の例では、元号クラスに、現在の元号を返す関数をプロパティとして割り当てています。また、遅延評価させるためにlazyを指定しています。
/// 元号情報クラス
class EraInfo {
// 今の年号を返す
lazy var currentEra: () -> String = {
let dateFormatter = DateFormatter();
dateFormatter.dateFormat = "Y/M/d"
let today = Date()
let startDays = [
("1989/1/8", "平成"),
("1926/12/25", "昭和"),
("1912/7/30", "大正"),
("1868/10/23", "明治")
]
for day in startDays {
if today.compare(dateFormatter.date(from: day.0)!) == ComparisonResult.orderedDescending {
return day.1
}
}
return "不明"
}
}
let info = EraInfo()
print(info.currentEra()) // 平成
プロパティなので後から内容を変えることもできます。
info.currentEra = {
return "平成" // 常に平成
}
計算型プロパティ
計算型プロパティは、値を保持せずに、他のプロパティの値から都度計算して結果を返したり、設定したりするプロパティです。
値の返却と設定に、get(ゲッター)、set(セッター)を使います。次の例では、ageというプロパティにアクセスすると生年月日から年齢を算出して返してます。また、ageを設定した時に、生年月日と計算が合わない場合は生まれ年を設定し直しています。
class Person {
var birthday: Date // 生年月日
var age: Int { // 年齢
get {
let components = self.calendar.dateComponents(
[Calendar.Component.year],
from: birthday, to: Date()
)
return components.year!
}
set(newAge) {
let diff = self.age - newAge
if diff != 0 {
self.birthday = self.calendar.date(byAdding: Calendar.Component.year, value: diff, to: self.birthday)!
}
}
}
let dateFormatter: DateFormatter // 日付フォーマッタ
let calendar: Calendar // カレンダー
// イニシャライザ
init(birthday: String) {
self.dateFormatter = DateFormatter();
self.dateFormatter.dateFormat = "Y/M/d"
self.calendar = Calendar.current
self.birthday = self.dateFormatter.date(from: birthday)!
}
}
let p = Person(birthday: "1980/12/31")
print(p.age)
p.age = p.age - 2
print(p.birthday) // 1982/12/31
上の例では、セッターにnewAgeという引数名を与えていますが、引数名を与えずに、newValueで、設定値を取得することもできます。
set {
let diff = self.age - newValue
if diff != 0 {
self.birthday = self.calendar.date(byAdding: .year,
value: diff,
to: birthday)!
}
}
セッターを提供しないことでリードオンリーなプロパティにすることもできます。ゲッターのみの場合は、getも不要です。
class Person {
var birthday: Date // 生年月日
var age: Int { // 年齢
let components = self.calendar.dateComponents([Calendar.Component.year], from: birthday, to: Date())
return components.year!
}
let dateFormatter: DateFormatter // 日付フォーマッタ
let calendar: Calendar // カレンダー
// イニシャライザ
init(birthday: String) {
self.dateFormatter = DateFormatter();
self.dateFormatter.dateFormat = "Y/M/d"
self.calendar = Calendar.current
self.birthday = dateFormatter.date(from: birthday)!
}
}
プロパティ監視
プロパティ値が設定される前と設定された後に処理を行うことができます。この機能を プロパティ監視 と呼びます。
値が変更される前に、willSet()が呼ばれます。また、値が変更された後に、didSet()が呼ばれます。
import Foundation
/// パーソンクラス
class Person {
let name: String
var age: Int = 0 {
willSet {
print("\(self.age)歳から\(newValue)歳へ年齢を変更します。")
}
didSet {
print("\(oldValue)歳から\(self.age)歳へ年齢を変更しました。")
}
}
// イニシャライザ
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
let p = Person(name: "山田太郎", age: 30)
p.age = 31
/*
30歳から31歳へ年齢を変更します。
30歳から31歳へ年齢を変更しました。
*/
プロパティ監視は、イニシャライザ内からの変更時には呼び出されません。上の例では、init()の中で、self.age としていますが、ここではwillSet()もdidSet()も呼び出されていません。インスタンスの生成が終わって、p.age = 31 としている所で初めて呼び出されています。
didSetの中でそのプロパティに別の値を設定すると上書きされます。willSet()やdidSet()が再度呼ばれることはありません。
上の例のように、willSetに引数を与えない場合は、newValueで新しい値を参照できます。同様にdidSetに引数を与えない場合は、oldValueで、変更前の値を参照できます。
セッターやゲッターの場合と同様、次の様に引数を与えてその名前を使って参照することもできます。
var age: Int = 0 {
willSet(newAge) {
print("\(self.age)歳から\(newAge)歳へ年齢を変更します。")
}
didSet(oldAge) {
print("\(oldAge)歳から\(self.age)歳へ年齢を変更しました。")
}
}
CocoaやUIKitを使ったアプリケーションの開発では、デリゲートパターンを多用します。
デリゲートパターンは、イベントに応じて他のオブジェクトにイベントを通知したり処理を委譲するパターンですが、プロパティ監視を使うと、値の変更に応じたイベントの通知を簡単に実装することができます。
/// バーソンプロトコル
protocol PersonDelegate {
func person(_ person: Person, willChangeAge age:Int)
func person(_ person: Person, didChangeAge age:Int)
}
/// パーソンクラス
class Person {
var delegate: PersonDelegate?
let name: String
var age: Int = 0 {
willSet(newAge) {
delegate?.person(self, willChangeAge: newAge)
}
didSet(oldAge) {
delegate?.person(self, didChangeAge: oldAge)
}
}
/// イニシャライザ
///
/// - Parameters:
/// - name: 名前
/// - age: 年齢
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
class SomeClass: PersonDelegate {
init() {
let p = Person(name: "山田太郎", age: 30)
p.delegate = self
print("ageをセットする")
p.age = 31
print("ageをセットした")
}
func person(_ person: Person, willChangeAge age: Int) {
print("willChangeAge")
}
func person(_ person: Person, didChangeAge age: Int) {
print("didChangeAge")
}
}
let someClass = SomeClass()
プロパティ監視は、遅延評価型のプロパティでは使用できません。また、計算型プロパティにも使用できません。計算型プロパティではセッターに直接処理を書けるからです。但し、計算型プロパティをもつクラスを継承したサブクラスでは、親クラスの計算型プロパティの監視は可能です。
配列やディクショナリ型のプロパティに対するプロパティ監視は、配列やディクショナリ全体の代入のみでなく、要素を追加したり削除する場合も呼ばれます。
class Person {
let name: String
var friends: [Person]? {
willSet {
for p in newValue! {
print(p.name)
}
}
}
init(_ name: String) {
self.name = name
}
}
let p = Person("ルフィ")
p.friends = [Person("ゾロ"), Person("ナミ"), Person("ウソップ")]
/* 実行結果
ゾロ
ナミ
ウソップ
/
p.friends!.append(Person("サンジ"))
/ 実行結果
ゾロ
ナミ
ウソップ
サンジ
/
p.friends!.remove(at: 2)
/ 実行結果
ゾロ
ナミ
サンジ
*/
# 型プロパティ
クラスや構造体のインスタンスではなく、クラスや構造体、列挙型そのものにプロパティを持たせることができます。他の言語ではクラスのこのようなプロパティをクラス変数と呼んだり、静的メンバ変数と呼んだりします。
構造体と列挙型については、保持型プロパティと計算型プロパティのどちらでも持たせることができます。クラスについては、計算型プロパティのみ持たせられます。また、クラスや構造体自体のイニシャライザが無いため、保持型プロパティは宣言時に初期値を与える必要があります。
構造体の型プロパティは、次のようにstaticをつけて宣言します。
```swift
/// ログイン情報
struct LoginInfo {
static var url = "https://login.example.com/"
static var port: Int {
if self.url.hasPrefix("https") {
return 443
} else {
return 80
}
}
var userid: String
var password: String
}
print(LoginInfo.url) // https://login.example.com/
print(LoginInfo.port) // 443
列挙型の型プロパティも同様です。
/// 曜日
enum DayOfWeek: Int {
static var firstDay: DayOfWeek {
return .Sunday
}
static var lastDay: DayOfWeek {
return .Saturday
}
case Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday
}
print(DayOfWeek.firstDay.rawValue) // 0
print(DayOfWeek.lastDay.rawValue) // 6
クラスの型プロパティは上の2つと異なり、classをつけて宣言します。
/// パーソンクラス
class Person {
class var tableName: String {
return "people" // DBテーブル名
}
var name: String
var address: String
var tel: String
var email: String
init(name: String, address: String, tel: String, email: String) {
self.name = name
self.address = address
self.tel = tel
self.email = email
}
}
print(Person.tableName) // people
Swift1.2から、クラスのクラス変数もstaticで宣言できるようになりました。
/// パーソンクラス
class Person {
static var tableName = "people" // DBテーブル名
var name: String
var address: String
var tel: String
var email: String
init(name: String, address: String, tel: String, email: String) {
self.name = name
self.address = address
self.tel = tel
self.email = email
}
}
print(Person.tableName) // people
型フロパティはインスタンスの属性としてアクセスすることはできません。あくまでも型名.プロパティ名という形式でアクセスします。
```swift
/// ログイン情報
struct LoginInfo {
static var url = "https://login.example.com/"
let userID: String
let password: String
}
print(LoginInfo.url)
var loginInfo = LoginInfo(userID: "hogehoge", password: "abcd123")
print(loginInfo.url) // エラー