iOS13的暗黑模式来了,项目最低支持iOS9怎么办?

苹果爸爸总是让人又爱又恨啊,今年的暗黑模式注定要让iOS开发者折腾半天。但是也再次体现了iOS开发者的价值,iOS生态独特的特性和其不断的变化与进步,才让iOS开发者始终被人铭记,不会完全被大前端和多端统一技术给淹没。从这个角度来说,要感谢苹果爸爸😘

说回正题,iOS13的dark mode相关API只能在iOS13以后才能使用。但是大部分的项目都还是会坚持支持老系统,以获取更多的用户。现在网上有许多关于iOS13 dark mode的适配文章,相关的技术点都很简单。主要的是字体颜色、图片的适配。看过之后,内心更加悲凉,iOS13 dark mode适配我都会了,老系统肿么办呢😂?

你需要一个轻量级、api友好、高度自定义且最低支持iOS9+的换肤方案。别担心!我的战友👬 ,让我为你推荐JXTheme方案,它主要借鉴了iOS13的暗黑模式适配API,使用JXTheme你会感到非常亲切。而且当你的应用最低支持iOS13时,可以方便的从JXTheme切换到系统API。

Github地址

大家可以先进入github地址,看一下效果。JXTheme Github地址

让我们从整个暗黑模式适配的流程来熟悉JXTheme的原理:

1.如何优雅的设置主题属性

通过给控件扩展命名空间属性theme,类似于SnapKitsnpKingfisherkf,这样可以将支持主题修改的属性,集中到theme属性。这样比直接给控件扩展属性theme_backgroundColor更加优雅。 核心代码如下:

view.theme.backgroundColor = ThemeProvider({ (style) in
    if style == .dark {
        return .white
    }else {
        return .black
    }
})
复制代码

2.如何根据传入的style配置对应的值

借鉴iOS13系统APIUIColor(dynamicProvider: <UITraitCollection) -> UIColor>)。自定义ThemeProvider结构体,初始化器为init(_ provider: @escaping ThemePropertyProvider<T>)。传入的参数ThemePropertyProvider是一个闭包,定义为:typealias ThemePropertyProvider<T> = (ThemeStyle) -> T。这样就可以针对不同的控件,不同的属性配置,实现最大化的自定义。 核心代码参考第一步示例代码。

3.如何保存主题属性配置闭包

对控件添加Associated object属性providers存储ThemeProvider。 核心代码如下:

public extension ThemeWrapper where Base: UIView {
    var backgroundColor: ThemeProvider<UIColor>? {
        set(new) {
            if new != nil {
                let baseItem = self.base
                let config: ThemeCustomizationClosure = {[weak baseItem] (style) in
                    baseItem?.backgroundColor = new?.provider(style)
                }
                //存储在扩展属性providers里面
                var newProvider = new
                newProvider?.config = config
                self.base.providers["UIView.backgroundColor"] = newProvider
                ThemeManager.shared.addTrackedObject(self.base, addedConfig: config)
            }else {
                self.base.configs.removeValue(forKey: "UIView.backgroundColor")
            }
        }
        get { return self.base.providers["UIView.backgroundColor"] as? ThemeProvider<UIColor> }
    }
}
复制代码

4.如何记录支持主题属性的控件

为了在主题切换的时候,通知到支持主题属性配置的控件。通过在设置主题属性时,就记录目标控件。 核心代码就是第3步里面的这句代码:

ThemeManager.shared.addTrackedObject(self.base, addedConfig: config)
复制代码

然后它会被记录到ThemeManagertrackedHashTable属性里面。因为trackedHashTableNSHashTable<AnyObject>.init(options: .weakMemory),通过弱引用记录控件,所以不存在内存问题。

5.如何切换主题并调用主题属性配置闭包

通过ThemeManager.changeTheme(to: style)完成主题切换,方法内部再调用被追踪的控件的providers里面的ThemeProvider.provider主题属性配置闭包。 核心代码如下:

public func changeTheme(to style: ThemeStyle) {
    currentThemeStyle = style
    self.trackedHashTable.allObjects.forEach { (object) in
        if let view = object as? UIView {
            view.providers.values.forEach { self.resolveProvider($0) }
        }
    }
}
private func resolveProvider(_ object: Any) {
    //castdown泛型
    if let provider = object as? ThemeProvider<UIColor> {
        provider.config?(currentThemeStyle)
    }else ...
}
复制代码

预览

preview

<figcaption></figcaption>

特性

  • [x] 支持iOS 9+,让你的APP更早的实现DarkMode;
  • [x] 使用theme命名空间属性:view.theme.xx = xx。告别theme_xx属性扩展用法;
  • [x] 使用ThemeProvider传入闭包配置。根据不同的ThemeStyle完成主题属性配置,实现最大化的自定义;
  • [x] ThemeStyle可通过extension自定义style,不再局限于lightdark;
  • [x] 提供customization属性,作为主题切换的回调入口,可以灵活配置任何属性。不再局限于提供的backgroundColortextColor等属性;
  • [x] 支持控件设置overrideThemeStyle,会影响到其子视图;
  • [x] 提供根据ThemeStyle配置属性的常规封装、Plist文件静态加载、服务器动态加载示例;

