說到圖片瀏覽器太雨,項目中比較常用的成熟框架有Objective-C版本的MWPhotoBrowser,IDMPhotoBrowser或者Swift版本的SKPhotoBrowser。
從核心功能來看,MWPhotoBrowser株汉,IDMPhotoBrowser這兩個框架,都很好地實現(xiàn)了對本地資源蹬蚁、相冊資源打月、網(wǎng)絡(luò)資源的獲取與顯示。并且很好地封裝了網(wǎng)絡(luò)和相冊的獲取方式,這在我看來這是他的優(yōu)勢薪前,同時也是他的不足润努。
這樣做的優(yōu)勢不言而喻,調(diào)用者只需要很少的幾行代碼示括,就可以集成一個圖片瀏覽器框架铺浇,省時省力。以MWPhotoBrowser為例垛膝,在不設(shè)置額外屬性的情況下鳍侣,只需要下面兩行代碼就可以創(chuàng)建:
MWPhotoBrowser *browser = [[MWPhotoBrowser alloc] initWithPhotos:self.photos];
[self.navigationController pushViewController:browser animated:YES];
使用者只要關(guān)注如何提供MWPhotoBrowser所要展示資源就可以了,不需要做額外的操作吼拥,非常地簡潔方便倚聚。
關(guān)于不足,由于MWPhotoBrowser內(nèi)部實現(xiàn)了獲取網(wǎng)絡(luò)圖片功能扔罪,在追求內(nèi)部實現(xiàn)盡量精簡的前提下秉沼,不可避免地要依賴加載圖片的第三方庫(SDWebImage)。如果原來項目并沒有使用SDWebImage矿酵,而是用YYWebImage或者Kingfisher唬复,那么使用MWPhotoBrowser便會引入冗余的框架,從而讓項目額外增加了一種圖片緩存機制全肮,不利于內(nèi)存以及磁盤使用率的優(yōu)化敞咧。
對于相冊資源的訪問,MWPhotoBrowser內(nèi)部也實現(xiàn)了通過PHAsset或者ALAsset獲取相片的功能辜腺。不過一般來說休建,項目會有自己的一套相冊選擇器,進而會有相應(yīng)的相冊資源獲取策略评疗。所以以個人觀點來看测砂,如何獲取相冊資源,應(yīng)該由使用者告知百匆,而不是在框架內(nèi)部自己實現(xiàn)一套砌些,這樣更加符合DRY。
接下來加匈,我會針對上面的不足存璃,實現(xiàn)一套兼容本地資源、相冊資源雕拼、網(wǎng)絡(luò)資源的簡易圖片選擇器纵东。
本文章對應(yīng)的所有代碼在倉庫TBVImageBrowser中,歡迎交流啥寇。
框架概覽
TBVImageBrowser的主要組成如下:
從圖一可以看出偎球,TBVImageBrowserView持有了一個遵守TBVImageProviderManagerProtocol的對象洒扎。根據(jù)此持有的策略管理對象,可以通過抽象策略接口TBVImageProviderProtocol訪問對應(yīng)的具體策略類:TBVWebImageProvider衰絮、TBVLocalImageProvider逊笆、TBVAssetImageProvider和自定義的Provider。
實際上具體的策略都可以由使用者實現(xiàn)岂傲,也就是說圖一中的TBVWebImageProvider、TBVLocalImageProvider子檀、TBVAssetImageProvider都可以去除镊掖,只要提供遵守策略接口TBVImageProviderProtocol的具體策略類就行了。一般來說褂痰,訪問資源的策略由使用者提供亩进,因為使用者知道自己實際的獲取方式。
從圖二中可以看出缩歪,TBVImageBrowserView持有的策略管理對象的內(nèi)部組成归薛。只要遵守TBVImageProviderManagerProtocol協(xié)議,都可以成為策略管理對象匪蝙。
除了以上幾個協(xié)議主籍,我還抽出了TBVImageIdentifierProtocol、TBVImageElementProtocol以及TBVImageProgressPresenterProtocol協(xié)議逛球。
TBVImageProviderIdentifierProtocol的聲明如下:
@protocol TBVImageIdentifierProtocol <NSObject>
@required
@property (strong, nonatomic, readonly) NSString *identifier;
@end
identifier作為匹配Provider和資源類型的標志千元,是每個策略必須要實現(xiàn)的。
TBVImageElementProtocol的聲明如下:
@protocol TBVImageElementProtocol <TBVImageIdentifierProtocol>
@required
@property (strong, nonatomic) NSObject *resource;
@property (assign, nonatomic) CGFloat progress;
@optional
@property (strong, nonatomic) NSDictionary *options;
@end
TBVImageElementProtocol遵守了TBVImageProviderIdentifierProtocol協(xié)議颤绕,提供解析自身資源的Provider標志幸海。resource用來存儲實際需要獲取的資源,progress則表示獲取的進度奥务。
TBVImageProgressPresenterProtocol的聲明如下:
@protocol TBVImageProgressPresenterProtocol <NSObject>
+ (instancetype)presenter;
- (void)setPresenterProgress:(CGFloat)progress animated:(BOOL)animated;
@end
由于項目中可能有自己的一套loading progress控件物独,僅僅為了圖片選擇器而引入另一套控件是不劃算的,所以BVImageBrowser的loading progress控件也讓使用者來提供氯葬,盡量減少不必要依賴挡篓。
TBVImageProviderManager
TBVImageProviderManager幫助TBVImageBrowserView管理所有添加的策略,讓TBVImageBrowserView得以關(guān)注其瀏覽業(yè)務(wù)本身溢谤,而不必摻雜獲取資源的具體邏輯瞻凤。
首先是添加刪除策略接口:
- (void)addImageProvider:(id<TBVImageProviderProtocol>)provider {
NSCParameterAssert(provider);
NSAssert(provider.identifier, @"identifier of %@ can not be nil.", provider);
TBVLogInfo(@"add provider %@", provider);
@synchronized (self) {
self.providerMap[provider.identifier] = provider;
}
}
- (BOOL)removeImageProvider:(id<TBVImageProviderProtocol>)provider {
NSAssert(provider.identifier, @"identifier of %@ can not be nil.", provider);
TBVLogInfo(@"remove provider %@", provider);
@synchronized (self) {
[self.providerMap removeObjectForKey:provider.identifier];
return [self.providerMap.allKeys containsObject:provider.identifier];
}
}
TBVImageProviderManager中會聲明一個providerMap字典,以策略的identifier作key世杀,策略作為value阀参。
接下來是獲取資源的接口:
- (RACSignal *)imageSignalForElement:(id<TBVImageElementProtocol>)element {
NSAssert(element.identifier, @"identifier of %@ can not be nil.", element);
@weakify(self)
return [[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
@strongify(self)
TBVLogInfo(@"\nimage resource:\n\t%@;\nidentifier:\n\t%@;\n", element.resource, element.identifier);
if ([self.providerMap.allKeys containsObject:element.identifier]) {
[subscriber sendNext:[self.providerMap[element.identifier]
imageSignalForElement:element
progress:^(CGFloat progress) {
element.progress = progress;
}]];
[subscriber sendCompleted];
} else {
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
userInfo[kTBVImageBrowserErrorKey] =
[NSString stringWithFormat:@"image provider with identifier %@ was not found", element.identifier];
[subscriber sendError:[NSError errorWithDomain:@"TBVImageProviderManager"
code:-1
userInfo:userInfo]];
}
return nil;
}]
switchToLatest]
catch:^RACSignal *(NSError *error) {
TBVLogError(@"\nerror domain: \n\t%@; \nerror code: \n\t%ld; \nerror info: \n\t%@;\n", error.domain, error.code, error.userInfo);
return [RACSignal empty];
}];
}
TBVImageProviderManager根據(jù)element提供的identifier,去providerMap字典中查找匹配的策略瞻坝,并調(diào)用策略接口蛛壳,獲取element的resource中存儲的資源杏瞻。
載入自定義loading progress控件
在加載一個loading progress控件時,我需要什么樣的接口?
首先是控件本身衙荐,TBVImageBrowserView需要使用者創(chuàng)建這個控件的實體給TBVImageBrowserView捞挥,而控件的具體屬性則由調(diào)用者在創(chuàng)建控件時一并設(shè)置。然后因為是loading progress控件忧吟,理所當然地應(yīng)該提供設(shè)置progress的接口砌函。由這兩個需求催生TBVImageProgressPresenterProtocol協(xié)議,來對使用者提供的loading progress控件進行限定溜族。
有了滿足要求的控件讹俊,如何在內(nèi)部進行創(chuàng)建?TBVImageBrowserView需要使用者提供控件對應(yīng)Class煌抒,然后在內(nèi)部以以下方式進行添加:
- (void)setupProgressPresenter:(Class)progressPresenter{
if (self.progressView || !progressPresenter) return;
if ([progressPresenter conformsToProtocol:@protocol(TBVImageProgressPresenterProtocol)]) {
id presenter = [progressPresenter presenter];
if ([presenter isKindOfClass:[UIView class]]) {
self.progressView = presenter;
[self.contentView addSubview:self.progressView];
CGSize size = CGSizeEqualToSize(CGSizeZero, self.progressView.frame.size) ?
CGSizeMake(40.0f, 40.0f) : self.progressView.frame.size ;
[self.progressView mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.equalTo(@(size.width));
make.height.equalTo(@(size.height));
make.center.equalTo(self.contentView);
}];
} else {
TBVLogError(@"progressPresenter should be subclass of UIView.");
}
} else {
TBVLogError(@"progressPresenter should comfirm TBVImageProgressPresenterProtocol.");
}
}
至此仍劈,載入自定義的loading progress控件已經(jīng)實現(xiàn)了。接下來以DACircularProgress控件為例寡壮,說明如何使用贩疙。
首先,創(chuàng)建DALabeledCircularProgressView的分類况既,然后在分類中遵守TBVImageProgressPresenterProtocol協(xié)議这溅,并實現(xiàn)其中的接口:
@implementation DALabeledCircularProgressView (TBVImageProgressPresenter)
+ (instancetype)presenter {
DALabeledCircularProgressView *progressView = [[DALabeledCircularProgressView alloc] initWithFrame:CGRectMake(0, 0, 40, 40)];
progressView.thicknessRatio = 0.1;
progressView.progressLabel.textColor = [UIColor whiteColor];
progressView.progressLabel.font = [UIFont systemFontOfSize:12];
progressView.userInteractionEnabled = NO;
return progressView;
}
- (void)setPresenterProgress:(CGFloat)progress animated:(BOOL)animated {
[self setProgress:progress animated:animated];
if (progress != 0 && progress != 1) TBVLogDebug(@"load progress %f", progress);
self.progressLabel.text = [NSString stringWithFormat:@"%.02f", progress];
}
@end
并且在初始化TBVImageBrowserView時,傳入DALabeledCircularProgressView類:
_configuration.progressPresenterClass = [DALabeledCircularProgressView class];
總結(jié)
TBVImageBrowser是在自己做IM發(fā)送相冊圖片時造的輪子坏挠,由于后期項目本身并沒有使用SDWebImage芍躏,并且有一套自己訪問相冊的策略,所以MWPhotoBrowser并不是很符合自己的需求降狠。
TBVImageBrowser遵循了一個原則:使用者應(yīng)該知道自己如何得到資源对竣,并向框架提供獲取資源的方法,這樣才能讓框架具有更好的擴展性榜配。
詳細的使用方法在倉庫說明中否纬,歡迎試用并一起完善這個項目。