Swift ライブラリの ReSwift を RxSwift に合わせて使えるようにするメモです。
ReSwift とは
ReSwift とは、iOS アプリを Redux のように作れるようにしてくれるライブラリです。
スマホアプリを作っていると
- アプリの状態管理を一元管理したい
- アプリの状態が変わったら、UI を最新に更新したい
という要求が出てくると思います。そんなときに、ReSwift でステートを一元管理し、UI の更新を RxSwift の I/F に合わせて使えると結構便利です。
ソースコード
ということで、メモ書きなので、さっとソースを貼り付けておきます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
|
/// アプリの `Store` クラス
/// Rx 拡張を行うために独自クラスを作成する
class AppStore : Store<AppState> {
required convenience init(reducer: @escaping (Action, AppState?) -> AppState, state: State?) {
self.init(reducer: reducer, state: state, middleware: [])
}
}
/// AppStore を Rx 化するためのプロキシ
class AppStoreProxy<SelectedState> : StoreSubscriber {
typealias StoreSubscriberStateType = SelectedState
private weak var source: AppStore?
private(set) var subject = PublishSubject<StoreSubscriberStateType>()
fileprivate init(source: AppStore) {
self.source = source
}
func newState(state: StoreSubscriberStateType) {
subject.on(.next(state))
}
deinit {
subject.on(.completed)
}
}
extension AppStore : ReactiveCompatible {
}
extension Reactive where Base: AppStore {
func state() -> Observable<AppStore.State> {
return state { $0 }
}
func state<SelectedState: StateType>(selector: @escaping ((AppStore.State) -> SelectedState)) -> Observable<SelectedState> {
return Observable.create { observer in
let proxy = AppStoreProxy<SelectedState>(source: self.base)
_ = proxy.subject.bind(to: observer)
self.base.subscribe(proxy, transform: { (subscriber: Subscription<AppState>) -> Subscription<SelectedState> in
subscriber.select(selector)
})
return Disposables.create {
self.base.unsubscribe(proxy)
}
}
}
func state<SelectedState: StateType & Identifiable>(selector: @escaping ((AppStore.State) -> SelectedState)) -> Observable<SelectedState> {
return Observable.create { observer in
let proxy = AppStoreProxy<SelectedState>(source: self.base)
_ = proxy.subject.bind(to: observer)
self.base.subscribe(proxy, transform: { (subscriber: Subscription<AppState>) -> Subscription<SelectedState> in
subscriber.select(selector)
})
return Disposables.create {
self.base.unsubscribe(proxy)
}
}
}
}
|
これで、ReSwift の State
を次のように書くことができます。
1
2
3
4
|
store.rx.state { $0.userState }
.map { $0.name }
.bind(to: nameLabel.rx.text)
.disposed(by: disposeBag)
|
オブザーブしているステートが更新されたときのみ通知して欲しい時
ReSwift を RxSwift のように使うだけであれば、上の拡張でいいのですが、たくさんステートが出来てくると、
関係ないステートが更新されたときにも変更が通知されてしまって、パフォーマンスを気にすることが出てくるかもしれません。
そんなときは、distinctUntilChanged
を使って、監視しているステートが更新されたかどうかを確認するようにするといいと思います。
State
自体を ==
で比較できるように、State に Identifier
を導入して、更新されたかどうかをチェック出来るようにしてみます。
(というのも、State は構造体(struct
) で作ることになると思うので、同一のステートかどうかの一致が大変なのです。)
State に一意性を持たせる
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
/// 識別子コンポーネント
struct IdentifiableComponent : Hashable {
typealias Identifier = UInt64
/// インスタンス生成毎に一意になる値を生成するクラス
private struct Counter {
static let lock = DispatchSemaphore(value: 1)
static var count: Identifier = 0
static func getAndIncrement() -> Identifier {
lock.wait()
defer { lock.signal() }
count += 1
return count
}
}
/// インスタンスの識別子
private(set) var identifier: Identifier = Counter.getAndIncrement()
var hashValue: Int { return identifier.hashValue }
mutating func update() {
identifier = Counter.getAndIncrement()
}
static func ==(lhs: IdentifiableComponent, rhs: IdentifiableComponent) -> Bool {
return lhs.identifier == rhs.identifier
}
}
protocol HasIdentifiableComponent : Equatable {
var identifiableComponent: IdentifiableComponent { get }
}
/// 識別子を持つタイプ
protocol Identifiable : HasIdentifiableComponent {
}
extension Identifiable {
var identifier: IdentifiableComponent.Identifier {
return identifiableComponent.identifier
}
static func ==(lhs: Self, rhs: Self) -> Bool {
return lhs.identifiableComponent == rhs.identifiableComponent
}
}
|
複雑そうなことをやっていますが、要はインスタンスを生成するたびに一意の数値を割り当てて、
状態が更新されたらその値をインクリメントするという方法で、数値比較だけで状態が変わったかを判断できるようにしています。
この識別子コンポーネントを State に持たせるようにして
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
struct UserState : StateType, Identifiable {
fileprivate(set) var identifiableComponent = IdentifiableComponent()
var name: String?
}
// MARK: - Reducer
extension UserState {
static func reducer(state: UserState?, action: Action) -> UserState {
var state = state ?? UserState()
switch action {
case let action as UserActions.SetName:
state.name = action.name
state.identifiableComponent.update()
default:
break
}
return state
}
|
こんな感じで、ステートが更新したときに、identifiableComponent.update()
を呼び出すと状態が更新されたことをマークします。
これで、ステートが更新されたときだけ UI を更新するようなコードを次のように書けるようになります。
1
2
3
4
5
|
store.rx.state { $0.userState }
.distinctUntilChanged()
.map { $0.name }
.bind(to: nameLabel.rx.text)
.disposed(by: disposeBag)
|
パフォーマンスも気にせず使えるようになりました。
既存のプロジェクトからの抜粋なので、一部省略している箇所があります。