Swiftpack.co - perseusrealdeal/DarkMode as Swift Package

Swiftpack.co is a collection of thousands of indexed Swift packages. Search packages.
See all packages published by perseusrealdeal.
perseusrealdeal/DarkMode 1.0.0
The elegant appearance mode business logic to release your Dark Mode features running from iOS 9
⭐️ 1
🕓 3 days ago
iOS
.package(url: "https://github.com/perseusrealdeal/DarkMode.git", from: "1.0.0")

Perseus Dark Mode && Adapted System UI

Actions Status Version Platforms iOS 9 SDK UIKit Swift 5.3 Swift Package Manager compatible License

Have a look of demo app, source code is here.

Standalone File Demo App

Table of contents

PART I - Perseus Dark Mode Library

Introductory remarks

  1. Build tools & Installation
  2. Solution key statements
  3. Dark Mode table decision
  4. Switching Dark Mode
  5. Catching Dark Mode triggered
  6. Sample Use Case of Dark Mode
  7. Sample Use Case of DarkModeImageView
  8. Sample Use Case of Adapted System UI

PART II - Adapted System UI Library

Introductory remarks

  1. Table 1. Adapted system colors
  2. Table 2. Adapted semantic colors

License


PART I - Perseus Dark Mode Library

Introductory remarks

Perseus Dark Mode is a swift package. Starting with iOS 13 Apple Inc. introduced Dark Mode on system level and now all apps are sensitive to this user option.

Represented solution was designed to support your apps running on such brilliant apple devices like, you know, my favorite one and my the first one is iPod Touch iOS 9.3.5 (5th, 13G36).

Using this solution allows you design the code of your app applying the Apple's Dark Mode for your early Apple devices with no need make any changes then.

This package consists of two libraries. Main is Perseus Dark Mode and satellite one is Adapted System UI.

1. Build tools & Installation

Tools used for designing the solution:

  • Xcode 13.3.1
  • Device iPod Touch iOS 9.3.5 (5th, 13G36)
  • Device iPad Air iOS 12.5.5 (iPad4,2 model, 16H62)
  • Simulator iPhone 12 mini (iOS 15.4, 19E240)

The solution can be used via swift package manager and as a standalone single file as well.

File PerseusDarkModeSingle.swift located in the package root and is dedicated for the standalone usage—can be copied and pasted into a host project tree under the same license.

2. Solution key statements

Dark Mode is a Singleton object

public class AppearanceService
{
    public static var shared: DarkMode = { DarkMode() } ()
    private init() { }
}

Dark Mode is a complex object

public protocol DarkModeProtocol
{
    var Style                  : AppearanceStyle { get }
    var SystemStyle            : SystemStyle { get }
    
    dynamic var StyleObservable: Int { get }
}

extension DarkMode: DarkModeProtocol { }

Dark Mode is hosted as a property in a screen object

public extension UIResponder { var DarkMode: DarkModeProtocol { AppearanceService.shared }}

3. Dark Mode table decision

Dark Mode option values

public enum DarkModeOption: Int
{
    case auto = 0
    case on   = 1
    case off  = 2
}

Dark Mode System values

public enum SystemStyle: Int
{
    case unspecified = 0
    case light       = 1
    case dark        = 2
}

Dark Mode decision table

Dark Mode decision table makes decision on Appearance style that can be either light or dark:

public enum AppearanceStyle: Int
{
    case light = 0
    case dark  = 1
}

Dark Mode default value is light:

public let DARK_MODE_STYLE_DEFAULT = AppearanceStyle.light
auto on off
.unspecified default dark light
.light light dark light
.dark dark dark light

In case if dark mode option is in auto and system style is .unspecified, default value is applied for iOS 12 and eariler, but, for iOS 13 and higher, device system appearance mode is.

Apps that are based on Perseus Dark Mode rely on AppearanceStyle as a business matter value available for accessing via UIResponder extension:

import UIKit

import PerseusDarkMode

class MyView: UIView 
{ 
    func functionName() 
    { 
        print("\(self.DarkMode.Style)")
    } 
}

4. Switching Dark Mode

Case: Manually

Inside your app there is only one way to let it know that you'd like to change Dark Mode. Use DarkModeUserChoice hosted as a property in AppearanceService to assign the value of Dark Mode you want.

So, in your code it should look like this:

let choice = DarkModeOption.auto

AppearanceService.DarkModeUserChoice = choice

AppearanceService.makeUp()

