みそかつモバイル #1 | SwiftのProperty Wrapperを見つめ直す #みそかつウェブ
https://gyazo.com/acfdd0f39237558705f210dee9d2cd0f
自己紹介 ikesyo.icon
いけしょー/池田 翔
マンガアプリチーム
スマートフォンアプリエンジニア(iOS/Android/React Native)
趣味はOSS活動です
最近は触れていません……
かつてはHimotoki/Carthage/ReactiveSwiftなども
本題
今日はSwiftのProperty Wrapperの話をします
Property Wrapper
Swift 5.1〜(2019/9)
SwiftUIのために導入された(と認識している)
その名のとおり、プロパティをラップするもの
プロパティのget/set(およびストレージ)を別の型に移譲する
変換処理やバリデーションを差し込んだり
プロパティに値をセットしたら設定ファイルに値を保存したり
雰囲気
code:swift
struct PlayerView: View {
var episode: Episode
@State private var isPlaying: Bool = false
var body: some View {
VStack {
Text(episode.title)
Text(episode.showTitle)
PlayButton(isPlaying: $isPlaying)
}
}
}
var isPlaying: Boolのプロパティ定義にState Property Wrapperを属性@として指定している
SwiftUIでは@Stateの値はこのstructが保持するのではなく、SwiftUIのランタイムが別途保持し、ビューの再構築時に値を復元する
isPlayingと普通にアクセスするとラップされたBoolの値が返ってくる
$isPlayingと$を頭に付けると、projected valueが返ってくる
ラップした値とは別の任意の型の値をProjection(射影)として返すことができる
例えばバリデーションの結果をBoolで返すとか、Property Wrapper自身を返すとか
State<Bool>のProjectionはBinding<Bool>
Projectionについては後述
SwiftUIで使われているProperty Wrapper(一部)
@State
@StateObject
@Published
@Environment
@EnvironmentObjecct
@ObservedObject
@Binding
@AppStorage
...
作り方
定義する
code:swift
@propertyWrapper
struct TwelveOrLess {
private var number = 0
var wrappedValue: Int {
get { return number }
set { number = min(newValue, 12) }
}
}
@propertyWrapper属性を付けた型を作る
(@propertyWrapper自体は組み込みの属性であってProperty Wrapperではない)
structでもenumでもclassでもよい
最低限必要なのはwrappedValueというプロパティ
getで値を返して、setで値を保存する
ここではstructのプロパティに値を保存している
(再掲)SwiftUIの@StateではSwiftUIのランタイムが保持し、ビューの再構築時に値を復元する
SwiftUIの@AppStorageだと @AppStorage("some_key") var someSetting: Boolのようにキーを指定してUserDefaults(アプリ単位の設定ファイル)に保存される
genericにしてもよい
code:swift
@propertyWrapper
struct Wrapper<Value> {
var wrappedValue: Value
}
struct Foo {
@Wrapper var intValue: Int = 0
@Wrapper var stringValue: String = ""
}
使ってみる
code:swift
struct SmallRectangle {
@TwelveOrLess var height: Int
@TwelveOrLess var width: Int
}
var rectangle = SmallRectangle()
print(rectangle.height)
// Prints "0"
rectangle.height = 10
print(rectangle.height)
// Prints "10"
rectangle.height = 24
print(rectangle.height)
// Prints "12"
コンパイラーによって生成されるコードの実態
code:swift
struct SmallRectangle {
private var _height = TwelveOrLess()
private var _width = TwelveOrLess()
var height: Int {
get { return _height.wrappedValue }
set { _height.wrappedValue = newValue }
}
var width: Int {
get { return _width.wrappedValue }
set { _width.wrappedValue = newValue }
}
}
Property Wrapper用に _ + 元の名前のプロパティが用意される
元のプロパティはget/setでProperty Wrapperの wrappedValue にアクセスする
初期値と引数
code:swift
@propertyWrapper
struct SmallNumber {
private var maximum: Int
private var number: Int
var wrappedValue: Int {
get { return number }
set { number = min(newValue, maximum) }
}
init() {
maximum = 12
number = 0
}
init(wrappedValue: Int) {
maximum = 12
number = min(wrappedValue, maximum)
}
init(wrappedValue: Int, maximum: Int) {
self.maximum = maximum
number = min(wrappedValue, maximum)
}
}
初期値なし
code:swift
struct ZeroRectangle {
@SmallNumber var height: Int
@SmallNumber var width: Int
}
init()が呼ばれる
初期値あり
code:swift
struct UnitRectangle {
@SmallNumber var height: Int = 1
@SmallNumber var width: Int = 1
}
init(wrappedValue: Int)が呼ばれる
プロパティに初期値として代入した値がwrappedValueに渡される
引数あり
code:swift
struct NarrowRectangle {
@SmallNumber(wrappedValue: 2, maximum: 5) var height: Int
@SmallNumber(wrappedValue: 3, maximum: 4) var width: Int
}
init(wrappedValue: Int, maximum: Int)を明示的に呼んでいる
初期値も引数もwrapperのイニシャライザの引数として渡している
code:swift
struct MixedRectangle {
@SmallNumber var height: Int = 1
@SmallNumber(maximum: 9) var width: Int = 2
}
初期値は代入で渡すこともできる
Projection(射影)
ラップした値とは別の任意の型の値をProjection(射影)として返すことができる
例えばバリデーションの結果をBoolで返すとか、Property Wrapper自身を返すとか
State<Bool>のProjectionはBinding<Bool>
定義
code: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
}
}
}
}
利用
code:swift
struct SomeStructure {
@SmallNumber var someNumber: Int
}
var someStructure = SomeStructure()
someStructure.someNumber = 4
print(someStructure.$someNumber)
// Prints "false"
someStructure.someNumber = 55
print(someStructure.$someNumber)
// Prints "true"
面白い・特徴的な機能ですね
SwiftUIでは
State -> Binding
親ビューで管理している状態を子ビューが変更して、状態が変わったら親から再構築
Published -> Publisher(RxのObservable的なやつ)
プロパティの変更を値のストリームとして流せる
利用例
現在値と1つ前の値を取れるProperty Wrapper
OSSとして公開しました
code:WithPrevious.swift
@propertyWrapper
public struct WithPrevious<Value> {
private var current: Value
private var previous: Value?
public init(_ value: Value) {
self.current = value
}
public init(wrappedValue: Value) {
self.init(wrappedValue)
}
public var wrappedValue: Value {
get { current }
set {
previous = current
current = newValue
}
}
public var projectedValue: Value? { previous }
}
code:WithPreviousTests.swift
import XCTest
import WithPrevious
final class WithPreviousTests: XCTestCase {
@WithPrevious var value = 0
func testWithPreviousWrapper() {
XCTAssertEqual(value, 0)
XCTAssertNil($value)
value = 10
XCTAssertEqual(value, 10)
XCTAssertEqual($value, 0)
}
}
発展的情報
合成(ネスト)
Property Wrapperを連続して付与すると入れ子にして合成できる
@State @Previous var currentPage = 0
これはState<Previous<Int>>になるが、生成コードはネストを剥がしてくれてcurrentPageで直接Intの0を得ることができる
code:swift
private var _currentPage: State<Previous<Int>> = State(wrappedValue: Previous(wrappedValue: 0))
var currentPage: Int {
get { return _currentPage.wrappedValue.wrappedValue }
set { _currentPage.wrappedValue.wrappedValue = newValue }
}
しかし$でProjectionを得る場合はネストを剥がしてはくれない
_currentPage.wrappedValue.projectedValue // Int?のようにする必要があるので注意
最近の進化
Swift 5.4:ローカル変数にも使えるようになった
code:swift
@propertyWrapper
struct Wrapper<T> {
var wrappedValue: T
}
func test() {
@Wrapper var value = 10
}
Swift 5.5:関数やクロージャの引数にも使えるようになる(この秋のXcode 13でリリース予定)
code:swift
@propertyWrapper
struct Wrapper<Value> {
var wrappedValue: Value
var projectedValue: Self { return self }
init(wrappedValue: Value) { ... }
init(projectedValue: Self) { ... }
}
func test(@Wrapper value: Int) {
print(value)
print($value)
print(_value)
}
test(value: 10)
let projection = Wrapper(wrappedValue: 10)
test($value: projection)
ラップされる値を渡すだけでなく、projectedValueを渡すこともできる
The call-site can pass a wrapped value or a projected value, and the property wrapper will be initialized using init(wrappedValue:) or init(projectedValue:), respectively.
hr.icon
採用情報
漫画が好き
宣言的UI
GraphQL
マルチモジュール化
などに興味ある方はご連絡お待ちしています!!
https://gyazo.com/ab1f9fdf81cf800c9f714022f122f722
📦 HAPPY PROPERTY WRAPPING 📦
😆THANK YOU❗️💖