var
にしてるのにプロパティ変更できない...
@propertyWrapper
や didSet
の使い所がイマイチわからない...
この記事では、
-
let
/var
と 値型 / 参照型 の関係性 - 計算プロパティ・プロパティオブザーバ・プロパティラッパーの挙動と応用
について実例コードとともにまとめていきます。
私は初心者なので、間違いあれば指摘していただけると助かります。
let
とvar
,そして値型と参照型
struct
,値型のインスタンスを変更してみる
struct StructSample{
var variable:String = "variable"
let constant:String = "constant"
}
let constantStructSample = StructSample() // 構造体(値型)を初期化, 定数 に代入
constantStructSample.variable = "" // let constantStructSampleによる定数エラー
constantStructSample.constant = "" // let constant による定数エラー
var variableStructSample = StructSample() // 構造体(値型)を初期化, 定数 に代入
variableStructSample.variable = "" // エラーなし
variableStructSample.constant = "" // let constantによる定数エラー
let
で定義したプロパティは、もちろん変更できない。
気になるのは var
で定義したプロパティの変更をしようとしている以下のコードがエラーになる点だ。
これについて詳しく言及する。
let constantStructSample = StructSample() // 構造体(値型)を初期化, 定数 に代入
constantStructSample.variable = "" // let constantStructSampleによる定数エラー
struct
のインスタンスは 値
まず struct
は値型である。
structSample.variable = ""
で、structSample
という 値 に変更をかけたとしよう。
この時、「インスタンス」がlet
で定義されているものは、「クラスのプロパティ」がvar
で定義されていたとしても、インスタンスそのものが定数 であるため、値の変更はできない。
中の variable
が変数であっても、大枠のインスタンスが定数である場合、変更できないのだ。
class
,参照型のインスタンスを変更してみる
class ClassSample{
var variable:String = "variable"
let constant:String = "constant"
}
let constantClassSample = ClassSample() // クラス(参照型)を初期化, 定数 に代入
constantClassSample.variable = "" // エラーなし (ここが変な感じ)
constantClassSample.constant = "" // let constantによるエラー
var variableClassSample = ClassSample() // クラス(参照型)を初期化, 変数 に代入
variableClassSample.variable = "" // エラーなし
variableClassSample.constant = "" // let constantによるエラー
let
で定義したプロパティは、もちろん変更できない。
気になるのは、先ほどの struct
で定義されたクラスのインスタンスで、定数で代入された以下のコードはエラーなのに、class
の場合はエラーにならない点だ。
let constantStructSample = StructSample() // 構造体(値型)をinit(),定数に代入
constantStructSample.variable = "" // エラー
let constantClassSample = ClassSample() // クラス(参照型)をinit(),定数に代入
constantClassSample.variable = "" // エラーなし
class
のインスタンスは 参照先
まず、class
は参照型である。
let constantClassSample = ClassSample()
とした時、constantClassSample
には何が入っているだろうか。
わかりやすい例を以下に用意した。
let structSample = StructSample() // 構造体(値型)をinit(),定数に代入
let classSample = ClassSample() // クラス(参照型)をinit(),定数に代入
print(structSample) // StructSample(variable: "variable", constant: "constant")
print(classSample) // __lldb_expr_400.ClassSample
StructSample(variable: "variable", constant: "constant")
__lldb_expr_400.ClassSample
値型のインスタンスは、値そのもの が入っていることが確認できる。
参照型のインスタンスは、インスタンスが存在するポインタアドレスが入っていることが確認できる。つまり、参照先 が代入されているのである。
let constantClassSample = ClassSample() // クラス(参照型)をinit()
var variableClassSample = ClassSample() // クラス(参照型)をinit()
ではこれらの違いはなんだろうか。
init()
されたインスタンスを指してるポインタ(参照)のアドレスを、前者は定数に格納して、後者は変数に格納しているのである。
話を元に戻すと、
let constantClassSample = ClassSample() // 参照型をinit()して定数に格納
constantClassSample.variable = "" // 定数なのにエラーなし
が、エラーが出ないのは、このインスタンスを指してる 参照 を定数にしているだけで、値の変更はできるのだ。
計算プロパティ
値を保持せず、代わりに、間接的に他のプロパティの値を取得し、なんらかの処理を即時行って値を返す get
を提供する。
他のプロパティに値を設定する set
を提供することもできる。
struct Rect{
var origin = Point()
var size = Size()
var center:Point {
get{ // centerへのアクセスで、値を計算して返す
let centerX = origin.x + size.width / 2
let centerY = origin.y + size.height / 2
return Point(x: centerX, y: centerY)
}
set(newCenter){ //centerに値を代入で、計算 -> origin書き換え
origin.x = newCenter.x - size.width / 2
origin.y = newCenter.y - size.height / 2
}
}
}
var square = Rect(
origin: Point(x: 0.0, y: 0.0),
size: Size(width: 10.0, height: 10.0)
)
var initialSquareCenter = square.center //getによる計算結果が返る
print(initialSquareCenter) //Point(x: 5.0, y: 5.0)
square.center = Point(x: 15, y: 15) //setによる書き換え
var newOrigin = square.origin //Point(x: 10.0, y: 10.0)
また、計算プロパティは以下のような省略・機能が利用できる。
-
get
が単一式で構成される場合return
を省略できる。(暗黙的に単一式をreturn
する) -
set
が、設定される新しい名前を定義しない場合、デフォルトのnewValue
が使用可能。
struct CompactRect {
var origin = Point()
var size = Size()
var center: Point {
get {
Point(x: origin.x + (size.width / 2),
y: origin.y + (size.height / 2))
}
set {
origin.x = newValue.x - (size.width / 2)
origin.y = newValue.y - (size.height / 2)
}
}
}
プロパティオブザーバ
プロパティオブザーバは、プロパティの値の変化を監視し、それに応じたアクションをすることができる。
プロパティにこれらのオブザーバの下記のいずれかまたは両方を定義可能。
-
willSet
は、値が格納される直前に呼び出される。 -
didSet
は、新しい値が格納された直後に呼び出される。
struct StepCounter{
var number:Int = 0 {
willSet{
print("値(\(newValue))に変更されようとしています。")
}
didSet{
print("値(\(oldValue))が,\(number)に変更されました。")
}
}
}
var counter = StepCounter()
counter.number = 1
値(1)に変更されようとしています。
値(0)が,1に変更されました。
willSet
では格納プロパティの値(この例ではnumber
)・新しく代入しようとする値 を使用することができる。
didSet
では格納プロパティの値(この例ではnumber
)・代入される前の値 を使用することができる。
以下の図は、willSet
,didSet
で値がどのように通知されるのかを流れで表したものである。
willSet
/ didSet
は、「値の変更通知」であって「変更制御」ではない
「条件に合わなければプロパティの代入を止めたい」という時、willSet
,didSet
にbreak
を使用すると良さそうと考えてしまいがちだが、willSet
や didSet
では 代入をキャンセルすることはできない 。
Swiftの willSet
/didSet
は、代入される直前・直後に呼ばれるだけの通知的な仕組みであり、willSet
は「今から newValue を代入する」と通知するだけの力しか持っていない。
また、didSet
は「たった今、値の変更完了した」と通知するだけで、didSet
のスコープ内で、値そのものをブロック(拒否)する力は持ってない。
また、break
を利用すると良さそうに感じるが、break は基本的に ループ(for
, while
)や switch
の中でしか使用できないため、willSet
の中に break
書いたら文法エラーになる。
条件を満たさない場合に代入を防ぐコードは、計算プロパティを使用するとうまくいく。
struct StepCounter {
private(set) var currentCount = 0
private var _number: Int = 0
var number: Int {
get { _number }
set {
if newValue >= 0 {
currentCount += newValue
_number = newValue
print("値が変更されました。")
} else {
print("0未満は設定できません。")
}
}
}
}
var counter = StepCounter()
counter.number = 10 // OK
counter.number = -5 // 拒否される
print(counter.currentCount) // 10(変わらない)
プロパティラッパー
プロパティラッパ(@propertyWrapper
) は、共通のプロパティ管理ロジックを一箇所にまとめて、共通処理の再利用を便利にする仕組み。
@propertyWrapper
struct TwelveOrLess {
private var number:Int = 0
private var maximum:Int = 12
var wrappedValue:Int{
get{
number
}
set{
number = min(newValue,maximum)
}
}
}
@propertyWrapper
を定義するには、必ず wrappedValue
というプロパティを定義する必要がある。
この名前をキーにして、Swiftのコンパイラが、値(height
, width
)の読み書きの際に、 wrappedValue
経由で共通のプロパティ管理ロジック(TwelveOrLess
)を適応させるからである。
struct SmallRectangle{
@TwelveOrLess var height:Int
@TwelveOrLess var width:Int
}
@propertyWrapper
が指定されたheight
の値は、wrappedValue
経由で、プロパティ管理ロジック(TwelveOrLess
)のチェックを受ける。要は set
に値が流れ込む。
var rectangle = SmallRectangle()
rectangle.height = 18
print(rectangle.height) // 12
内部で起こること
プロパティラッパを変数に指定すると、コンパイラは以下の裏コードを自動生成する。
private var _height = TwelveOrLess() //デフォルト値(number=0,maximum=12)を利用した初期化
var height: Int {
get { _height.wrappedValue }
set { _height.wrappedValue = newValue }
}
このコードは見えないが、上記コードを裏側で定義してくれているおかげで、
-
height
にはTwelveOrLess()
による初期化 - さらに計算プロパティが組み込まれる(
height
が実体を持たず、中の_height.wrappedValue
にアクセスする)
ことになる。@TwelveOrLess var height:Int
とすることで、初期化 と 計算プロパティ が隠蔽される形になる。
プロパティの初期値設定
上記のようなプロパティラッパーの定義では、任意の値を初期値として指定することはできない。
これは、プロパティラッパー側にイニシャライザ( init(wrappedValue:)
)を用意していないからである
struct SmallRectangle {
@TwelveOrLess var height: Int = 5 // エラーになる
}
こう書いてしまった場合、コンパイラは以下のような解釈をする。
private var _height = TwelveOrLess(wrappedValue: 5)
もし、初期値を指定したい場合はカスタムイニシャライザを実装する。
@propertyWrapper
struct TwelveOrLess {
private var number:Int
private var maximum:Int
var wrappedValue:Int{
get{
number
}
set{
number = min(newValue,maximum)
}
}
init(wrappedValue:Int){
maximum = 12
number = min(wrappedValue,maximum)
}
}
struct SmallRectangle{
@TwelveOrLess var height:Int = 18
}
また、そのほかの引数を取る場合、カスタムイニシャライザを複数用意しておくことができる。
@propertyWrapper
struct TwelveOrLess {
private var number:Int
private var maximum:Int
var wrappedValue:Int{
get{
number
}
set{
number = min(newValue,maximum)
}
}
init(wrappedValue:Int){
maximum = 12
number = min(wrappedValue,maximum)
}
init(wrappedValue:Int,maximum:Int){
self.maximum = maximum
number = min(wrappedValue,maximum)
}
}
また、初期値指定の際に以下のような書き方ができる。特に引数が、wrappedValue
以外にある場合、有効である。
struct SmallRectangle{
@TwelveOrLess var height:Int = 18 // 引数がwrappedValueのみの場合
@TwelveOrLess(wrappedValue: 200, maximum: 120) var width:Int // 引数が複数ある場合
}
イニシャライザは明示したほうがいい
Private initializer 'init(number:projectedValue:)' cannot have more restrictive access than its enclosing property wrapper type 'SmallNumber' (which is internal)
というエラーがたまにでる。
プロパティラッパー定義時に、
@propertyWrapper
struct TwelveOrLess {
private var number = 0 // ← これが "デフォルトの初期値"
var wrappedValue: Int {
get { number }
set { number = min(newValue, 12) }
}
}
としたとき、イニシャライザは定義していないため、Swiftが暗黙的にイニシャライザを生成している。
init() {
self.number = 0
}
ただたまに、暗黙的なイニシャライザがprivate
レベルで自動生成される時があるらしい。
そのため、特別な理由がなければ init()
は定義しておくが吉。これでイニシャライザもinternal
レベルで定義される。(@propertyWrapper
はデフォルトでinternal
レベルなのでイニシャライザも@propertyWrapper
と同じレベルに合わせておくということ。)
@propertyWrapper
struct SmallNumber {
private var number = 0
var wrappedValue: Int {
get { return number }
set {
if newValue > 12 {
number = 12
} else {
number = newValue
}
}
}
init(){} // 別にこれだけでもいい
}
もしくは、
@propertyWrapper
struct SmallNumber {
private var number:Int
var wrappedValue: Int {
get { return number }
set {
if newValue > 12 {
number = 12
} else {
number = newValue
}
}
}
init(){ // ちゃんと書いてみる
number = 0
}
}
projectedValue
という追加情報
projectedValue
は wrappedValue
以外にSwiftが特別扱いする、もう一つのプロパティで、Swift が $プロパティ名
という特別な構文でアクセスさせてくれるラッパー側のもうひとつの出口。
@propertyWrapper
struct SmallNumber {
private var number = 0
var projectedValue = false
var wrappedValue: Int {
get { return number }
set {
if newValue > 12 {
number = 12
projectedValue = true
} else {
number = newValue
projectedValue = false
}
}
}
init(){}
}
struct SomeStructure {
@SmallNumber var someNumber: Int
}
var someStructure = SomeStructure()
someStructure.someNumber = 4
print(someStructure.$someNumber)
// false
someStructure.someNumber = 55
print(someStructure.$someNumber)
// true
また、ログを残すようなコードもかける。
@propertyWrapper
struct Loggable {
private var value: String = ""
private(set) var log: [String] = []
var wrappedValue: String {
get { value }
set {
value = newValue
log.append("Set to: \(newValue)")
}
}
var projectedValue: [String] {
log // ← Stringの配列
}
}
struct User {
@Loggable var name: String
}
var user = User()
user.name = "Taro"
user.name = "Jiro"
print(user.$name) // ["Set to: Taro", "Set to: Jiro"]
少しまとめてみる
input | 役割 | output |
---|---|---|
wrappedValue |
実際の値を外からアクセスする | object.value |
projectedValue |
追加の情報や機能を$変数 で提供 |
object.$value |
応用例
自動トリミング(空白削除)
@propertyWrapper
struct Trimmed {
private var value: String = ""
var wrappedValue: String {
get { value }
set { value = newValue.trimmingCharacters(in: .whitespacesAndNewlines) }
}
}
struct UserInput {
@Trimmed var name: String
}
var input = UserInput()
input.name = " Taro "
print(input.name) // → "Taro"
ログ出力付きプロパティ
@propertyWrapper
struct Logged<T> {
private var value: T
private let name: String
init(wrappedValue: T, name: String) {
self.value = wrappedValue
self.name = name
}
var wrappedValue: T {
get { value }
set {
print("\(name) を \(value) から \(newValue) に変更しました")
value = newValue
}
}
}
struct Settings {
@Logged(name: "音量") var volume: Int = 5
}
var s = Settings()
s.volume = 8
// → 音量 を 5 から 8 に変更しました
UserDefault
連携
@propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
var wrappedValue: T {
get {
UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
struct Preferences {
@UserDefault(key: "username", defaultValue: "guest")
var username: String
}
var prefs = Preferences()
print(prefs.username) // → "guest"(初期状態)
prefs.username = "nobu"
print(UserDefaults.standard.string(forKey: "username")!) // → "nobu"
数値の範囲制御
@propertyWrapper
struct Clamped<Value: Comparable> {
private var value: Value
let range: ClosedRange<Value>
init(wrappedValue: Value, _ range: ClosedRange<Value>) {
self.range = range
self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
}
var wrappedValue: Value {
get { value }
set { value = min(max(newValue, range.lowerBound), range.upperBound) }
}
}
struct BrightnessSetting {
@Clamped(0...100) var brightness: Int = 50
}
var setting = BrightnessSetting()
setting.brightness = 120
print(setting.brightness) // → 100
UITextFieldの入力値を制御するプロパティラッパー
@propertyWrapper
struct MaxLength {
private var value: String = ""
private let limit: Int
init(wrappedValue: String, _ limit: Int) {
self.limit = limit
self.value = String(wrappedValue.prefix(limit))
}
var wrappedValue: String {
get { value }
set { value = String(newValue.prefix(limit)) }
}
}
struct UserInput {
@MaxLength(10) var name: String = ""
}
class ViewController: UIViewController {
// View: UITextField を storyboard から接続
@IBOutlet weak var nameTextField: UITextField!
// Model
var userInput = UserInput()
override func viewDidLoad() {
super.viewDidLoad()
// イベント監視
nameTextField.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged)
}
@objc func textFieldDidChange(_ textField: UITextField) {
userInput.name = textField.text ?? ""
print("入力された名前(制限済): \(userInput.name)")
// 文字が切り詰められたら表示も更新してあげる
if textField.text != userInput.name {
textField.text = userInput.name
}
}
}
複数のラッパーを組み合わせることもできる。
@Trimmed
@Logged(name: "名前")
var name: String = ""
static
キーワード
static
は「全インスタンスで共通の情報」を定義する際に便利である。また、static
を用いてプロパティを定義すると、アクセス時、インスタンス化が不要になる。
struct Roster{
static var company = "Apple.inc"
var name:String
}
社員個票用のクラスを作成した。
この時、company
,name
にアクセスしてみよう。
通常の定義をしたname
プロパティは、インスタンス化をしてから.name
でアクセスする。
let employee = Roster(name: "Sam")
let employeeName = employee.name
一方、static
で定義したcompany
プロパティはインスタンス化が不要である。
company
プロパティは全インスタンスで共通の値であるため、インスタンスごとに用意するのではなく、クラス自体にただ一つ用意されたプロパティとして扱う。
アクセス時はクラス名.プロパティ
でアクセスする。
let companyName = Roster.company
staticはクラスにくっついてるって分かってるけど、共通であればインスタンスからアクセスできても自然では?
たとえば以下は許可されていない。
let employee = Roster(name: "Sam")
let companyName = employee.company //インスタンスからアクセス->(エラー)
Samの例を見てみよう。
let sam = Employee(name: "Sam")
print(sam.companyName) // ← 「Samの会社の名前」-> うーん (ちなみにこう書くとエラーが出る)
これは一見できるが、「会社はSamのもの」と勘違いされる可能性がある。
実際は、全社員で共有する会社名を表しているため、クラス名からアクセスするべき。(swiftらしい)
まとめ
-
struct
(値型)は「まるごとコピー」だから、let
で定義するとプロパティ変更も不可 -
class
(参照型)は「アドレス参照」だから、let
でもプロパティ変更OK -
@propertyWrapper
で共通処理を1箇所にまとめて、安全・シンプルに! -
willSet
/didSet
はロジック補助やログ用途に便利 -
projectedValue
($変数名
)を使えば、裏の値や追加情報も取り出せる
疲れt...