Apple dad is always loved and hated, and this year’s Dark mode is sure to give iOS developers a hard time. But it also shows the value of iOS developers again. The unique characteristics of the iOS ecosystem and its constant change and progress, so that iOS developers are always remembered, not completely overwhelmed by the big front-end and multi-terminal unified technology. In that respect, thanks to Apple dad 😘

Anyway, the dark mode API for iOS13 will only be available after iOS13. But most projects stick with the old system in order to get more users. There are a lot of iOS13 dark mode adaptations out there, and the technical points are pretty simple. The main is font color, picture adaptation. After reading, the heart is more sad, iOS13 dark mode adaptation I will, how to do the old system 😂?

You need a lightweight, API-friendly, highly customizable, iOS9+ minimum peels. Don’t worry! My comrade 👬, let me recommend JXTheme for you, it mainly borrowed from iOS13 dark mode adaptation API, using JXTheme you will feel very familiar. And if your app supports at least iOS13, you can easily switch from the JXTheme to the system API.

Making the address

You can go to the Github address first to see the effect. JXTheme making address

Let’s familiarize ourselves with the JXTheme through the entire Dark mode adaptation process:

1. How to elegantly set theme properties

By extending the theme property to the control namespace, similar to SnapKit’s SNP and Kingfisher’s KF, you can concentrate the theme property that supports theme modification. This is more elegant than extending the control’s property theme_backgroundColor directly. The core code is as follows:

view.theme.backgroundColor = ThemeProvider({ (style) in
    if style == .dark {
        return .white
    }else {
        return .black
    }
})
Copy the code

2. How to configure the corresponding value based on the style passed in

IOS13 APIUIColor(dynamicProvider:

UIColor>). Customize the ThemeProvider structure with the init(_ provider: @escaping ThemePropertyProvider

) initializer. The argument ThemePropertyProvider passed in is a closure defined as typeAlias ThemePropertyProvider

= (ThemeStyle) -> T. This allows for maximum customization for different controls and different property configurations. The core code refers to the sample code in step 1.


)>

3. How do I save the topic property configuration closure

Add the Associated object property providers to the control to store the ThemeProvider. The core code is as follows:

public extension ThemeWrapper where Base: UIView {
    var backgroundColor: ThemeProvider<UIColor>? {
        set(new) {
            ifnew ! =nil {
                let baseItem = self.base
                let config: ThemeCustomizationClosure = {[weak baseItem] (style) inbaseItem? .backgroundColor = new? .provider(style) }// Stored in the extended property providers
                varnewProvider = new newProvider? .config = configself.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>}}}Copy the code

4. How to record controls that support theme properties

To notify controls that support the configuration of theme properties when the theme is switched. By logging the target control when the theme property is set. The core code is this line from step 3:

ThemeManager.shared.addTrackedObject(self.base, addedConfig: config)
Copy the code

It will then be logged to the trackedHashTable property of ThemeManager. Because trackedHashTable is NSHashTable

.init(options:.weakMemory), through weak reference record control, there is no memory problem.

5. How do I switch topics and invoke topic property configuration closures

Thememanager.changetheme (to: style) is used to complete the theme switch, and the method calls the themeProvider.provider theme property configuration closure of the traced control’s providers. The core code is as follows:

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 generics
    if let provider = object as? ThemeProvider<UIColor> { provider.config? (currentThemeStyle) }else. }Copy the code

preview

features

  • Support iOS 9+, let your APP earlier implementationDarkMode;
  • usethemeNamespace attributes:view.theme.xx = xx. Say goodbye totheme_xxAttribute extension usage;
  • useThemeProviderPassing in the closure configuration. According to differentThemeStyleComplete the topic attribute configuration to achieve maximum customization;
  • ThemeStylethroughextensionCustom styles are no longer limited tolightanddark;
  • providecustomizationProperty, as a callback entry for topic switching, you can flexibly configure any property. No longer limited to what is offeredbackgroundColor,textColorAnd other properties;
  • Support control SettingsoverrideThemeStyle, which affects its child views;
  • Provide according toThemeStyleGeneral encapsulation of configuration properties, Plist file static loading, server dynamic loading examples;

