Property wrapperを使って値の初期化をラップする
Swiftの言語仕様にはProperty Wrappeerがあります.これは名前の通り本来の値をラップする機能です.例えば,ValidatedPropertyKit
はこの機能を使って,attributeが付いたプロパティに対してバリデーションを行っています.機能の詳細はプロポーザルを御覧ください.
Property Wrappeerの実態はstructやclassなどのTypeです.そのため,値のgetter/setterをラップする以外の処理も色々書けてしまいます.本記事では,プロパティの初期化時に行いたい処理をProperty wrapperを用いて行ってみます.Property wrapperを使うとその処理を隠蔽し,かつattributeを付与するだけで使い回せるという利点があります.ビジネスルールと殆ど関係無いが,頻繁に記述する分析用のロギング処理などでその威力を大きく発揮するでしょう.
では,実際にロギング処理をProperty wrapperを使って実装してみます.
具体的には,@Tracking
attributeが付いたUIButtonインスタンがタップされた際に,ロギングされるようにします.
まず,次のようなロギングの対象となるEventとロギングを行うグローバルなfunctionがあるとします.
enum Event {
case tap(of: String)
}
func log(_ event: Event) {
print("logged \(event)")
}
次に,ロギングのトリガーとなるタップイベントをtrackingEventTrigger
として公開します.
protocol Trackable {
var trackingEventTrigger: Observable<Void> { get }
}
extension UIButton: Trackable {
var trackingEventTrigger: Observable<Void> {
self.rx.tap.map { () }
}
}
そして,Property Wrappeerを作成します.initialize時にtapイベントtrackingEventTrigger
を購読し,ロギングを行うような実装になっています.
@propertyWrapper class Tracking<Base: Trackable> {
private let disposeBag = DisposeBag()
let wrappedValue: Base
var trackEvent: Event? = nil
init(wrappedValue: @autoclosure @escaping () -> Base) {
self.wrappedValue = wrappedValue()
observe()
}
private func observe() {
wrappedValue
.trackingEventTrigger
.subscribe(onNext: { [weak self] in
guard let trackEvent = self?.trackEvent else { return }
log(trackEvent)
})
.disposed(by: disposeBag)
}
}
以上の記述によって,次のように@Tracking
attributeを付けるだけで,タップ時にロギングされるようになります.
class MyViewController : UIViewController {
@Tracking private var button = UIButton()
~~~~~~~~~~~
~~~~~~~~~~~
override func viewDidLoad() {
super.viewDidLoad()
_button.trackEvent = .tap(of: "hoge")
}
}
利用している箇所を見ると,ロギングに関する処理を隠蔽できている事がわかります.また,他のボタンでロギングする場合も@Tracking
を付けるだけで済むためうまく共通化もできています.
一方で,作ってみたものも次のような理由から実用には耐えられないと考えています.
- Property wrapperされたプロパティはmutableになってしまう
- Property wrapperの性質上,当たり前と言えば当たり前
- あるViewに複数のトラッキングイベントが紐づく場合に対応できない
- 例えば,インプレッションとタップ
よって今回は,こんなことも出来るよという紹介でした.
PureなSwiftで頑張ろうと思ったのですが,こういう処理を隠蔽しようと思うとAspectやメタプロを使うのが適しているのかもしれません.
Discussion