And do not forget call makeUp() if you want to be notified via NotificationCenter.

Case: Via System

To match the system appearance mode call AppearanceService.processTraitCollectionDidChange(_:) method once in traitCollectionDidChange(_:) of the main screen (UIViewController or UIWindow). The sample is below:

import UIKit

import PerseusDarkMode

class MainViewController: UIViewController
{
    override func viewDidLoad()
    {
        super.viewDidLoad()
        configure()
        
        AppearanceService.register(stakeholder: self, selector: #selector(makeUp))
    }
    
    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?)
    {
        super.traitCollectionDidChange(previousTraitCollection)
        
        if #available(iOS 13.0, *)
        {
            AppearanceService.processTraitCollectionDidChange(previousTraitCollection)
        }
    }
    
    @objc private func makeUp()
    {
        // Point to define a reaction to Dark Mode event is here
    }

    private func configure() { }
}

5. Catching Dark Mode triggered

Case: Using KVO

Create an observer somewhere in your code like this:

 var observer: DarkModeObserver?
 
 observer = DarkModeObserver()

Then, give it a closure to run your code each time when Dark Mode is changing:


observer?.action = 
    { newStyle in 
        
        // Point to define a reaction to Dark Mode event is here

    }

or like this:

var observer = DarkModeObserver() 
    { newStyle in
    
        // Point to define a reaction to Dark Mode event is here
        
    }

Case: Getting informed by NotificationCenter

To get notified by NotificationCenter your object should be registered with AppearanceService:

import UIKit

class MyView: UIView 
{ 
    @objc func makeUp() 
    { 
        // Point to define a reaction to Dark Mode event is here
    } 
}
let view = MyView()