使用示例

扩展ThemeStyle添加自定义style

ThemeStyle内部仅提供了一个默认的unspecifiedstyle,其他的业务style需要自己添加,比如只支持lightdark,代码如下:

extension ThemeStyle {
    static let light = ThemeStyle(rawValue: "light")
    static let dark = ThemeStyle(rawValue: "dark")
}
复制代码

基础使用

view.theme.backgroundColor = ThemeProvider({ (style) in
    if style == .dark {
        return .white
    }else {
        return .black
    }
})
imageView.theme.image = ThemeProvider({ (style) in
    if style == .dark {
        return UIImage(named: "catWhite")!
    }else {
        return UIImage(named: "catBlack")!
    }
})
复制代码

自定义属性配置

view.theme.customization = ThemeProvider({[weak self] style in
    //可以选择任一其他属性
    if style == .dark {
        self?.view.bounds = CGRect(x: 0, y: 0, width: 30, height: 30)
    }else {
        self?.view.bounds = CGRect(x: 0, y: 0, width: 80, height: 80)
    }
})
复制代码

配置封装示例

JXTheme是一个提供主题属性配置的轻量级基础库,不限制使用哪种方式加载资源。下面提供的三个示例仅供参考。

常规配置封装示例

一般的换肤需求,都会有一个UI标准。比如UILabel.textColor定义三个等级,代码如下:

enum TextColorLevel: String {
    case normal
    case mainTitle
    case subTitle
}
复制代码

然后可以封装一个全局函数传入TextColorLevel返回对应的配置闭包,就可以极大的减少配置时的代码量,全局函数如下:

func dynamicTextColor(_ level: TextColorLevel) -> ThemeProvider<UIColor> {
    switch level {
    case .normal:
        return ThemeProvider({ (style) in
            if style == .dark {
                return UIColor.white
            }else {
                return UIColor.gray
            }
        })
    case .mainTitle:
        ...
    case .subTitle:
        ...
    }
}
复制代码

主题属性配置时的代码如下:

themeLabel.theme.textColor = dynamicTextColor(.mainTitle)
复制代码

本地Plist文件配置示例

常规配置封装一样,只是该方法是从本地Plist文件加载配置的具体值,具体代码参加ExampleStaticSourceManager

根据服务器动态添加主题

常规配置封装一样,只是该方法是从服务器加载配置的具体值,具体代码参加ExampleDynamicSourceManager

有状态的控件

某些业务需求会存在一个控件有多种状态,比如选中与未选中。不同的状态对于不同的主题又会有不同的配置。配置代码参考如下:

statusLabel.theme.textColor = ThemeProvider({[weak self] (style) in
    if self?.statusLabelStatus == .isSelected {
        //选中状态一种配置
        if style == .dark {
            return .red
        }else {
            return .green
        }
    }else {
        //未选中状态另一种配置
        if style == .dark {
            return .white
        }else {
            return .black
        }
    }
})
复制代码

当控件的状态更新时,需要刷新当前的主题属性配置,代码如下:

func statusDidChange() {
    statusLabel.theme.textColor?.refresh()
}
复制代码

如果你的控件支持多个状态属性,比如有textColorbackgroundColorfont等等,你可以不用一个一个的主题属性调用refresh方法,可以使用下面的代码完成所有配置的主题属性刷新:

func statusDidChange() {
    statusLabel.theme.refresh()
}
复制代码

overrideThemeStyle

不管主题如何切换,overrideThemeStyleParentView及其子视图的themeStyle都是dark

overrideThemeStyleParentView.theme.overrideThemeStyle = .dark
复制代码

其他说明

为什么使用theme命名空间属性,而不是使用theme_xx扩展属性呢?

  • 如果你给系统的类扩展了N个函数,当你在使用该类时,进行函数索引时,就会有N个扩展的方法干扰你的选择。尤其是你在进行其他业务开发,而不是想配置主题属性时。
  • KingfisherSnapKit等知名三方库,都使用了命名空间属性实现对系统类的扩展,这是一个更Swift的写法,值得学习。

主题切换通知

extension Notification.Name {
    public static let JXThemeDidChange = Notification.Name("com.jiaxin.theme.themeDidChangeNotification")
}
复制代码

ThemeManager根据用户ID存储主题配置

/// 配置存储的标志key。可以设置为用户的ID,这样在同一个手机,可以分别记录不同用户的配置。需要优先设置该属性再设置其他值。
public var storeConfigsIdentifierKey: String = "default"
复制代码

迁移到系统API指南

当你的应用最低支持iOS13时,如果需要的话可以按照如下指南,迁移到系统方案。 迁移到系统API指南,点击阅读

Github地址

最后再复习一下github地址,点击进入查看更多细节。JXTheme Github地址

作者:金字塔程序员
链接:https://juejin.im/post/5d882ff85188253ec722e21f

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,588评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,456评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,146评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,387评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,481评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,510评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,522评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,296评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,745评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,039评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,202评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,901评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,538评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,165评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,415评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,081评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,085评论 2 352

推荐阅读更多精彩内容