tripleCC的技术博客

ʕ•̫͡•ʔ-̫͡-ʕ•͓͡•ʔ-̫͡-ʕ•̫͡•ʔ-̫͡-ʕ•͓͡•ʔ-̫͡-ʔ

利用策略模式增强图片浏览器的扩展性

说到图片浏览器,项目中比较常用的成熟框架有Objective-C版本的MWPhotoBrowserIDMPhotoBrowser或者Swift版本的SKPhotoBrowser

从核心功能来看,MWPhotoBrowser,IDMPhotoBrowser这两个框架,都很好地实现了对本地资源、相册资源、网络资源的获取与显示。并且很好地封装了网络和相册的获取方式,在我看来,这是他的优势,但同时,高度的集成也催生了一些不足。

这样做的优势不言而喻,调用者只需要很少的几行代码,就可以集成一个图片浏览器框架,省时省力。以MWPhotoBrowser为例,在不设置额外属性的情况下,只需要下面两行代码就可以创建:

1
2
MWPhotoBrowser *browser = [[MWPhotoBrowser alloc] initWithPhotos:self.photos];
[self.navigationController pushViewController:browser animated:YES];

使用者只要关注如何提供MWPhotoBrowser所要展示资源就可以了,不需要做额外的操作,非常地简洁方便。

关于不足,由于MWPhotoBrowser内部实现了获取网络图片功能,在追求内部实现尽量精简的前提下,不可避免地要依赖加载图片的第三方库(SDWebImage)。如果原来项目并没有使用SDWebImage,而是用YYWebImage或者Kingfisher,那么使用MWPhotoBrowser便会引入冗余的框架,从而让项目额外增加了一种图片缓存机制,不利于内存以及磁盘使用率的优化。

对于相册资源的访问,MWPhotoBrowser内部也实现了通过PHAsset或者ALAsset获取相片的功能。不过一般来说,项目会有自己的一套相册选择器,进而会有相应的相册资源获取策略。所以以个人观点来看,如何获取相册资源,应该由使用者告知,而不是在框架内部自己实现一套,这样更加符合DRY。

接下来,我会针对上面的不足,实现一套兼容本地资源、相册资源、网络资源的简易图片选择器。
本文章对应的所有代码在仓库TBVImageBrowser中。

框架概览

TBVImageBrowser的主要组成如下:
图一
图二

从图一可以看出,TBVImageBrowserView持有了一个遵守TBVImageProviderManagerProtocol的对象。根据此持有的策略管理对象,可以通过抽象策略接口TBVImageProviderProtocol访问对应的具体策略类:TBVWebImageProvider、TBVLocalImageProvider、TBVAssetImageProvider和自定义的Provider。

实际上具体的策略都可以由使用者实现,也就是说图一中的TBVWebImageProvider、TBVLocalImageProvider、TBVAssetImageProvider都可以去除,只要提供遵守策略接口TBVImageProviderProtocol的具体策略类就行了。一般来说,访问资源的策略由使用者提供,因为使用者知道自己实际的获取方式。

从图二中可以看出,TBVImageBrowserView持有的策略管理对象的内部组成。只要遵守TBVImageProviderManagerProtocol协议,都可以成为策略管理对象。

除了以上几个协议,我还抽出了TBVImageIdentifierProtocol、TBVImageElementProtocol以及TBVImageProgressPresenterProtocol协议。 TBVImageProviderIdentifierProtocol的声明如下:

1
2
3
4
@protocol TBVImageIdentifierProtocol <NSObject>
@required
@property (strong, nonatomic, readonly) NSString *identifier;
@end

identifier作为匹配Provider和资源类型的标志,是每个策略必须要实现的。
TBVImageElementProtocol的声明如下:

1
2
3
4
5
6
7
@protocol TBVImageElementProtocol <TBVImageIdentifierProtocol>
@required
@property (strong, nonatomic) NSObject *resource;
@property (assign, nonatomic) CGFloat progress;
@optional
@property (strong, nonatomic) NSDictionary *options;
@end

TBVImageElementProtocol遵守了TBVImageProviderIdentifierProtocol协议,提供解析自身资源的Provider标志。resource用来存储实际需要获取的资源,progress则表示获取的进度。
TBVImageProgressPresenterProtocol的声明如下:

1
2
3
4
@protocol TBVImageProgressPresenterProtocol <NSObject>
+ (instancetype)presenter;
- (void)setPresenterProgress:(CGFloat)progress animated:(BOOL)animated;
@end

由于项目中可能有自己的一套loading progress控件,仅仅为了图片选择器而引入另一套控件是不划算的,所以BVImageBrowser的loading progress控件也让使用者来提供,尽量减少不必要依赖。

TBVImageProviderManager

TBVImageProviderManager帮助TBVImageBrowserView管理所有添加的策略,让TBVImageBrowserView得以关注其浏览业务本身,而不必掺杂获取资源的具体逻辑。

首先是添加删除策略接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (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。 接下来是获取资源的接口:

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
- (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根据element提供的identifier,去providerMap字典中查找匹配的策略,并调用策略接口,获取element的resource中存储的资源。

载入自定义loading progress控件

在加载一个loading progress控件时,我需要什么样的接口?
首先是控件本身,TBVImageBrowserView需要使用者创建这个控件的实体给TBVImageBrowserView,而控件的具体属性则由调用者在创建控件时一并设置。然后因为是loading progress控件,理所当然地应该提供设置progress的接口。由这两个需求催生TBVImageProgressPresenterProtocol协议,来对使用者提供的loading progress控件进行限定。

有了满足要求的控件,如何在内部进行创建?TBVImageBrowserView需要使用者提供控件对应Class,然后在内部以以下方式进行添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (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控件已经实现了。接下来以DACircularProgress控件为例,说明如何使用。
首先,创建DALabeledCircularProgressView的分类,然后在分类中遵守TBVImageProgressPresenterProtocol协议,并实现其中的接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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类:

1
_configuration.progressPresenterClass = [DALabeledCircularProgressView class];

总结

TBVImageBrowser是在自己做IM发送相册图片时造的轮子,由于后期项目本身并没有使用SDWebImage,并且有一套自己访问相册的策略,所以MWPhotoBrowser并不是很符合自己的需求。

TBVImageBrowser遵循了一个原则:使用者应该知道自己如何得到资源,并向框架提供获取资源的方法,这样才能让框架具有更好的扩展性。
详细的使用方法在仓库说明中。

Comments