AppearanceService.register(stakeholder: view, selector: #selector(view.makeUp))

Call AppearanceService.makeUp()

Use AppearanceService.makeUp() to call all selected makeUp methods:

AppearanceService.makeUp()

It should be called first time in didFinishLaunchingWithOptions:

import UIKit

import PerseusDarkMode

class AppDelegate: UIResponder { var window: UIWindow? }

extension AppDelegate: UIApplicationDelegate
{
    func application(_ application: UIApplication, didFinishLaunchingWithOptions
                     launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
    {
        // Init the app's window
        window = UIWindow(frame: UIScreen.main.bounds)
        
        // Give it a root view for the first screen
        window!.rootViewController = MainViewController.storyboardInstance()
        window!.makeKeyAndVisible()
        
        // And, finally, apply a new style for all screens
        AppearanceService.makeUp()
        
        return true
    }
}

6. Sample Use Case of Dark Mode

import UIKit

import PerseusDarkMode
import AdaptedSystemUI

class MainViewController: UIViewController
{
    let darkModeObserver = DarkModeObserver()
    
    override func viewDidLoad()
    {
        super.viewDidLoad()
        configure()
        
        AppearanceService.register(stakeholder: self, selector: #selector(makeUp))
        
        darkModeObserver.action =
            { newStyle in

                // Point to define a reaction to Dark Mode event is here
                print("\(newStyle), \(self.DarkMode.Style)")
            }
    }
    
    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?)
    {
        super.traitCollectionDidChange(previousTraitCollection)
        
        if #available(iOS 13.0, *)
        {
            AppearanceService.processTraitCollectionDidChange(previousTraitCollection)
        }
    }
    
    @objc private func makeUp()
    {
        // Point to define a reaction to Dark Mode event is here
        view.backgroundColor = .systemRed_Adapted
    }

    private func configure() { }
}

In addition to sample use case

If your view or view controller is declared as a lazy one or a sub view like UITableViewCell it's not bad to add the following condition after registering:

if AppearanceService.isEnabled { makeUp() }

For instance, here is a definition of some exemplar of UITableViewCell:

import UIKit

import PerseusDarkMode
import AdaptedSystemUI

class MemberTableViewCell: UITableViewCell
{
    override func awakeFromNib()
    {
        super.awakeFromNib()
        configure()
        
        AppearanceService.register(stakeholder: self, selector: #selector(makeUp))
        if AppearanceService.isEnabled { makeUp() }
    }

    private func configure() { }

    @objc private func makeUp()
    {
        // Point to define a reaction to Dark Mode event is here
        backgroundColor = .systemGray_Adapted
    }
}

7. Sample Use Case of DarkModeImageView

DarkModeImageView shows a quite light implementation of a dynamic image idea that is sensetive to Dark Mode.

import UIKit

import PerseusDarkMode

let frame = CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: 1, height: 1))
var imageView = DarkModeImageView(frame: frame)

image.configure(UIImage(named: "ImageNameForLight"), UIImage(named: "ImageNameForDark"))

Also, images for both light and dark styles can be setted up via Interface Builder using Attributes Inspector:

public class DarkModeImageView: UIImageView
{
    @IBInspectable
    var imageLight: UIImage?
    {
        didSet
        {
            light = imageLight
            image = AppearanceService.shared.Style == .light ? light : dark
        }
    }
    
    @IBInspectable
    var imageDark : UIImage?
    {
        didSet
        {
            dark = imageDark
            image = AppearanceService.shared.Style == .light ? light : dark
        }
    }
    
    private(set) var darkModeObserver: DarkModeObserver?
    
    private(set) var light: UIImage?
    private(set) var dark: UIImage?
    
    // ...
}

8. Sample Use Case of Adapted System UI

import UIKit

import AdaptedSystemUI

let view = UIView()
view.backgroundColor = .systemBlue_Adapted

In case if a certain color of a Dark Mode sensitive color required use the line of code below:

let _ = UIColor.label_Adapted.resolvedColor(with: self.traitCollection).cgColor

PART II - Adapted System UI Library

Introductory remarks

Colors listed in this section, table 1 and 2, represent colors specified by the official specification. But, not all system colors have been started available from iOS 13.0, colors .systemMint and .systemCyan available only from iOS 15.0. RGBA details of semantic colors have been exctracted from iOS 15.4, see table 2.

  • Adapted System UI library uses SDK color for sure starting from iOS 13 and the specification for early iOS releases.

  • System colors .systemMint, .systemCyan, and .systemBrown are not available in Xcode 12.5, only starting from 13.

There is an interesting case with .systemTeal color. The difference between the official specification and what's on the screen takes place in iOS 13, have a look of the extraction below:

.systemTeal RGBA Light RGBA Dark
Specification 48, 176, 199 64, 200, 224
iOS 13.7 90, 200, 250 100, 210, 255
iOS 15.4 48, 176, 199 64, 200, 224

One way to bridge the gap appearing with .systemTeal is using the customised color. Technique of customising colors is widely used in demo app.

In fact the actual color values provided below in table 1 and 2 may fluctuate from System release to release but for early System releases the colors adapted follow the specification as is.

Table 1. Adapted system colors

RGBA Light RGBA Dark UIKit API Adapted
255, 59, 48
#FF3B30FF
255, 69, 58
#FF453AFF
.systemRed_Adapted
255, 149, 0
#FF9500FF
255, 159, 10
#FF9F0AFF
.systemOrange_Adapted
255, 204, 0
#FFCC00FF
255, 214, 10
#FFD60AFF
.systemYellow_Adapted
52, 199, 89
#34C759FF
48, 209, 88
#30D158FF
.systemGreen_Adapted
0, 199, 190
#00C7BEFF
102, 212, 207
#66D4CFFF
.systemMint_Adapted
48, 176, 199
#30B0C7FF
64, 200, 224
#40C8E0FF
.systemTeal_Adapted
50, 173, 230
#32ADE6FF
100, 210, 255
#64D2FFFF
.systemCyan_Adapted
0, 122, 255
#007AFFFF
10, 132, 255
#0A84FFFF
.systemBlue_Adapted
88, 86, 214
#5856D6FF
94, 92, 230
#5E5CE6FF
.systemIndigo_Adapted
175, 82, 222
#AF52DEFF
191, 90, 242
#BF5AF2FF
.systemPurple_Adapted
255, 45, 85
#FF2D55FF
255, 55, 95
#FF375FFF
.systemPink_Adapted
162, 132, 94
#A2845EFF
172, 142, 104
#AC8E68FF
.systemBrown_Adapted
142, 142, 147
#8E8E93FF
142, 142, 147
#8E8E93FF
.systemGray_Adapted
174, 174, 178
#AEAEB2FF
99, 99, 102
#636366FF
.systemGray2_Adapted
199, 199, 204
#C7C7CCFF
72, 72, 74
#48484AFF
.systemGray3_Adapted
209, 209, 214
#D1D1D6FF
58, 58, 60
#3A3A3CFF
.systemGray4_Adapted
229, 229, 234
#E5E5EAFF
44, 44, 46
#2C2C2EFF
.systemGray5_Adapted
242, 242, 247
#F2F2F7FF
28, 28, 30
#1C1C1EFF
.systemGray6_Adapted

Table 2. Adapted semantic colors

RGBA Light RGBA Dark UIKit API Adapted
Foreground
Label
0, 0, 0, 1
#000000FF
255, 255, 255, 1
#FFFFFFFF
.label_Adapted
60, 60, 67, 0.6
#3C3C4399
235, 235, 245, 0.6
#EBEBF599
.secondaryLabel_Adapted
60, 60, 67, 0.3
#3C3C434D
235, 235, 245, 0.3
#EBEBF54D
.tertiaryLabel_Adapted
60, 60, 67, 0.18
#3C3C432E
235, 235, 245, 0.16
#EBEBF529
.quaternaryLabel_Adapted
Text
60, 60, 67, 0.3
#3C3C434D
235, 235, 245, 0.3
#EBEBF54D
.placeholderText_Adapted
Separator
60, 60, 67, 0.29
#3C3C434A
84, 84, 88, 0.6
#54545899
.separator_Adapted
198, 198, 200, 1
#C6C6C8FF
56, 56, 58, 1
#38383AFF
.opaqueSeparator_Adapted
Link
0, 122, 255, 1
#007AFFFF
9, 132, 255, 1
#0984FFFF
.link_Adapted
Fill
120, 120, 128, 0.2
#78788033
120, 120, 128, 0.36
#7878805C
.systemFill_Adapted
120, 120, 128, 0.16
#78788029
120, 120, 128, 0.32
#78788052
.secondarySystemFill_Adapted
118, 118, 128, 0.12
#7676801F
118, 118, 128, 0.24
#7676803D
.tertiarySystemFill_Adapted
116, 116, 128, 0.08
#74748014
118, 118, 128, 0.18
#7676802E
.quaternarySystemFill_Adapted
Background
Standard
255, 255, 255, 1
#FFFFFFFF
28, 28, 30, 1
#1C1C1EFF
.systemBackground_Adapted
242, 242, 247, 1
#F2F2F7FF
44, 44, 46, 1
#2C2C2EFF
.secondarySystemBackground_Adapted
255, 255, 255, 1
#FFFFFFFF
58, 58, 60, 1
#3A3A3CFF
.tertiarySystemBackground_Adapted
Grouped
242, 242, 247, 1
#F2F2F7FF
28, 28, 30, 1
#1C1C1EFF
.systemGroupedBackground_Adapted
255, 255, 255, 1
#FFFFFFFF
44, 44, 46, 1
#2C2C2EFF
.secondarySystemGroupedBackground_Adapted
242, 242, 247, 1
#F2F2F7FF
58, 58, 60, 1
#3A3A3CFF
.tertiarySystemGroupedBackground_Adapted

License

Copyright © 7530 Mikhail Zhigulin of Novosibirsk.

Where 7530 is the year from the creation of the world according to a Slavic calendar.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

GitHub

link
Stars: 1
Last commit: 3 days ago
jonrohan Something's broken? Yell at me @ptrpavlik. Praise and feedback (and money) is also welcome.

Release Notes

Dark Mode Release 1
Yesterday

Gives a control of Apple's device appearance mode. Used with UIKit and for iOS branch of Apple system tree.

It brings the property to a screen object named DarkMode via UIResponder extension.

extension UIResponder { var DarkMode: DarkModeProtocol { AppearanceService.shared }}

Using this property, current appearance mode can be easy accessed that is either light or dark.

import UIKit

import PerseusDarkMode

class MyView: UIView 
{ 
    func functionName() 
    { 
        print("\(self.DarkMode.Style)")
    } 
}

There is the AppearanceService singleton object dedicated for managing Dark Mode:

  • Matching system's appearance mode:
AppearanceService.processTraitCollectionDidChange(previousTraitCollection)
  • Registering objects that should be sensitive to Dark Mode:
AppearanceService.register(stakeholder: self, selector: #selector(makeUp))
  • Informing registered objects to change their appearance:
AppearanceService.makeUp()
  • Changing Dark Mode that can be off, on or auto:
AppearanceService.DarkModeUserChoice = .on

As a satellite to Dark Mode library there is Adapted System UI library.

Adapted System UI library brings system and semantic colors introduced in iOS 13 to early iOS releases.

import UIKit

import AdaptedSystemUI

let view = UIView()
view.backgroundColor = .systemBlue_Adapted

Happy coding!

Swiftpack is being maintained by Petr Pavlik | @ptrpavlik | @swiftpackco | API | Analytics