在全局?jǐn)?shù)據(jù)同步系列文章中(一)(二)分別解決了model和view的全局同步座舍,但是依然有一些問題拳球,所以在這里給一個終極解決方案DDKeyPathChannel蛛勉。
重新來說明下解決的問題
由于各種原因剔桨,目前有兩個表示同一種類型的model限匣。
@interface UserModel1 : NSObject
@property (strong, nonatomic) NSString *id;
@property (strong, nonatomic) NSString *name;
@property (assign, nonatomic) NSInteger age;
@end
@interface UserModel2 : NSObject
@property (strong, nonatomic) NSString *id;
@property (strong, nonatomic) NSString *nickName;
@property (assign, nonatomic) NSInteger age;
@end
現(xiàn)在需要其中一個屬性修改了讥珍,或者第三方要求更新屬性历极,如何更好的同步各個不同model之間的屬性呢?
另一個問題衷佃,又如何把這個狀態(tài)更新到UI上呢趟卸?
以前方案的問題
首先,來看看之前解決方案的幾個問題纲酗。
- 所有基類都需要實現(xiàn)特定接口協(xié)議衰腌。這對于model來說會比較簡單,但是對于UIView來說就比較麻煩觅赊。
- 使用上右蕊,實現(xiàn)上比較麻煩,需要注意的地方比較多吮螺,容易犯錯誤饶囚。
那么有沒有不影響到原來的類的方式呢?
新思路
既然現(xiàn)有的類去實現(xiàn)這個協(xié)議比較麻煩鸠补,那么找一個第三方類萝风,永久的實現(xiàn)這個接口,并且把消息轉(zhuǎn)發(fā)到現(xiàn)有的類不就可以了嗎紫岩。
我們都知道有一個類不繼承于NSObject
规惰,功能就是代理,那么我們利用這個類來做消息轉(zhuǎn)發(fā)泉蝌。
接口如下
@interface DDKeyPathChannelBaseProxy : NSProxy <DDKeyPathChannelProtocol>
// 以下兩個屬性確定對象唯一性
@property (readonly, nonatomic) NSInteger channelType;
@property (readonly, nonatomic) NSString *channelId;
// 原本的對象
@property (weak, readonly, nonatomic) __kindof NSObject *target;
- (instancetype)initWithChannelType:(NSInteger)channelType channelId:(NSString *)channelId target:(NSObject *)target;
@end
// 這是一個通過keyPath+白名單的實現(xiàn)歇万,可以通過mapper來映射真正的keyPath
@interface DDKeyPathChannelProxy : DDKeyPathChannelBaseProxy
@property (strong, nonatomic) NSArray<NSString *> *whiteList;
@property (strong, nonatomic) NSDictionary<NSString *, NSString *> *keyPathMapper; // messageKeyPath : realKeyPath
@property (strong, nonatomic) void(^valueWillChangeBlock)(__kindof NSObject *target, NSString *keyPath, __kindof id newValue);
@property (strong, nonatomic) void(^valueDidChangeBlock)(__kindof NSObject *target, NSString *keyPath, __kindof id newValue);
@end
// 這是一個block的實現(xiàn)揩晴,可以在同步的時候自定義轉(zhuǎn)換與實現(xiàn)
@interface DDKeyPathBlockChannelProxy : DDKeyPathChannelBaseProxy
@property (strong, nonatomic) NSString *keyPath;
@property (strong, nonatomic) void(^valueChangedBlock)(__kindof NSObject *target, NSString *keyPath, __kindof id newValue);
@end
轉(zhuǎn)發(fā)的核心在于消息的傳遞
// keyPath
- (void)setValue:(id)value forKey:(NSString *)key {
if (self.valueWillChangeBlock) self.valueWillChangeBlock(self.target, key, value);
[self.target setValue:value forKey:key];
if (self.valueDidChangeBlock) self.valueDidChangeBlock(self.target, key, value);
}
// block
- (void)setValue:(id)value forKey:(NSString *)key {
if ([key isEqualToString:self.keyPath]) {
if (self.valueChangedBlock) {
self.valueChangedBlock(self.target, key, value);
}
}
}
那么我們怎么去掛載這個代理對象呢,想到associate object贪磺,那么我們也很容易的控制自己的生命周期了硫兰。
這樣,我們就不需要在現(xiàn)有類中實現(xiàn)方法來支持該功能了寒锚,而且這樣也更好的封裝屏蔽了這些比較特殊的功能劫映。在實踐中感覺這種方式的使用成本是最低的,大家也比較容易接受刹前。
[self.user1 bindChannelType:ChannelTypeUser
channelId:self.user1.id];
[self.user2 addChannelProxyWithChannelType:ChannelTypeUser
channelId:self.user2.id
config:^(DDKeyPathChannelProxy *proxy) {
proxy.keyPathMapper = @{ @"name": @"nickName" };
}];
// 更新屬性
[[DDKeyPathChannelManager sharedChannel] emitChannelType:ChannelTypeUser
channelId:@"1"
value:@"Tom"
forKeyPath:@"name"];
[[DDKeyPathChannelManager sharedChannel] emitChannelType:ChannelTypeUser
channelId:@"1"
value:@(30)
forKeyPath:@"age"];
UI層更新也可以通過這種方式泳赋,也可以選擇使用KVO。
題外話
關(guān)于這個功能腮郊,很多人肯定想到了ReactiveX
摹蘑,關(guān)于這點(diǎn),兩者的確有部分相似的場景轧飞,但也有很多不同的地方。
關(guān)于更新UI這點(diǎn)撒踪,兩者從效果上來看的確是一致的
object -> (signal, keyPath) -> UI
兩者最大的不同在于过咬,ReactiveX
是monad的思想,是有輸入輸出制妄,擁有明確的輸入對象和觀察對象掸绞,行為流程是從上游到下游。而本套方案是一個中間人模式耕捞,是一個星狀結(jié)構(gòu)衔掸,更像通知一點(diǎn)。
但是兩者思想是類似的俺抽,ReactiveX
是把各種行為封裝成Signal
敞映,而我們是把消息使用keyPath
來承載與轉(zhuǎn)發(fā)。
如果想要使用ReactiveX
來實現(xiàn)這個功能也不是不可以磷斧,創(chuàng)建一個全局的熱型號(subject)振愿,控制好回收(dispose),也是可以實現(xiàn)該功能弛饭,但總感覺和RX的概念有點(diǎn)偏差了冕末。
總結(jié)
從第一篇方案,到現(xiàn)在最終比較完美的一套方案侣颂,也是因為我們的需求在一步一步的變化档桃,要求我們使用更好、更靈活的方案才能滿足的結(jié)果憔晒。這個過程是一個不斷思考不斷反思的過程藻肄,從這個方案的演化中蔑舞,我深有感悟,很多東西在創(chuàng)造出來的時候看似完美仅炊,但實際上還有很大的完善空間斗幼,同時別人的方案也會對自己的想法有很多的幫助。所以多了解別人的實現(xiàn)方案對自己的提升還是很有幫助的抚垄。