wxxsw / SwiftTheme

🎨 Powerful theme/skin manager for iOS 9+ 主题/换肤, 暗色模式
MIT License
2.52k stars 305 forks source link

ThemePicker keypath 方式初始化的一些疑问 #27

Closed cjsliuj closed 7 years ago

cjsliuj commented 7 years ago

看了下代码,发现 ThemeFontPicker 和 ThemeDictionaryPicker 没有以keyPath为参数的初始化方法,ThemeFontPicker 没有比较好好理解,字符串确实不太好表达 Font,ThemeDictionaryPicker 没有就比较奇怪了(plist中是可以表达dictionary的呀),而且 ThemeManager+Plist 中也定义了 dictionaryForKeyPath(_),可能是我还没太理解,希望能够解释以下,谢谢~。

另外,上述两者如果没有 keypath初始化方法,那么使用的时候就会和其他picker不一致了,其他的picker只要指定 keypath,就可以自动完成根据theme切换属性值的功能,而如果是上述的两个picker的话,恐怕要外界自行根据theme去init?

cjsliuj commented 7 years ago

有个想法,plist方案无法解决保存非常规类型数据的问题,其实dictionary才是通用方案,plist方案其实可以算作其子集(只接收基础数据值类型的 dictionary),但是做动态下发有点麻烦,需先 archive, 然后下发到app再unarchive,也是可以实现。

wxxsw commented 7 years ago

Q:ThemeFontPickerThemeDictionaryPicker 没有 keyPath 的初始化方法:

