// 2017.8.24 更新
进阶篇:再谈 Swift 换肤功能,看完该文后强烈建议看下进阶篇,彩蛋彩蛋彩蛋哦。
在我过去的一个多月里,发现很多不愉快的事情,导致 OpenGL SE系列的文章好久没有更新了,今天来分享下以前做的主题管理来做一个新开始,OpenGL SE的文章会继续坚持写下去,欢迎关注。
只需@3x图片
现在工作改做SDK后,发现很少和界面相关的东西打交道了,但做过APP的同学们都应该知道,为了适应各种屏幕的尺寸,图片资源需要提供@1x、@2x和@3x来适配屏幕界面,现在基本没有 1x屏幕的设备了,可以不用提供这个分辨率的图片了。但@2x和@3x可以说是重复的资源,这只会增大应用包的大小。
在这,我只使用@3x的图片来做适配:
先写在前面,iOS 8后系统自动会将@3x图片自动适配图片,也就是说你的应用不支持iOS 8以下系统的话,你可以直接使用@3x的资源就可以了,你可以直接跳过这一节。
这里为了实现换肤功能,所有的资源我都会存在Bundle里面,首先解释下,Bundle是静态的,作为一个资源包是不参加项目编译的,也就是说,bundle包中不能包含可执行的文件,它仅仅是作为资源,被解析成为特定的2进制数据。对于在iOS 8系统上会自动将@3x的资源自动适配后,我们只需要考虑iOS 8下的系统,这个时候我们只需要手动去重新绘制图片的大小(比较消耗性能的动作),实现如下:
Swift:
private func scaledImageFrom3x() -> UIImage {
let locScale = UIScreen.mainScreen().scale
let theRate: CGFloat = 1.0 / 3.0
let oldSize = self.size
let scaleWidth = CGFloat(oldSize.width) * theRate
let scaleHeight = CGFloat(oldSize.height) * theRate
var scaleRect = CGRectZero
scaleRect.size.width = scaleWidth
scaleRect.size.height = scaleHeight
UIGraphicsBeginImageContextWithOptions(scaleRect.size, false, locScale)
drawInRect(scaleRect)
var newImage = UIImage()
newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return newImage
}
OC:
- (UIImage *)scaledImageFrom3x
{
float locScale = [UIScreen mainScreen].scale;
float theRate = 2.0 / 3.0;
UIImage *newImage = nil;
CGSize oldSize = self.size;
CGFloat scaledWidth = oldSize.width * theRate;
CGFloat scaledHeight = oldSize.height * theRate;
CGRect scaledRect = CGRectZero;
scaledRect.size.width = scaledWidth;
scaledRect.size.height = scaledHeight;
UIGraphicsBeginImageContextWithOptions(scaledRect.size, NO, locScale);
[self drawInRect:scaledRect];
newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
if(newImage == nil) {
NSLog(@"could not scale image");
}
return newImage;
}
换肤功能
换肤功能,其实就是图片和颜色等资源的切换,也就是说你有几套皮肤,就提供对应的几套资源,当切换皮肤的时候,切换资源访问的路径并发出要换肤的通知,当前界面监听换肤的通知后再去刷新界面就完成了换肤的功能了。
我们实现一个ThemeManager的主题管理类,应用的所有资源访问都通过这个类来实现统一管理,所有的主题基本上都是由颜色和资源(图片,音频,文本等)来决定的,所以换肤时只要更改主题颜色库(themeColors)和主题资源库(themeBundle),实现如下:
Swift:
class CPThemeManager: NSObject {
private var themeStyle: CPThemeType?
private var themeBundle: NSBundle?
private var themeColors: Dictionary<String, AnyObject>?
// MARK: 单例
static let shareInstance = CPThemeManager()
private override init() {}
}
OC:
static BTThemeManager * _themeManager = nil;
@interface BTThemeManager () {
NSDictionary *_themeColors;
}
@property (nonatomic, strong) NSDictionary *themeColors;
@property (nonatomic, strong) NSBundle *themeBundle;
@end
@implementation BTThemeManager
@synthesize themeStyle = _themeStyle;
@synthesize themeColors = _themeColors;
+ (BTThemeManager *)getInstance
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_themeManager = [[BTThemeManager alloc]init];
});
return _themeManager;
}
- (id) init
{
if (self = [super init]) {}
return self;
}
@end
上面也说了,换肤实质只是更换资源访问的路径,所以提供一个设置主题的方法来进行资源路径的设置(setThemeStyle),在重新设置完资源路径后,再对外发出更新界面的通知,实现如下:
Swift:
func setThemeStyle(themeStyle: CPThemeType) {
//设置资源路径
NSNotificationCenter.defaultCenter().postNotificationName("CPThemeChangeNotification", object: nil)
}
OC:
- (void)setThemeStyle:(BTThemeType)themeStyle
{
if (_themeStyle == themeStyle ) {
return;
}
_themeStyle = themeStyle;
//设置资源路径
[[NSNotificationCenter defaultCenter] postNotificationName:BTThemeChangeNotification object:nil];
}
下面将说整个主题管理功能的最重要一步:监听主题的切换。
首先定义一个需要更新主题的协议方法:
Swift:
protocol CPThemeListenerProtocol {
func CPThemeDidNeedUpdateStyle() -> Void
}
OC:
@protocol BTThemeListenerProtocol <NSObject>
- (void) BTThemeDidNeedUpdateStyle;
@end
然后在主题管理理里面添加一个注册监听主题切换方法:
Swift:
func addThemeListener(object: CPBaseViewController) {
NSNotificationCenter.defaultCenter().addObserver(object,
selector:#selector(object.CPThemeDidNeedUpdateStyle),
name: "CPThemeChangeNotification",
object: nil)
}
func removeThemeListener(object: AnyObject) {
NSNotificationCenter.defaultCenter().removeObserver(object)
}
因为Swift selector现在只能通过类名.方法名来设置,导致如果要使用则必须要继承一个基类,如果你们有更好的方法,求分享下。
OC:
- (void)addThemeListener:(id )obj
{
if([obj respondsToSelector:@selector(BTThemeDidNeedUpdateStyle)]){
[[NSNotificationCenter defaultCenter] addObserver:obj selector:@selector(BTThemeDidNeedUpdateStyle) name:BTThemeChangeNotification object:nil];
}
}
- (void) removeThemeListener:(id)obj
{
if (obj) {
[[NSNotificationCenter defaultCenter] removeObserver:obj];
}
}
最后在要实现主题切换的页面里添加主题管理类的监听切换方法,并实现协议的方法,把需要做主题切换的资源访问都放在这个方法里面,然后就搞定啦。搞了?好像少了点什么,还没有说如何去访问资源呢,这个我想大家都能自己去去实现,就是在基类里实现一个统一访问资源的方法:
- (void )BTThemeImage:(NSString *)imageName completionHandler:(void (^)(UIImage *image))handler;
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 耗时的操作
NSString *imagePath = [NSString stringWithFormat:@"image/%@",imageName];
UIImage *image = nil;
//通过资源路径去访问
if (image == nil) {
image = [UIImage imageNamed:imageName];
}
dispatch_async(dispatch_get_main_queue(), ^{
// 更新界面
handler(image);
});
});
}
写在最后:欢迎大家一起多交流多学习,有更好的想法实现什么的, 求分享~~~