Use the sample

extensionThemeStyleAdd custom styles

ThemeStyle provides only a default unspecifiedstyle internally, other business styles need to be added, such as only support light and dark, code as follows:

extension ThemeStyle {
    static let light = ThemeStyle(rawValue: "light")
    static let dark = ThemeStyle(rawValue: "dark")}Copy the code

Based on using

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")! }})Copy the code

Custom property configuration

view.theme.customization = ThemeProvider({[weak self] style in
    // You can select any other properties
    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)}})Copy the code

Configuration Encapsulation Example

JXTheme is a lightweight base library that provides configuration of theme properties without limiting how resources are loaded. The three examples provided below are for your reference only.

General configuration encapsulation example

In general, there is a UI standard for peels. For example, uilabel.textColor defines three levels as follows:

enum TextColorLevel: String {
    case normal
    case mainTitle
    case subTitle
}
Copy the code

TextColorLevel returns the corresponding configuration closure, which can greatly reduce the amount of configuration code. The global function is as follows:

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: ... }}Copy the code

The following code is used to configure the topic properties:

themeLabel.theme.textColor = dynamicTextColor(.mainTitle)
Copy the code

Example of local Plist file configuration

As with normal configuration encapsulation, except that this method loads the specific value of the configuration from the local Plist file, and the specific code participates in Example’s StaticSourceManager class

Dynamically add themes based on the server

As with normal configuration encapsulation, except that this method loads the configuration concrete value from the server, and the concrete code participates in Example’s DynamicSourceManager class

Stateful controls

Some business requirements have a control with multiple states, such as selected and unchecked. Different states have different configurations for different topics. The configuration code is as follows:

statusLabel.theme.textColor = ThemeProvider({[weak self] (style) in
    if self? .statusLabelStatus == .isSelected {// Select a configuration
        if style == .dark {
            return .red
        }else {
            return .green
        }
    }else {
        // State another configuration is not selected
        if style == .dark {
            return .white
        }else {
            return .black
        }
    }
})
Copy the code

When the status of the control is updated, the current topic property configuration needs to be refreshed as follows:

func statusDidChange(a){ statusLabel.theme.textColor? .refresh() }Copy the code

If your control supports multiple state properties, such as textColor, backgroundColor, font, etc., you can use the following code to refresh all configured theme properties instead of calling refresh one by one:

func statusDidChange(a) {
    statusLabel.theme.refresh()
}
Copy the code

overrideThemeStyle

No matter how theme switch, overrideThemeStyleParentView themeStyle and its child views are dark

overrideThemeStyleParentView.theme.overrideThemeStyle = .dark
Copy the code

Other instructions

Why usethemeNamespace attributes instead of usingtheme_xxWhat about extended properties?

  • If you extend a system class by N functions, when you use that class, you will have N extended methods interfering with your choice of function indexing. This is especially true if you are doing other business development than you want to configure topic properties.
  • likeKingfisher,SnapKitAnd other well-known tripartite libraries, all use the namespace attribute implementation of the system class extension, which is a moreSwiftIs worth learning.

Subject Change notification

extension Notification.Name {
    public static let JXThemeDidChange = Notification.Name("com.jiaxin.theme.themeDidChangeNotification")}Copy the code

ThemeManagerStore topic configuration by user ID

/// Configure the flag key for the storage. It can be set to the user ID, so that the configuration of different users can be recorded on the same mobile phone. You need to set this property before any other values. public var storeConfigsIdentifierKey: String ="default"
Copy the code

Migrating to the System API guide

If your app supports iOS13 at least, you can follow these guidelines to migrate to the OS solution if necessary. Migrate to system API guide, click to read

Making the address

Finally, to review the Github address, click in for more details. JXTheme making address