A:好问题,首先看一个例子(摘自 Demo Target - AppDelegate.swift

let titleAttributes: [[String: AnyObject]] = globalBarTextColors.map { hexString in
    return [
        NSForegroundColorAttributeName: UIColor(rgba: hexString),
        NSFontAttributeName: UIFont.systemFont(ofSize: 16),
        NSShadowAttributeName: shadow
    ]
}
navigationBar.theme_titleTextAttributes = ThemeDictionaryPicker.pickerWithDicts(titleAttributes)

这是 ThemeDictionaryPicker 使用集合初始化的一个使用场景,创建一个颜色不同的字典集合,然后转换为 ThemeDictionaryPicker 后设置到导航栏的 theme_titleTextAttributes 属性中。

然而如果通过 plist 设置的话,颜色、字体、阴影都无法实例化,因为无法知道字典中某个 key 对应的是什么类型。

如果要让 SwiftTheme 支持连字典里面的内容都可以实例化,使用方式上的改动是不可避免的:

第一步:使用特定 key 的名称,例如是 NSForegroundColorAttributeName 就转为 UIColor。 第二步:使用特定格式的 value,例如 font: PingFangSC-Regular 16 。 或者使用嵌套,如果值是 font 则使用字典,里面再包含 namesize 等。

然后总结一下我看到的问题:

第一步: key 的名称长,如果不是复制过来的,没有自动补全很容易出错。 强迫用户使用特定的 key,与其它自定义的 key 使用方法不统一。

第二步: 如果使用特定格式,类似于 UIFont,是否每一种类型都要定义一种不同的格式?而且识别与提取真正的值会影响运行速度(可能影响很小)。 如果使用嵌套,虽然可以在类型中包含每个属性的值,但是每种类型的初始化方法都是不同的,如果必要的初始化参数没有,还是会增加调试的负担。

所以基于以上的原因和疑问,ThemeDictionaryPicker 现在暂时不支持通过 keyPath 初始化,ThemeFontPicker 的原因是类似的,需要统一解决方案才能做。

所以目前通过 plist 实现上面的例子大概是这样的:

    // ...
   NotificationCenter.default.addObserver(self, selector: #selector(themeUpdate), name: NSNotification.Name(rawValue: ThemeUpdateNotification), object: nil)
}

func themeUpdate() {
    let dict = ThemeManager.dictionaryForKeyPath("someKeyPath")! // 为了方便演示所以强制解包
    let titleAttributes: [String: AnyObject] = [
        NSForegroundColorAttributeName: UIColor(rgba: dict["color"] as! String),
        NSFontAttributeName: UIFont.systemFont(ofSize: dict["fontSize"] as! CGFloat)
    ]
    navigationBar.titleTextAttributes = titleAttributes
}

注册通知这里体验不是很好,之后可能会做一些改进,让使用者只关心转换的过程。 大概就是这样,这个问题当时还是比较纠结的,等于两害相权取其轻,期待你的回复。

wxxsw commented 7 years ago

Q:Dictionaryplist 更通用,plist 应该作为子集。

A:其实现在就是这样的,在 ThemeManager.swift 你可以看到 public class func setTheme(plistName: String, path: ThemePath)public class func setTheme(dict: NSDictionary, path: ThemePath) 方法,实际上前者只是从 plist 中取出 Dictionary 然后调用了后者。

cjsliuj commented 7 years ago

谢谢上面的回答。 刚又遇到了个问题。。不太好扩展啊 我想给 UIBarItem 扩展 theme_image 和 theme_selectedImage 结果发现 UIKit+Theme.swift 中,getThemePicker 和 setThemePicker 是 私有的 ,这条路不通,继续往下,发现 themePickers 是 internal 的。。。这可咋弄

cjsliuj commented 7 years ago

对了还有个问题,picker 把值检索出来后,最后给个回调让外界有机会处理一下, e.g. vc.tabBarItem.image = UIImage.init(named: "xxx")?.withRenderingMode(.alwaysOriginal) 这种场景 theme_image 就没法处理了 个人想法:目前内部已经保存了'属性设置器(setImage/setText/xxxxx)'、'值检索器(各种picker)',再添加一个'值处理器'即可

wxxsw commented 7 years ago

你可以 fork 一下项目,并在 UIKit+Theme.swift 中增加 UIBarItemextension

然后在 CocoaPods 中使用你 fork 的项目,例如:

pod 'SwiftTheme', :git => 'https://github.com/cjsliujie/SwiftTheme.git'

如果使用没有问题,欢迎提个 Pull Request

wxxsw commented 7 years ago

plist 因为是设置 keyPath,而不像设置数组那样可以接触到值,没有办法对值进行处理,这个确实可以增加设置的方法,你说的值处理器是如何实现的,可以详细说一下,我现在还没有什么 idea

顺便提一下:如果我没记错的话 tabBarItem 动态改变 image 是不会在界面上生效的,相关的 #26

cjsliuj commented 7 years ago

谢谢上面关于 tabBarItem 的提醒,节省我不少调试时间 关于 '值处理器',大概想法是: 外界如何设置:创建picker的时候可以同时选择传入一个闭包,其中包含了值处理逻辑 内部如何保存:与 themePickers 类似,可以再搞个字典 ,key 仍然是 selector,value 是 '值处理器' 执行时机:performThemePicker 中 ,真正的 perform 之前

上面是刚开始下意识的想法,后来转念一想,更加通用的模型是: 内部更新属性前调用外部设置的回调,让外部有机会处理 内部更新完属性后调用外部设置的回调,让外部有机会处理

这样可以解决 如你上面说的 tabBarItem 的问题。且相比通知,代码逻辑更加紧凑。

总结下就是:目前 SwiftTheme 完全托管了属性的设置与属性值的检索,但是最好能够提供一种方式,让外部有机会介入这个过程,这样会更灵活。(突然想到:这样的话,外部使用者可以自定义非基本数据类型的 string形式 的字面量表达式,picker检索出来后 ,外部自己去转换成合适的值)

wxxsw commented 7 years ago

大概明白你的意思,是不是类似于:

imageView.theme_image = ThemeImagePicker(keyPath: "someKeyPath") { stringValue in
    return UIImage(urlString: stringValue)!.withRenderingMode(.alwaysOriginal)
}

imageView.theme_image = ThemeImagePicker(keyPath: "someKeyPath").map { imageValue in
    return imageValue.withRenderingMode(.alwaysOriginal)
}
cjsliuj commented 7 years ago

wxxsw commented 7 years ago

在版本 0.3.3 中增加了提供外部处理的初始化和类方法,增加了一个 map 参数,可以传入一个处理函数。

下面是一个例子,设置导航栏的标题文字属性:

navigationBar.theme_titleTextAttributes = ThemeDictionaryPicker(keyPath: "Global.barTextColor", map: { value in
    guard
        let rgba = value as? String,
        let color = try? UIColor(rgba_throws: rgba) else {
            return nil
    }

    let shadow = NSShadow(); shadow.shadowOffset = CGSize.zero
    let titleTextAttributes = [
        NSForegroundColorAttributeName: color,
        NSFontAttributeName: UIFont.systemFont(ofSize: 16),
        NSShadowAttributeName: shadow
    ]

   return titleTextAttributes
})

在之前这只能通过注册一个主题切换的通知来实现,现在可以灵活的进行转换,然后由 SwiftTheme 继续做主题切换的工作。

也就是说,如果传入 map 函数,keyPath 的值可以是任意类型。所以 map 给你的参数永远是一个 Any? 类型,你知道 keyPath 究竟对应的是什么类型(字符串字典数组或是别的什么),处理并最后返回 ThemeDictionaryPicker 需要的字典即可。

另外,所有 ThemePicker 的子类现在都拥有这个方法,感谢 @cjsliujie 提供的 idea !