利用策略模式增強圖片瀏覽器的擴展性

說到圖片瀏覽器太雨,項目中比較常用的成熟框架有Objective-C版本的MWPhotoBrowserIDMPhotoBrowser或者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)該知道自己如何得到資源对竣,并向框架提供獲取資源的方法,這樣才能讓框架具有更好的擴展性榜配。

詳細的使用方法在倉庫說明中否纬,歡迎試用并一起完善這個項目。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蛋褥,一起剝皮案震驚了整個濱河市临燃,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌烙心,老刑警劉巖膜廊,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異淫茵,居然都是意外死亡爪瓜,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門匙瘪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來铆铆,“玉大人蝶缀,你說我怎么就攤上這事”』酰” “怎么了翁都?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長谅猾。 經(jīng)常有香客問我柄慰,道長,這世上最難降的妖魔是什么税娜? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任先煎,我火速辦了婚禮,結(jié)果婚禮上巧涧,老公的妹妹穿的比我還像新娘。我一直安慰自己遥倦,他們只是感情好谤绳,可當我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著袒哥,像睡著了一般缩筛。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上堡称,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天瞎抛,我揣著相機與錄音,去河邊找鬼却紧。 笑死桐臊,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的晓殊。 我是一名探鬼主播断凶,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼巫俺!你這毒婦竟也來了认烁?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤介汹,失蹤者是張志新(化名)和其女友劉穎却嗡,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體嘹承,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡窗价,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了赶撰。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片舌镶。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡柱彻,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出餐胀,到底是詐尸還是另有隱情哟楷,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布否灾,位于F島的核電站卖擅,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏墨技。R本人自食惡果不足惜惩阶,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望扣汪。 院中可真熱鬧断楷,春花似錦、人聲如沸崭别。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽茅主。三九已至舞痰,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間诀姚,已是汗流浹背响牛。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留赫段,地道東北人呀打。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像糯笙,于是被迫代替她去往敵國和親聚磺。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫炬丸、插件瘫寝、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,022評論 4 62
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,501評論 25 707
  • 上周就報名練字,但是一直拖著沒練字稠炬,作業(yè)也沒交焕阿,直到今天看到群里老師發(fā)的打卡記錄,心里有點不好受了首启,真覺得不應(yīng)該暮屡,...
    biliali閱讀 157評論 0 0
  • 如果你愛自己的寶貝, 就請多愛自己的鄉(xiāng)鄰毅桃。 鄉(xiāng)鄰家的孩子褒纲, 一定是寶貝成長中的游戲准夷。 如果你愛自己的寶貝, 就請多...
    花椒粒兒范范閱讀 187評論 2 5
  • 1. 我發(fā)誓楔绞,我是在和波力通話以后才看到今天的晨讀內(nèi)容。 但是一切都那么自然而然地發(fā)生了… 2. 波力說唇兑,曾經(jīng)的你...
    徐鎂鑫閱讀 563評論 14 28