diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 5dd8fd0..52abb44 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -1,46 +1,40 @@
-name: 'Tests'
+# .github/workflows/main.yml
+name: 'Build & Tests'
-on:
+on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
# schedule:
# - cron: '5 5 * * 5'
-
+
jobs:
- unit-testing-iOS:
- runs-on: macos-latest
+
+ macOS-build:
+ runs-on: macos-14
steps:
- name: Checkout the code
- uses: actions/checkout@v2
- - name: Show xcodebuild version
- run: xcodebuild -version
- - name: Show xcode embedded SDKs
- run: xcodebuild -showsdks
- - name: Show buildable schemes
- run: xcodebuild -list
- - uses: mxcl/xcodebuild@v1.9.2
+ uses: actions/checkout@v4
+ - name: Select Xcode 15.4
+ run: sudo xcode-select -s "/Applications/Xcode_15.4.app"
+ - uses: mxcl/xcodebuild@v3
with:
- platform: iOS
+ platform: macOS
scheme: 'PerseusDarkMode'
- action: test
+ action: build
code-coverage: true
verbosity: xcpretty
upload-logs: always
-
- unit-testing-macOS:
- runs-on: macos-latest
+
+ macOS-test:
+ runs-on: macos-14
steps:
- name: Checkout the code
- uses: actions/checkout@v2
- - name: Show xcodebuild version
- run: xcodebuild -version
- - name: Show xcode embedded SDKs
- run: xcodebuild -showsdks
- - name: Show buildable schemes
- run: xcodebuild -list
- - uses: mxcl/xcodebuild@v1.9.2
+ uses: actions/checkout@v4
+ - name: Select Xcode 15.4
+ run: sudo xcode-select -s "/Applications/Xcode_15.4.app"
+ - uses: mxcl/xcodebuild@v3
with:
platform: macOS
scheme: 'PerseusDarkMode'
@@ -48,4 +42,35 @@ jobs:
code-coverage: true
verbosity: xcpretty
upload-logs: always
-
+
+ iOS-build:
+ runs-on: macos-14
+ steps:
+ - name: Checkout the code
+ uses: actions/checkout@v4
+ - name: Select Xcode 15.4
+ run: sudo xcode-select -s "/Applications/Xcode_15.4.app"
+ - uses: mxcl/xcodebuild@v3
+ with:
+ platform: iOS
+ scheme: 'PerseusDarkMode'
+ action: build
+ code-coverage: true
+ verbosity: xcpretty
+ upload-logs: always
+
+ iOS-test:
+ runs-on: macos-14
+ steps:
+ - name: Checkout the code
+ uses: actions/checkout@v4
+ - name: Select Xcode 15.4
+ run: sudo xcode-select -s "/Applications/Xcode_15.4.app"
+ - uses: mxcl/xcodebuild@v3
+ with:
+ platform: iOS
+ scheme: 'PerseusDarkMode'
+ action: test
+ code-coverage: true
+ verbosity: xcpretty
+ upload-logs: always
diff --git a/.github/workflows/swiftlint.yml b/.github/workflows/swiftlint.yml
new file mode 100644
index 0000000..85575f3
--- /dev/null
+++ b/.github/workflows/swiftlint.yml
@@ -0,0 +1,17 @@
+# .github/workflows/swiftlint.yml
+name: Style
+
+on:
+ push:
+ branches: [ master ]
+ pull_request:
+ branches: [ master ]
+
+jobs:
+ SwiftLint:
+ runs-on: macos-14
+ steps:
+ - uses: actions/checkout@v4
+ - uses: cirruslabs/swiftlint-action@v1
+ with:
+ version: latest
diff --git a/.gitignore b/.gitignore
index f6dcfcc..c8a1b78 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,14 +5,11 @@
## User settings
xcuserdata/
-## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
-*.xcscmblueprint
-*.xccheckout
-
-## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
+## Build generated
build/
DerivedData/
-*.moved-aside
+
+## Various settings
*.pbxuser
!default.pbxuser
*.mode1v3
@@ -21,10 +18,14 @@ DerivedData/
!default.mode2v3
*.perspectivev3
!default.perspectivev3
+xcuserdata/
## Other
-.DS_Store
-swiftlint.txt
+*.moved-aside
+*.xccheckout
+*.xcscmblueprint
+*.DS_Store
+SwiftLintOutput
## Obj-C/Swift specific
*.hmap
@@ -34,61 +35,16 @@ swiftlint.txt
*.dSYM.zip
*.dSYM
-## Playgrounds
-timeline.xctimeline
-playground.xcworkspace
-
# Swift Package Manager
#
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
# Packages/
# Package.pins
Package.resolved
-.xcodeproj
+*.xcodeproj
#
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
# hence it is not needed unless you have added a package configuration file to your project
.swiftpm
.build/
-
-# CocoaPods
-#
-# We recommend against adding the Pods directory to your .gitignore. However
-# you should judge for yourself, the pros and cons are mentioned at:
-# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
-#
-# Pods/
-#
-# Add this line if you want to avoid checking in source code from the Xcode workspace
-# *.xcworkspace
-
-# Carthage
-#
-# Add this line if you want to avoid checking in source code from Carthage dependencies.
-# Carthage/Checkouts
-
-Carthage/Build/
-
-# Accio dependency management
-Dependencies/
-.accio/
-
-# fastlane
-#
-# It is recommended to not store the screenshots in the git repo.
-# Instead, use fastlane to re-generate the screenshots whenever they are needed.
-# For more information about the recommended setup visit:
-# https://docs.fastlane.tools/best-practices/source-control/#source-control
-
-fastlane/report.xml
-fastlane/Preview.html
-fastlane/screenshots/**/*.png
-fastlane/test_output
-
-# Code Injection
-#
-# After new code Injection tools there's a generated folder /iOSInjectionProject
-# https://github.com/johnno1962/injectionforxcode
-
-iOSInjectionProject/
diff --git a/.swiftlint.yml b/.swiftlint.yml
index e486b73..7694456 100644
--- a/.swiftlint.yml
+++ b/.swiftlint.yml
@@ -50,4 +50,4 @@ function_parameter_count:
large_tuple:
warning: 4
- error: 5
+ error: 5
\ No newline at end of file
diff --git a/APPROBATION.md b/APPROBATION.md
new file mode 100644
index 0000000..f9d608c
--- /dev/null
+++ b/APPROBATION.md
@@ -0,0 +1,53 @@
+# Approbation Matrix / PerseusDarkMode 2.0.0
+
+> NOTE: To catch all log messages Mac Console should be started first then after a little while the logged app.
+
+> Compilation: macOS Monterey 12.7.6 / Xcode 14.2
+
+## macOS approbation result
+
+> The approbation macOS app for PerseusDarkMode 2.0.0 is [Arkenstone](https://github.com/perseusrealdeal/Arkenstone).
+
+| macOS | Version | Result | Details |
+| ----------- | ------- | :-----: | ------- |
+| High Sierra | 10.13 | ok | - |
+| Mojave | 10.14 | ok | - |
+| Catalina | 10.15 | ok | - |
+| Big Sur | 11.7 | ok | - |
+| Monterey | 12.7 | ok | - |
+| Ventura | 13.7 | ok | - |
+| Sonoma | 14.7 | ok | - |
+| Sequoia | 15.3 | ok | - |
+
+## iOS approbation result
+
+> The approbation iOS app for PerseusDarkMode 2.0.0 is [The One Ring](https://github.com/perseusrealdeal/TheOneRing).
+
+| Device | Simulator | OS Version | Result | Details |
+| --------------- | :-------: | ---------- | :-----: | ------- |
+| iPad Air | no | 12.5.7 | ok | - |
+| iPhone SE (3rd) | yes | 16.2 | ok | - |
+
+## A3 environment
+
+### List of available Apple machines
+
+> Excluded: virtualization (e.g. VirtualBox) and hackintosh
+
+| Machine | Memory | Storage |
+| ----------- | ------ | ---------------------- |
+| Mac mini | 16GB | SATA 480GB, NVMe 256GB |
+| MacBook Pro | 8GB | 256GB |
+
+### System configuration for A3 environment
+
+| macOS | Version | Machine | Xcode | OpenCore | Git Client |
+| ----------- | ------- | ----------- | ------ | -------- | -------------- |
+| High Sierra | 10.13.6 | Mac mini | 10.1 | - | GitHub Desktop |
+| Mojave | 10.14.6 | Mac mini | 11.3.1 | - | GitHub Desktop |
+| Catalina | 10.15.7 | Mac mini | 11.7 | - | GitHub Desktop |
+| Big Sur | 11.7.10 | Mac mini | 13.2.1 | - | GitHub Desktop |
+| Monterey | 12.7.6 | Mac mini | 14.2 | - | SmartGit |
+| Ventura | 13.7.4 | MacBook Pro | 15.2 | - | GitHub Desktop |
+| Sonoma | 14.7.4 | MacBook Pro | 16.2 | yes | GitHub Desktop |
+| Sequoia | 15.3.1 | MacBook Pro | 16.2 | yes | GitHub Desktop |
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..9ed09b4
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,55 @@
+# Changelog
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+Dates in this file meets Gregorian calendar. Date in format YYYY-MM-DD.
+
+## [2.0.0] - [2025-04-27], PerseusDarkMode
+
+- Minimum build requirements: macOS 10.13+, iOS 11.0+, Xcode 14.2+. If standalone Xcode 10.1+.
+
+### Changed
+
+- Package structure.
+- Calculation macOS Dark Mode.
+
+### Added
+
+- Approbation and Changelog docs.
+
+### Included
+
+- Functions to switch Dark Mode.
+- PerseusUISystemKit classes, goes as PDMSupportingStar.swift only, out of import.
+
+### Improved
+
+- PerseusDarkMode API.
+- Source Code.
+- Documentation.
+
+### Fixed
+
+- PerseusDarkMode auto detect macOS DarkMode changes.
+
+### Updated
+
+- PerseusLogger to [CPL v1.1.0](https://github.com/perseusrealdeal/ConsolePerseusLogger).
+
+### Removed
+
+- Unit tests, import test only.
+
+## [1.1.5] - [2023-01-14], PerseusDarkMode
+
+- Minimum build requirements: macOS 10.9+, iOS 9.3+, and Xcode 10.1+.
+
+### Added
+
+- [PerseusLogger](https://gist.github.com/perseusrealdeal/df456a9825fcface44eca738056eb6d5).
+- DarkMode feature as an extenstion variable of UIResponder/NSResponer classes iOS and macOS.
+- DarkMode feature for iOS Settings bundle.
+- Observing DarkMode with KVO.
+
+## [2022-04-01], First commit
diff --git a/LICENSE b/LICENSE
index 417ef55..ae281d0 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,10 +1,13 @@
MIT License
-Copyright © 7530 - 7531 Mikhail Zhigulin of Novosibirsk
+Copyright © 7530 - 7533 Mikhail A. Zhigulin of Novosibirsk
+Copyright © 7533 PerseusRealDeal
The year starts from the creation of the world according to a Slavic calendar.
September, the 1st of Slavic year.
+Use Stars to adopt for the specifics you need.
+
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
diff --git a/PDMStar.swift b/PDMStar.swift
new file mode 100644
index 0000000..9791618
--- /dev/null
+++ b/PDMStar.swift
@@ -0,0 +1,458 @@
+//
+// PDMStar.swift
+// Version: 2.0.0
+//
+// Standalone PerseusDarkMode.
+//
+//
+// For iOS and macOS only. Use Stars to adopt for the specifics you need.
+//
+// DESC: THE DARKNESS YOU CAN FORCE.
+//
+// Created by Mikhail Zhigulin in 7530.
+//
+// Copyright © 7530 - 7533 Mikhail Zhigulin of Novosibirsk
+// Copyright © 7533 PerseusRealDeal
+//
+// All rights reserved.
+//
+//
+// MIT License
+//
+// Copyright © 7530 - 7533 Mikhail A. Zhigulin of Novosibirsk
+// Copyright © 7533 PerseusRealDeal
+//
+// The year starts from the creation of the world according to a Slavic calendar.
+// September, the 1st of Slavic year.
+//
+// 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.
+//
+// swiftlint:disable file_length
+//
+
+#if canImport(UIKit)
+import UIKit
+#elseif canImport(Cocoa)
+import Cocoa
+#endif
+
+// import ConsolePerseusLogger
+
+public let APPEARANCE_DEFAULT = AppearanceStyle.light
+
+public let DARK_MODE_USER_CHOICE_KEY = "DarkModeUserChoiceOptionKey"
+public let DARK_MODE_USER_CHOICE_DEFAULT = DarkModeOption.auto
+
+#if os(iOS)
+public let DARK_MODE_SETTINGS_KEY = "DarkModeSettingsKey"
+#endif
+
+#if os(macOS)
+public var DARK_APPEARANCE_DEFAULT_IN_USE: NSAppearance {
+ guard let darkAppearanceOS = DARK_APPEARANCE_DEFAULT else {
+ if #available(macOS 10.14, *) {
+ return NSAppearance(named: .darkAqua)!
+ } // For HighSierra.
+ return NSAppearance(named: .vibrantDark)!
+ }
+ return darkAppearanceOS
+}
+public var DARK_APPEARANCE_DEFAULT: NSAppearance?
+public var LIGHT_APPEARANCE_DEFAULT_IN_USE = NSAppearance(named: .aqua)!
+#endif
+
+public extension Notification.Name {
+ static let MakeAppearanceUpNotification =
+ Notification.Name("MakeAppearanceUpNotification")
+#if os(macOS)
+ static let AppleInterfaceThemeChangedNotification =
+ Notification.Name("AppleInterfaceThemeChangedNotification")
+#endif
+}
+
+// swiftlint:disable identifier_name
+public extension NSObject {
+ var DarkMode: DarkMode { return DarkModeAgent.shared }
+ var DarkModeUserChoice: DarkModeOption { return DarkModeAgent.DarkModeUserChoice }
+}
+// swiftlint:enable identifier_name
+
+public enum AppearanceStyle: Int, CustomStringConvertible {
+
+ case light = 0
+ case dark = 1
+
+ public var description: String {
+ switch self {
+ case .light:
+ return ".light"
+ case .dark:
+ return ".dark"
+ }
+ }
+}
+
+public enum DarkModeOption: Int, CustomStringConvertible {
+
+ case auto = 0
+ case on = 1
+ case off = 2
+
+ public var description: String {
+ switch self {
+ case .auto:
+ return ".auto"
+ case .on:
+ return ".on"
+ case .off:
+ return ".off"
+ }
+ }
+}
+
+public class DarkMode: NSObject {
+
+ public var style: AppearanceStyle { return appearance }
+
+ @objc public dynamic var styleObservable: Int = APPEARANCE_DEFAULT.rawValue
+
+ internal var appearance: AppearanceStyle = APPEARANCE_DEFAULT {
+ didSet { styleObservable = style.rawValue }
+ }
+}
+
+public class DarkModeAgent {
+
+ // MARK: - Properties
+
+ public static var shared: DarkMode = { _ = instance; return DarkMode() }()
+ public static var DarkModeUserChoice: DarkModeOption {
+ return userChoice
+ }
+
+ // MARK: - Internals
+
+ private static var userChoice: DarkModeOption {
+ get {
+ let rawValue = ud.valueExists(forKey: DARK_MODE_USER_CHOICE_KEY) ?
+ ud.integer(forKey: DARK_MODE_USER_CHOICE_KEY) :
+ DARK_MODE_USER_CHOICE_DEFAULT.rawValue
+
+ if let result = DarkModeOption.init(rawValue: rawValue) { return result }
+ return DARK_MODE_USER_CHOICE_DEFAULT
+ }
+ set {
+ ud.setValue(newValue.rawValue, forKey: DARK_MODE_USER_CHOICE_KEY)
+#if os(iOS)
+ // Set DarkMode option for Settings bundle
+ ud.setValue(newValue.rawValue, forKey: DARK_MODE_SETTINGS_KEY)
+#endif
+ }
+ }
+
+#if os(macOS)
+ private static var distributedNCenter = DistributedNotificationCenter.default
+#endif
+ private static var nCenter = NotificationCenter.default
+ private static var ud = UserDefaults.standard
+
+ private var observation: NSKeyValueObservation?
+
+ // MARK: - Singletone
+
+ private static var instance = { DarkModeAgent() }()
+ private init() {
+ // log.message("[\(type(of: self))].\(#function)", .info)
+#if os(macOS)
+ if #available(macOS 10.14, *) {
+ DarkModeAgent.distributedNCenter.addObserver(
+ self,
+ selector: #selector(processAppleInterfaceThemeChanged),
+ name: .AppleInterfaceThemeChangedNotification,
+ object: nil
+ )
+ observation = NSApp.observe(\.effectiveAppearance) { _, _ in
+ if DarkModeAgent.userChoice == .auto {
+
+ let effectiveAppearance = NSApplication.shared.effectiveAppearance
+ let wanted = self.getRequired(from: effectiveAppearance)
+
+ DarkModeAgent.shared.appearance = wanted
+ self.notifyAllRegistered()
+ }
+ }
+ } // For HighSierra there is no need in observation cos' system style never change.
+#endif
+ }
+
+ // MARK: - Contract
+
+ public static func register(stakeholder: Any, selector: Selector) {
+ self.nCenter.addObserver(stakeholder,
+ selector: selector,
+ name: .MakeAppearanceUpNotification,
+ object: nil)
+ }
+
+ public static func force(_ userChoice: DarkModeOption) {
+
+ DarkModeAgent.userChoice = userChoice
+ DarkModeAgent.instance.refresh()
+
+#if os(iOS)
+ DarkModeAgent.instance.notifyAllRegistered()
+#elseif os(macOS)
+ if #unavailable(macOS 10.14) { // For HighSierra.
+ DarkModeAgent.instance.notifyAllRegistered()
+ } else if DarkModeAgent.userChoice != .auto { // If .auto observation notifies change.
+ DarkModeAgent.instance.notifyAllRegistered()
+ }
+#endif
+ }
+
+ public static func makeUp() {
+ DarkModeAgent.instance.notifyAllRegistered()
+ }
+
+ // MARK: - iOS Contract specifics
+
+#if os(iOS)
+ @available(iOS 13.0, *)
+ public static func processTraitCollectionDidChange(_ previous: UITraitCollection?) {
+ if let previous = previous?.userInterfaceStyle {
+ if UIWindow.systemStyle.rawValue != previous.rawValue {
+ DarkModeAgent.instance.processAppleInterfaceThemeChanged()
+ }
+ }
+ }
+
+ // Returns nil if DarkMode equal to User choice or no value saved otherwise new value.
+ public static func isDarkModeSettingsKeyChanged() -> DarkModeOption? {
+ let option = ud.valueExists(forKey: DARK_MODE_SETTINGS_KEY) ?
+ ud.integer(forKey: DARK_MODE_SETTINGS_KEY) : -1
+
+ guard option != -1, let settingsDarkMode = DarkModeOption.init(rawValue: option)
+ else { return nil }
+
+ return settingsDarkMode != userChoice ? settingsDarkMode : nil
+ }
+#endif
+
+ // MARK: - Implementation
+
+ @objc private func processAppleInterfaceThemeChanged() {
+ refresh()
+ notifyAllRegistered()
+ }
+
+ private func refresh() {
+ let choice = DarkModeAgent.userChoice
+#if os(iOS)
+ let current = UIWindow.systemStyle
+ let required = makeDecision(current, choice)
+
+ if #available(iOS 13.0, *) {
+ UIWindow.refreshKeyWindow()
+ }
+
+ DarkModeAgent.shared.appearance = required
+#elseif os(macOS)
+ if #available(macOS 10.14, *) {
+ switch choice {
+ case .auto:
+ NSApp.appearance = nil
+ // DarkModeAgent is informed about new appearance by observation.
+ case .on:
+ NSApp.appearance = DARK_APPEARANCE_DEFAULT_IN_USE
+ DarkModeAgent.shared.appearance = .dark
+ case .off:
+ NSApp.appearance = LIGHT_APPEARANCE_DEFAULT_IN_USE
+ DarkModeAgent.shared.appearance = .light
+ }
+ } else { // For HighSierra.
+ DarkModeAgent.shared.appearance = choice == .on ? .dark : .light
+ }
+#endif
+ }
+
+ private func notifyAllRegistered() {
+ DarkModeAgent.nCenter.post(name: .MakeAppearanceUpNotification, object: nil)
+ }
+
+#if os(macOS)
+ private func getRequired(from appearance: NSAppearance?) -> AppearanceStyle {
+ let choice = DarkModeAgent.userChoice
+
+ if #available(macOS 10.14, *) {
+ guard choice == .auto else {
+ return choice == .off ? .light : .dark
+ }
+
+ if let match = appearance?.bestMatch(from: [.darkAqua, .vibrantDark]) {
+ return [.darkAqua, .vibrantDark].contains(match) ? .dark : .light
+ }
+
+ return .light
+ } else { // For HighSierra.
+ return choice == .off ? .dark : .light
+ }
+ }
+#endif
+}
+
+// MARK: - Helpers
+
+extension UserDefaults {
+ public func valueExists(forKey key: String) -> Bool {
+ return object(forKey: key) != nil
+ }
+}
+
+public class DarkModeObserver: NSObject {
+
+ public var action: ((_ newStyle: AppearanceStyle) -> Void)?
+
+ private var objectToObserve = DarkModeAgent.shared
+ private var observation: NSKeyValueObservation?
+
+ public override init() {
+ super.init()
+ setupObservation()
+ }
+
+ public init(_ action: @escaping ((_ newStyle: AppearanceStyle) -> Void)) {
+ super.init()
+
+ self.action = action
+ setupObservation()
+ }
+
+ private func setupObservation() {
+ observation = objectToObserve.observe(\.styleObservable) { _, _ in
+ self.action?(self.objectToObserve.style)
+ }
+ }
+}
+
+#if os(iOS)
+public enum SystemStyle: Int, CustomStringConvertible {
+
+ case unspecified = 0
+ case light = 1
+ case dark = 2
+
+ public var description: String {
+ switch self {
+ case .unspecified:
+ return ".unspecified"
+ case .light:
+ return ".light"
+ case .dark:
+ return ".dark"
+ }
+ }
+}
+
+extension UIWindow {
+
+ static var key: UIWindow? {
+ if #available(iOS 13, *) {
+ return UIApplication.shared.windows.first { $0.isKeyWindow }
+ } else {
+ return UIApplication.shared.keyWindow
+ }
+ }
+
+ static var systemStyle: SystemStyle {
+ if #available(iOS 13.0, *) {
+ guard let keyWindow = UIWindow.key else { return .unspecified }
+
+ switch keyWindow.traitCollection.userInterfaceStyle {
+ case .unspecified:
+ return .unspecified
+ case .light:
+ return .light
+ case .dark:
+ return .dark
+
+ @unknown default:
+ return .unspecified
+ }
+ } else {
+ return .unspecified // For iOS 12.0 and earlier.
+ }
+ }
+}
+
+// MARK: - Calculating Dark Mode Required
+
+/// Calculates the current required appearance style of the app.
+///
+/// Dark Mode decision-making:
+///
+/// | User
+/// -------------+-----------------------
+/// System | auto | on | off
+/// -------------+---------+------+------
+/// .unspecified | default | dark | light
+/// .light | light | dark | light
+/// .dark | dark | dark | light
+///
+public func makeDecision(_ system: SystemStyle, _ user: DarkModeOption) -> AppearanceStyle {
+
+ if (system == .unspecified) && (user == .auto) { return APPEARANCE_DEFAULT }
+ if (system == .unspecified) && (user == .on) { return .dark }
+ if (system == .unspecified) && (user == .off) { return .light }
+
+ if (system == .light) && (user == .auto) { return .light }
+ if (system == .light) && (user == .on) { return .dark }
+ if (system == .light) && (user == .off) { return .light }
+
+ if (system == .dark) && (user == .auto) { return .dark }
+ if (system == .dark) && (user == .on) { return .dark }
+ if (system == .dark) && (user == .off) { return .light }
+
+ return APPEARANCE_DEFAULT
+
+}
+#endif
+
+#if os(iOS) && compiler(>=5)
+extension UIWindow {
+
+ @available(iOS 13.0, *)
+ public static func refreshKeyWindow() {
+
+ guard let keyWindow = UIWindow.key else { return }
+
+ var overrideStyle: UIUserInterfaceStyle = .unspecified
+
+ switch DarkModeAgent.DarkModeUserChoice {
+ case .auto:
+ overrideStyle = .unspecified
+ case .on:
+ overrideStyle = .dark
+ case .off:
+ overrideStyle = .light
+ }
+
+ keyWindow.overrideUserInterfaceStyle = overrideStyle
+ }
+}
+#endif
diff --git a/PDMSupportingStar.swift b/PDMSupportingStar.swift
new file mode 100644
index 0000000..624f7d0
--- /dev/null
+++ b/PDMSupportingStar.swift
@@ -0,0 +1,739 @@
+//
+// PDMSupportingStar.swift
+// Version: 2.0.0
+//
+// The Darkness Support (PerseusUISystemKit previously)
+//
+//
+// For iOS and macOS only. Use Stars to adopt for the specifics you need.
+//
+// Created by Mikhail Zhigulin of Novosibirsk in 7530.
+//
+// The year starts from the creation of the world according to a Slavic calendar.
+// September, the 1st of Slavic year.
+//
+//
+// Unlicensed Free Software
+//
+// This is free and unencumbered software released into the public domain.
+//
+// Anyone is free to copy, modify, publish, use, compile, sell, or
+// distribute this software, either in source code form or as a compiled
+// binary, for any purpose, commercial or non-commercial, and by any
+// means.
+//
+// In jurisdictions that recognize copyright laws, the author or authors
+// of this software dedicate any and all copyright interest in the
+// software to the public domain. We make this dedication for the benefit
+// of the public at large and to the detriment of our heirs and
+// successors. We intend this dedication to be an overt act of
+// relinquishment in perpetuity of all present and future rights to this
+// software under copyright law.
+//
+// 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 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.
+//
+// For more information, please refer to
+//
+// swiftlint:disable file_length
+//
+
+// import PerseusDarkMode
+
+#if canImport(UIKit)
+import UIKit
+#elseif canImport(Cocoa)
+import Cocoa
+#endif
+
+#if os(iOS)
+public typealias Color = UIColor
+#elseif os(macOS)
+public typealias Color = NSColor
+#endif
+
+public func rgba255(_ red: CGFloat,
+ _ green: CGFloat,
+ _ blue: CGFloat,
+ _ alpha: CGFloat = 1.0) -> Color {
+ return Color(red: red/255, green: green/255, blue: blue/255, alpha: alpha)
+}
+
+public extension Color {
+
+ var RGBA255: (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) {
+ var red: CGFloat = 0
+ var green: CGFloat = 0
+ var blue: CGFloat = 0
+ var alpha: CGFloat = 0
+
+ getRed(&red, green: &green, blue: &blue, alpha: &alpha)
+
+ return (red*255, green*255, blue*255, alpha)
+ }
+}
+
+public protocol SemanticColorProtocol {
+
+// MARK: - FOREGROUND CONTENT
+
+ // MARK: - Label Colors
+
+ /// Label.
+ ///
+ /// - Light: 0, 0, 0
+ /// - Dark: 255, 255, 255
+ static var labelPerseus: Color { get }
+
+ /// Secondary label.
+ ///
+ /// - Light: 60, 60, 67, 0.6
+ /// - Dark: 235, 235, 245, 0.6
+ static var secondaryLabelPerseus: Color { get }
+
+ /// Tertiary label.
+ ///
+ /// - Light: 60, 60, 67, 0.3
+ /// - Dark: 235, 235, 245, 0.3
+ static var tertiaryLabelPerseus: Color { get }
+
+ /// Quaternary label.
+ ///
+ /// - Light: 60, 60, 67, 0.18
+ /// - Dark: 235, 235, 245, 0.16
+ static var quaternaryLabelPerseus: Color { get }
+
+ // MARK: - Text Colors
+
+ /// Placeholder text.
+ ///
+ /// - Light: 60, 60, 67, 0.3
+ /// - Dark: 235, 235, 245, 0.3
+ static var placeholderTextPerseus: Color { get }
+
+ // MARK: - Separator Colors
+
+ /// Separator.
+ ///
+ /// - Light: 60, 60, 67, 0.29
+ /// - Dark: 84, 84, 88, 0.6
+ static var separatorPerseus: Color { get }
+
+ /// Opaque separator.
+ ///
+ /// - Light: 198, 198, 200
+ /// - Dark: 56, 56, 58
+ static var opaqueSeparatorPerseus: Color { get }
+
+ // MARK: - Link Color
+
+ /// Link.
+ ///
+ /// - Light: 0, 122, 255
+ /// - Dark: 9, 132, 255
+ static var linkPerseus: Color { get }
+
+ // MARK: - Fill Colors
+
+ /// System fill.
+ ///
+ /// - Light: 120, 120, 128, 0.2
+ /// - Dark: 120, 120, 128, 0.36
+ static var systemFillPerseus: Color { get }
+
+ /// Secondary system fill.
+ ///
+ /// - Light: 120, 120, 128, 0.16
+ /// - Dark: 120, 120, 128, 0.32
+ static var secondarySystemFillPerseus: Color { get }
+
+ /// Tertiary system fill.
+ ///
+ /// - Light: 118, 118, 128, 0.12
+ /// - Dark: 118, 118, 128, 0.24
+ static var tertiarySystemFillPerseus: Color { get }
+
+ /// Quaternary system fill.
+ ///
+ /// - Light: 116, 116, 128, 0.08
+ /// - Dark: 118, 118, 128, 0.18
+ static var quaternarySystemFillPerseus: Color { get }
+
+// MARK: - BACKGROUND CONTENT
+
+ // MARK: - Standard
+
+ /// System background.
+ ///
+ /// - Light: 255, 255, 255
+ /// - Dark: 28, 28, 30
+ static var systemBackgroundPerseus: Color { get }
+
+ /// Secondary system background.
+ ///
+ /// - Light: 242, 242, 247
+ /// - Dark: 44, 44, 46
+ static var secondarySystemBackgroundPerseus: Color { get }
+
+ /// Tertiary system background.
+ ///
+ /// - Light: 255, 255, 255
+ /// - Dark: 58, 58, 60
+ static var tertiarySystemBackgroundPerseus: Color { get }
+
+ // MARK: - Grouped
+
+ /// System grouped background.
+ ///
+ /// - Light: 242, 242, 247
+ /// - Dark: 28, 28, 30
+ static var systemGroupedBackgroundPerseus: Color { get }
+
+ /// Secondary system grouped background.
+ ///
+ /// - Light: 255, 255, 255
+ /// - Dark: 44, 44, 46
+ static var secondarySystemGroupedBackgroundPerseus: Color { get }
+
+ /// Tertiary system grouped background.
+ ///
+ /// - Light: 242, 242, 247
+ /// - Dark: 58, 58, 60
+ static var tertiarySystemGroupedBackgroundPerseus: Color { get }
+}
+
+public protocol SystemColorProtocol {
+
+ /// Red is .systemRed
+ ///
+ /// - Light: 255, 59, 48
+ /// - Dark: 255, 69, 58
+ static var perseusRed: Color { get }
+
+ /// Orange is .systemOrange
+ ///
+ /// - Light: 255, 149, 0
+ /// - Dark: 255, 159, 10
+ static var perseusOrange: Color { get }
+
+ /// Yellow is .systemYellow
+ ///
+ /// - Light: 255, 204, 0
+ /// - Dark: 255, 214, 10
+ static var perseusYellow: Color { get }
+
+ /// Green is .systemGreenGreen
+ ///
+ /// - Light: 52, 199, 89
+ /// - Dark: 48, 209, 88
+ static var perseusGreen: Color { get }
+
+ /// Mint is .systemMint
+ ///
+ /// - Light: 0, 199, 190
+ /// - Dark: 102, 212, 207
+ static var perseusMint: Color { get }
+
+ /// Teal is .systemTeal
+ ///
+ /// - Light: 48, 176, 199
+ /// - Dark: 64, 200, 224
+ static var perseusTeal: Color { get }
+
+ /// Cyan is .systemCyan
+ ///
+ /// - Light: 50, 173, 230
+ /// - Dark: 100, 210, 255
+ static var perseusCyan: Color { get }
+
+ /// Blue is .systemBlue
+ ///
+ /// - Light: 0, 122, 255
+ /// - Dark: 10, 132, 255
+ static var perseusBlue: Color { get }
+
+ /// Indigo is .systemIndigo
+ ///
+ /// - Light: 88, 86, 214
+ /// - Dark: 94, 92, 230
+ static var perseusIndigo: Color { get }
+
+ /// Purple is .systemPurple
+ ///
+ /// - Light: 175, 82, 222
+ /// - Dark: 191, 90, 242
+ static var perseusPurple: Color { get }
+
+ /// Pink is .systemPink
+ ///
+ /// - Light: 255, 45, 85
+ /// - Dark: 255, 55, 95
+ static var perseusPink: Color { get }
+
+ /// Brown is .systemBrown
+ ///
+ /// - Light: 162, 132, 94
+ /// - Dark: 172, 142, 104
+ static var perseusBrown: Color { get }
+
+// MARK: - System Gray Colors
+
+ /// Gray is .systemGray
+ ///
+ /// - Light: 142, 142, 147
+ /// - Dark: 142, 142, 147
+ static var perseusGray: Color { get }
+
+ /// Gray (2) is .systemGray62
+ ///
+ /// - Light: 174, 174, 178
+ /// - Dark: 99, 99, 102
+ static var perseusGray2: Color { get }
+
+ /// Gray (3) is .systemGray3
+ ///
+ /// - Light: 199, 199, 204
+ /// - Dark: 72, 72, 74
+ static var perseusGray3: Color { get }
+
+ /// Gray (4) is .systemGray4
+ ///
+ /// - Light: 209, 209, 214
+ /// - Dark: 58, 58, 60
+ static var perseusGray4: Color { get }
+
+ /// Gray (5) is .systemGray5
+ ///
+ /// - Light: 229, 229, 234
+ /// - Dark: 44, 44, 46
+ static var perseusGray5: Color { get }
+
+ /// Gray (6) is .systemGray6
+ ///
+ /// - Light: 242, 242, 247
+ /// - Dark: 28, 28, 30
+ static var perseusGray6: Color { get }
+}
+
+extension Color: SemanticColorProtocol {
+
+ /// .label
+ public static var labelPerseus: Color {
+ return DarkModeAgent.shared.style == .light ?
+ rgba255(0, 0, 0) : rgba255(255, 255, 255)
+ }
+
+ /// .secondaryLabel
+ public static var secondaryLabelPerseus: Color {
+ return DarkModeAgent.shared.style == .light ?
+ rgba255(60, 60, 67, 0.6) : rgba255(235, 235, 245, 0.6)
+ }
+
+ /// .tertiaryLabel
+ public static var tertiaryLabelPerseus: Color {
+ return DarkModeAgent.shared.style == .light ?
+ rgba255(60, 60, 67, 0.3) : rgba255(235, 235, 245, 0.3)
+ }
+
+ /// .quaternaryLabel
+ public static var quaternaryLabelPerseus: Color {
+ return DarkModeAgent.shared.style == .light ?
+ rgba255(60, 60, 67, 0.18) : rgba255(235, 235, 245, 0.16)
+ }
+
+ /// .placeholderText
+ public static var placeholderTextPerseus: Color {
+ return DarkModeAgent.shared.style == .light ?
+ rgba255(60, 60, 67, 0.3) : rgba255(235, 235, 245, 0.3)
+ }
+
+ /// .separator
+ public static var separatorPerseus: Color {
+ return DarkModeAgent.shared.style == .light ?
+ rgba255(60, 60, 67, 0.29) : rgba255(84, 84, 88, 0.6)
+ }
+
+ /// .opaqueSeparator
+ public static var opaqueSeparatorPerseus: Color {
+ return DarkModeAgent.shared.style == .light ?
+ rgba255(198, 198, 200) : rgba255(56, 56, 58)
+ }
+
+ /// .link
+ public static var linkPerseus: Color {
+ return DarkModeAgent.shared.style == .light ?
+ rgba255(0, 122, 255) : rgba255(9, 132, 255)
+ }
+
+ /// .systemFill
+ public static var systemFillPerseus: Color {
+ return DarkModeAgent.shared.style == .light ?
+ rgba255(120, 120, 128, 0.2) : rgba255(120, 120, 128, 0.36)
+ }
+
+ /// .secondarySystemFill
+ public static var secondarySystemFillPerseus: Color {
+ return DarkModeAgent.shared.style == .light ?
+ rgba255(120, 120, 128, 0.16) : rgba255(120, 120, 128, 0.32)
+ }
+
+ /// .tertiarySystemFill
+ public static var tertiarySystemFillPerseus: Color {
+ return DarkModeAgent.shared.style == .light ?
+ rgba255(118, 118, 128, 0.12) : rgba255(118, 118, 128, 0.24)
+ }
+
+ /// .quaternarySystemFill
+ public static var quaternarySystemFillPerseus: Color {
+ return DarkModeAgent.shared.style == .light ?
+ rgba255(116, 116, 128, 0.08) : rgba255(118, 118, 128, 0.18)
+ }
+
+ /// .systemBackground
+ public static var systemBackgroundPerseus: Color {
+ return DarkModeAgent.shared.style == .light ?
+ rgba255(255, 255, 255) : rgba255(28, 28, 30)
+ }
+
+ /// .secondarySystemBackground
+ public static var secondarySystemBackgroundPerseus: Color {
+ return DarkModeAgent.shared.style == .light ?
+ rgba255(242, 242, 247) : rgba255(44, 44, 46)
+ }
+
+ /// .tertiarySystemBackground
+ public static var tertiarySystemBackgroundPerseus: Color {
+ return DarkModeAgent.shared.style == .light ?
+ rgba255(255, 255, 255) : rgba255(58, 58, 60)
+ }
+
+ /// .systemGroupedBackground
+ public static var systemGroupedBackgroundPerseus: Color {
+ return DarkModeAgent.shared.style == .light ?
+ rgba255(242, 242, 247) : rgba255(28, 28, 30)
+ }
+
+ /// .secondarySystemGroupedBackground
+ public static var secondarySystemGroupedBackgroundPerseus: Color {
+ return DarkModeAgent.shared.style == .light ?
+ rgba255(255, 255, 255) : rgba255(44, 44, 46)
+ }
+
+ /// .tertiarySystemGroupedBackground
+ public static var tertiarySystemGroupedBackgroundPerseus: Color {
+ return DarkModeAgent.shared.style == .light ?
+ rgba255(242, 242, 247) : rgba255(58, 58, 60)
+ }
+}
+
+extension Color: SystemColorProtocol {
+
+ /// .systemRed
+ public static var perseusRed: Color {
+ return DarkModeAgent.shared.style == .light ?
+ rgba255(255, 59, 48) : rgba255(255, 69, 58)
+ }
+
+ /// .systemOrange
+ public static var perseusOrange: Color {
+ return DarkModeAgent.shared.style == .light ?
+ rgba255(255, 149, 0) : rgba255(255, 159, 10)
+ }
+
+ /// .systemYellow
+ public static var perseusYellow: Color {
+ return DarkModeAgent.shared.style == .light ?
+ rgba255(255, 204, 0) : rgba255(255, 214, 10)
+ }
+
+ /// .systemGreen
+ public static var perseusGreen: Color {
+ return DarkModeAgent.shared.style == .light ?
+ rgba255(52, 199, 89) : rgba255(48, 209, 88)
+ }
+
+ /// .systemMint
+ public static var perseusMint: Color {
+ return DarkModeAgent.shared.style == .light ?
+ rgba255(0, 199, 190) : rgba255(102, 212, 207)
+ }
+
+ /// .systemTeal
+ public static var perseusTeal: Color {
+ return DarkModeAgent.shared.style == .light ?
+ rgba255(48, 176, 199) : rgba255(64, 200, 224)
+ }
+
+ /// .systemCyan
+ public static var perseusCyan: Color {
+ return DarkModeAgent.shared.style == .light ?
+ rgba255(50, 173, 230) : rgba255(100, 210, 255)
+ }
+
+ /// .systemBlue
+ public static var perseusBlue: Color {
+ return DarkModeAgent.shared.style == .light ?
+ rgba255(0, 122, 255) : rgba255(10, 132, 255)
+ }
+
+ /// .systemIndigo
+ public static var perseusIndigo: Color {
+ return DarkModeAgent.shared.style == .light ?
+ rgba255(88, 86, 214) : rgba255(94, 92, 230)
+ }
+
+ /// .systemPurple
+ public static var perseusPurple: Color {
+ return DarkModeAgent.shared.style == .light ?
+ rgba255(175, 82, 222) : rgba255(191, 90, 242)
+ }
+
+ /// .systemPink
+ public static var perseusPink: Color {
+ return DarkModeAgent.shared.style == .light ?
+ rgba255(255, 45, 85) : rgba255(255, 55, 95)
+ }
+
+ /// .systemBrown
+ public static var perseusBrown: Color {
+ return DarkModeAgent.shared.style == .light ?
+ rgba255(162, 132, 94) : rgba255(172, 142, 104)
+ }
+
+ /// .systemGray
+ public static var perseusGray: Color {
+ return DarkModeAgent.shared.style == .light ?
+ rgba255(142, 142, 147) : rgba255(142, 142, 147)
+ }
+
+ /// .systemGray2
+ public static var perseusGray2: Color {
+ return DarkModeAgent.shared.style == .light ?
+ rgba255(174, 174, 178) : rgba255(99, 99, 102)
+ }
+
+ /// .systemGray3
+ public static var perseusGray3: Color {
+ return DarkModeAgent.shared.style == .light ?
+ rgba255(199, 199, 204) : rgba255(72, 72, 74)
+ }
+
+ /// .systemGray4
+ public static var perseusGray4: Color {
+ return DarkModeAgent.shared.style == .light ?
+ rgba255(209, 209, 214) : rgba255(58, 58, 60)
+ }
+
+ /// .systemGray5
+ public static var perseusGray5: Color {
+ return DarkModeAgent.shared.style == .light ?
+ rgba255(229, 229, 234) : rgba255(44, 44, 46)
+ }
+
+ /// .systemGray6
+ public static var perseusGray6: Color {
+ return DarkModeAgent.shared.style == .light ?
+ rgba255(242, 242, 247) : rgba255(28, 28, 30)
+ }
+}
+
+#if os(iOS)
+
+@IBDesignable
+public class DarkModeImageView: UIImageView {
+
+ @IBInspectable
+ public var imageLight: UIImage? {
+ didSet {
+ light = imageLight
+ image = DarkMode.style == .light ? light : dark
+ }
+ }
+
+ @IBInspectable
+ public var imageDark: UIImage? {
+ didSet {
+ dark = imageDark
+ image = DarkMode.style == .light ? light : dark
+ }
+ }
+
+ private(set) var theDarknessTrigger: DarkModeObserver?
+
+ private(set) var light: UIImage?
+ private(set) var dark: UIImage?
+
+ override init(frame: CGRect) {
+ super.init(frame: frame)
+ configure()
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ super.init(coder: aDecoder)
+ configure()
+ }
+
+ private func configure() {
+ theDarknessTrigger = DarkModeObserver { style in
+ self.image = style == .light ? self.light : self.dark
+ }
+
+ image = DarkMode.style == .light ? self.light : self.dark
+ }
+
+ public func configure(_ light: UIImage?, _ dark: UIImage?) {
+ self.light = light
+ self.dark = dark
+
+ theDarknessTrigger?.action = { style in
+ self.image = style == .light ? self.light : self.dark
+ }
+
+ image = DarkMode.style == .light ? self.light : self.dark
+ }
+}
+
+#elseif os(macOS)
+
+public enum ScaleImageViewMacOS: Int, CustomStringConvertible {
+
+ case scaleNone = 0 // No scale at all
+ case axesIndependently = 1 // Aspect Fill
+ case proportionallyUpOrDown = 2 // Aspect Fit
+ case proportionallyDown = 3 // Center Top
+ case proportionallyClipToBounds = 4 // Aspect Fill with cliping to ImageView bounds
+
+ public var description: String {
+ switch self {
+ case .scaleNone:
+ return "As is, no scaling."
+ case .axesIndependently:
+ return "Aspect Fill."
+ case .proportionallyUpOrDown:
+ return "Aspect Fit."
+ case .proportionallyDown:
+ return "Center Top."
+ case .proportionallyClipToBounds:
+ return "Aspect Fill cliped to bounds."
+ }
+ }
+
+ public var value: NSImageScaling {
+ switch self {
+ case .scaleNone:
+ return .scaleNone
+ case .axesIndependently:
+ return .scaleAxesIndependently
+ case .proportionallyUpOrDown:
+ return .scaleProportionallyUpOrDown
+ case .proportionallyDown:
+ return .scaleProportionallyDown
+ case .proportionallyClipToBounds:
+ return .scaleNone
+ }
+ }
+}
+
+@IBDesignable
+public class DarkModeImageView: NSImageView {
+
+ @IBInspectable
+ public var imageLight: NSImage? {
+ didSet {
+ image = DarkMode.style == .light ? imageLight : imageDark
+ }
+ }
+
+ @IBInspectable
+ public var imageDark: NSImage? {
+ didSet {
+ image = DarkMode.style == .light ? imageLight : imageDark
+ }
+ }
+
+ @IBInspectable
+ public var aspectFillClipToBounds: Bool = false
+
+ public var customScale: ScaleImageViewMacOS = .scaleNone {
+ didSet {
+ guard customScale != .proportionallyClipToBounds else {
+
+ self.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
+ self.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
+
+ self.aspectFillClipToBounds = true
+ self.imageScaling = .scaleNone
+
+ return
+ }
+
+ self.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
+ self.setContentCompressionResistancePriority(.defaultHigh, for: .vertical)
+
+ self.aspectFillClipToBounds = false
+ self.imageScaling = customScale.value
+ }
+ }
+
+ private(set) var theDarknessTrigger: DarkModeObserver?
+
+ override public func awakeFromNib() {
+ guard aspectFillClipToBounds else { return }
+
+ self.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
+ self.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
+
+ self.imageScaling = .scaleNone
+ }
+
+ override init(frame: CGRect) {
+ super.init(frame: frame)
+ configure()
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ super.init(coder: aDecoder)
+ configure()
+ }
+
+ public func configure(_ light: NSImage?, _ dark: NSImage?) {
+ configure()
+
+ self.imageLight = light
+ self.imageDark = dark
+ }
+
+ private func configure() {
+ theDarknessTrigger = DarkModeObserver { style in
+ self.image = style == .light ? self.imageLight : self.imageDark
+ }
+ }
+
+ override public func draw(_ dirtyRect: NSRect) {
+ guard aspectFillClipToBounds, let image = self.image else {
+ super.draw(dirtyRect)
+ return
+ }
+
+ let viewWidth = self.bounds.size.width
+ let viewHeight = self.bounds.size.height
+
+ let width = image.size.width
+ let height = image.size.height
+
+ let imageViewRatio = viewWidth / viewHeight
+ let imageRatio = width / height
+
+ image.size.width = imageRatio < imageViewRatio ? viewWidth : viewHeight * imageRatio
+ image.size.height = imageRatio < imageViewRatio ? viewWidth / imageRatio : viewHeight
+
+ super.draw(dirtyRect)
+ }
+}
+
+#endif
diff --git a/Package.swift b/Package.swift
index 7b06ba7..ad7aada 100644
--- a/Package.swift
+++ b/Package.swift
@@ -1,23 +1,30 @@
-// swift-tools-version:4.2
+// swift-tools-version:5.7
/* Package.swift
- Version: 1.1.5
+ Version: 2.0.0
+
+ For iOS and macOS only. Use Stars to adopt for the specifics you need.
Created by Mikhail Zhigulin in 7530.
- Copyright © 7530 - 7531 Mikhail Zhigulin of Novosibirsk.
+ Copyright © 7530 - 7533 Mikhail A. Zhigulin of Novosibirsk
+ Copyright © 7533 PerseusRealDeal
Licensed under the MIT license. See LICENSE file.
All rights reserved.
Abstract:
- Package manifest for Perseus Dark Mode.
+ Package manifest for the Darkness.
*/
import PackageDescription
let package = Package(
name: "PerseusDarkMode",
+ platforms: [
+ .macOS(.v10_13),
+ .iOS(.v11)
+ ],
products: [
.library(
name: "PerseusDarkMode",
@@ -31,7 +38,7 @@ let package = Package(
name: "PerseusDarkMode",
dependencies: []),
.testTarget(
- name: "DarkModeTests",
+ name: "UnitTests",
dependencies: ["PerseusDarkMode"])
]
)
diff --git a/PerseusDarkMode.podspec b/PerseusDarkMode.podspec
deleted file mode 100644
index 98d3394..0000000
--- a/PerseusDarkMode.podspec
+++ /dev/null
@@ -1,22 +0,0 @@
-Pod::Spec.new do |p|
-
-p.name = "PerseusDarkMode"
-p.version = "1.1.4"
-p.summary = "Variable for changing Dark Mode."
-p.description = "Designed for constructing Dark Mode sensitive features."
-p.homepage = "https://github.com/perseusrealdeal/PerseusDarkMode"
-
-p.license = { :type => "MIT", :file => "LICENSE" }
-p.author = { "perseusrealdeal" => "mzhigulin@gmail.com" }
-
-p.source = { :git => "https://github.com/perseusrealdeal/PerseusDarkMode.git", :tag => p.version.to_s }
-
-p.ios.deployment_target = '9.3'
-p.osx.deployment_target = '10.9'
-
-p.swift_version = "4.2"
-p.requires_arc = true
-
-p.source_files = 'Sources/**/*.swift'
-
-end
diff --git a/PerseusDarkModeSingle.swift b/PerseusDarkModeSingle.swift
deleted file mode 100644
index 8d8f720..0000000
--- a/PerseusDarkModeSingle.swift
+++ /dev/null
@@ -1,451 +0,0 @@
-//
-// PerseusDarkModeSingle.swift
-// Version: 1.1.5
-//
-// Created by Mikhail Zhigulin in 7530.
-//
-// Copyright © 7530 - 7531 Mikhail Zhigulin of Novosibirsk.
-// All rights reserved.
-//
-//
-// MIT License
-//
-// Copyright © 7530 - 7531 Mikhail Zhigulin of Novosibirsk
-//
-// The year starts from the creation of the world according to a Slavic calendar.
-// September, the 1st of Slavic year.
-//
-// 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.
-//
-// swiftlint:disable file_length block_based_kvo
-//
-
-#if canImport(UIKit)
-import UIKit
-#elseif canImport(Cocoa)
-import Cocoa
-#endif
-
-#if os(iOS)
-public typealias Responder = UIResponder
-#elseif os(macOS)
-public typealias Responder = NSResponder
-#endif
-
-// MARK: - Notifications
-
-public extension Notification.Name {
- static let MakeAppearanceUpNotification =
- Notification.Name("MakeAppearanceUpNotification")
-#if os(macOS)
- static let AppleInterfaceThemeChangedNotification =
- Notification.Name("AppleInterfaceThemeChangedNotification")
-#endif
-}
-
-// MARK: - Constants
-
-public let DARK_MODE_USER_CHOICE_KEY = "DarkModeUserChoiceOptionKey"
-public let DARK_MODE_USER_CHOICE_DEFAULT = DarkModeOption.auto
-public let DARK_MODE_STYLE_DEFAULT = AppearanceStyle.light
-public let OBSERVERED_VARIABLE_NAME = "styleObservable"
-
-// MARK: - Appearance service
-
-// swiftlint:disable identifier_name
-public extension Responder {
- var DarkMode: DarkMode { return AppearanceService.shared }
-}
-// swiftlint:enable identifier_name
-
-public class AppearanceService {
-
- public static var shared: DarkMode = { _ = it; return DarkMode() }()
-
- private(set) static var it = { AppearanceService() }()
- private init() {
-#if os(macOS)
- AppearanceService.distributedNCenter.addObserver(
- self,
- selector: #selector(interfaceModeChanged),
- name: .AppleInterfaceThemeChangedNotification,
- object: nil
- )
-#endif
- }
-
-#if os(macOS)
- @objc internal func interfaceModeChanged() {
- if #available(macOS 10.14, *) {
- AppearanceService.processAppearanceOSDidChange()
- }
- }
-
- @available(macOS 10.14, *)
- public static var defaultDarkAppearanceOS: NSAppearance.Name = .darkAqua
- public static var defaultLightAppearanceOS: NSAppearance.Name = .aqua
-#endif
-
- public static var isEnabled: Bool { return hidden_isEnabled }
-
-#if os(macOS)
- /// Default Distributed NotificationCenter.
- public static var distributedNCenter = DistributedNotificationCenter.default
-#endif
-
- /// Default NotificationCenter.
- public static var nCenter = NotificationCenter.default
- /// Default UserDefaults.
- public static var ud = UserDefaults.standard
-
- public static var DarkModeUserChoice: DarkModeOption {
- get {
- // Load enum Int value
-
- let rawValue = ud.valueExists(forKey: DARK_MODE_USER_CHOICE_KEY) ?
- ud.integer(forKey: DARK_MODE_USER_CHOICE_KEY) :
- DARK_MODE_USER_CHOICE_DEFAULT.rawValue
-
- // Try to cast Int value to enum
-
- if let result = DarkModeOption.init(rawValue: rawValue) { return result }
-
- return DARK_MODE_USER_CHOICE_DEFAULT
- }
- set {
- ud.setValue(newValue.rawValue, forKey: DARK_MODE_USER_CHOICE_KEY)
-
- // Used for KVO to immediately notify a change has happened
- recalculateStyleIfNeeded()
- }
- }
-
- // MARK: - Public API: register stakeholder
-
- public static func register(stakeholder: Any, selector: Selector) {
- nCenter.addObserver(stakeholder,
- selector: selector,
- name: .MakeAppearanceUpNotification,
- object: nil)
- }
-
- // MARK: - Public API: make the app's appearance up
-
- public static func makeUp() {
- hidden_isEnabled = true
- hidden_changeManually = true
-
- if #available(iOS 13.0, macOS 10.14, *) { overrideUserInterfaceStyleIfNeeded() }
-
- recalculateStyleIfNeeded()
-
- nCenter.post(name: .MakeAppearanceUpNotification, object: nil)
- hidden_changeManually = false
- }
-
-#if os(iOS)
- @available(iOS 13.0, *)
- public static func processTraitCollectionDidChange(
- _ previousTraitCollection: UITraitCollection?) {
- if hidden_changeManually { return }
-
- guard let previousSystemStyle = previousTraitCollection?.userInterfaceStyle,
- previousSystemStyle.rawValue != shared.systemStyle.rawValue
- else { return }
-
- hidden_systemCalledMakeUp()
- }
-#elseif os(macOS)
- @available(macOS 10.14, *)
- internal static func processAppearanceOSDidChange() {
- if hidden_changeManually { return }
- hidden_systemCalledMakeUp()
- }
-#endif
-
- // MARK: - Implementation helpers, privates and internals
-
- private(set) static var hidden_isEnabled: Bool = false {
- willSet { if newValue == false { return }}
- }
-
- internal static var hidden_changeManually: Bool = false
-
- internal static func hidden_systemCalledMakeUp() {
- if hidden_changeManually { return }
-
- hidden_isEnabled = true
-
- recalculateStyleIfNeeded()
- nCenter.post(name: .MakeAppearanceUpNotification, object: nil)
- }
-
- public static func recalculateStyleIfNeeded() {
- let actualStyle = DarkModeDecision.calculate(DarkModeUserChoice, shared.systemStyle)
-
- if shared.hidden_style != actualStyle { shared.hidden_style = actualStyle }
- }
-
- @available(iOS 13.0, macOS 10.14, *)
- internal static func overrideUserInterfaceStyleIfNeeded() {
- if hidden_changeManually == false { return }
-#if os(iOS) && compiler(>=5)
- guard let keyWindow = UIWindow.key else { return }
- var overrideStyle: UIUserInterfaceStyle = .unspecified
-
- switch DarkModeUserChoice {
- case .auto:
- overrideStyle = .unspecified
-
- case .on:
- overrideStyle = .dark
-
- case .off:
- overrideStyle = .light
- }
-
- keyWindow.overrideUserInterfaceStyle = overrideStyle
-
-#elseif os(macOS)
- switch DarkModeUserChoice {
- case .auto:
- NSApplication.shared.appearance = nil
- case .on:
- NSApplication.shared.appearance =
- NSAppearance(named: AppearanceService.defaultDarkAppearanceOS)
- case .off:
- NSApplication.shared.appearance =
- NSAppearance(named: AppearanceService.defaultLightAppearanceOS)
- }
-#endif
- }
-}
-
-// MARK: - Dark Mode
-
-public class DarkMode: NSObject {
-
- // MARK: - The App's current Appearance Style
-
- public var style: AppearanceStyle { return hidden_style }
-
- // MARK: - Observable Appearance Style Value (Using Key-Value Observing)
-
- @objc public dynamic var styleObservable: Int = DARK_MODE_STYLE_DEFAULT.rawValue
-
- // MARK: - System's Appearance Style
-
- public var systemStyle: SystemStyle {
- if #available(iOS 13.0, macOS 10.14, *) {
-#if os(iOS)
- guard let keyWindow = UIWindow.key else { return .unspecified }
-
- switch keyWindow.traitCollection.userInterfaceStyle {
- case .unspecified:
- return .unspecified
- case .light:
- return .light
- case .dark:
- return .dark
-
- @unknown default:
- return .unspecified
- }
-#elseif os(macOS)
- if let isDark = UserDefaults.standard.string(forKey: "AppleInterfaceStyle"),
- isDark == "Dark" {
- return .dark
- } else {
- return .light
- }
-#endif
- } else {
- return .unspecified
- }
- }
-
- internal var hidden_style: AppearanceStyle = DARK_MODE_STYLE_DEFAULT {
- didSet { styleObservable = style.rawValue }
- }
-}
-
-// MARK: - Dark Mode decision-making table
-
-public class DarkModeDecision {
- private init() { }
-
- // MARK: - Calculating Dark Mode decision
-
- /// Calculates the current appearance style of the app.
- ///
- /// Dark Mode decision-making:
- ///
- /// | DarkModeOption
- /// -------------+-----------------------
- /// SystemStyle | auto | on | off
- /// -------------+---------+------+------
- /// .unspecified | default | dark | light
- /// .light | light | dark | light
- /// .dark | dark | dark | light
- ///
- public class func calculate(_ userChoice: DarkModeOption,
- _ systemStyle: SystemStyle) -> AppearanceStyle {
- // Calculate outputs
-
- if (systemStyle == .unspecified) && (userChoice == .auto) {
- return DARK_MODE_STYLE_DEFAULT
- }
- if (systemStyle == .unspecified) && (userChoice == .on) { return .dark }
- if (systemStyle == .unspecified) && (userChoice == .off) { return .light }
-
- if (systemStyle == .light) && (userChoice == .auto) { return .light }
- if (systemStyle == .light) && (userChoice == .on) { return .dark }
- if (systemStyle == .light) && (userChoice == .off) { return .light }
-
- if (systemStyle == .dark) && (userChoice == .auto) { return .dark }
- if (systemStyle == .dark) && (userChoice == .on) { return .dark }
- if (systemStyle == .dark) && (userChoice == .off) { return .light }
-
- // Output default value if somethings goes out of the decision table
-
- return DARK_MODE_STYLE_DEFAULT
- }
-}
-
-// MARK: - Appearance Style Observering
-
-public class DarkModeObserver: NSObject {
-
- public var action: ((_ newStyle: AppearanceStyle) -> Void)?
- private(set) var objectToObserve = AppearanceService.shared
-
- public override init() {
- super.init()
-
- objectToObserve.addObserver(self,
- forKeyPath: OBSERVERED_VARIABLE_NAME,
- options: .new,
- context: nil)
- }
-
- public init(_ action: @escaping ((_ newStyle: AppearanceStyle) -> Void)) {
- super.init()
-
- self.action = action
- objectToObserve.addObserver(self,
- forKeyPath: OBSERVERED_VARIABLE_NAME,
- options: .new,
- context: nil)
- }
-
- public override func observeValue(forKeyPath keyPath: String?,
- of object: Any?,
- change: [NSKeyValueChangeKey: Any]?,
- context: UnsafeMutableRawPointer?) {
- guard
- keyPath == OBSERVERED_VARIABLE_NAME,
- let style = change?[.newKey],
- let styleRawValue = style as? Int,
- let newStyle = AppearanceStyle.init(rawValue: styleRawValue)
- else { return }
-
- action?(newStyle)
- }
-
- deinit {
- objectToObserve.removeObserver(self, forKeyPath: OBSERVERED_VARIABLE_NAME)
- }
-}
-
-// MARK: - Dark Mode Option
-
-public enum DarkModeOption: Int, CustomStringConvertible {
-
- case auto = 0
- case on = 1
- case off = 2
-
- public var description: String {
- switch self {
- case .auto:
- return ".auto"
- case .on:
- return ".on"
- case .off:
- return ".off"
- }
- }
-}
-
-// MARK: - Appearance Style
-
-public enum AppearanceStyle: Int, CustomStringConvertible {
-
- case light = 0
- case dark = 1
-
- public var description: String {
- switch self {
- case .light:
- return ".light"
- case .dark:
- return ".dark"
- }
- }
-}
-
-// MARK: - System Style
-
-public enum SystemStyle: Int, CustomStringConvertible {
-
- case unspecified = 0
- case light = 1
- case dark = 2
-
- public var description: String {
- switch self {
- case .unspecified:
- return ".unspecified"
- case .light:
- return ".light"
- case .dark:
- return ".dark"
- }
- }
-}
-
-// MARK: - Helpers
-
-extension UserDefaults {
- public func valueExists(forKey key: String) -> Bool {
- return object(forKey: key) != nil
- }
-}
-
-#if os(iOS)
-extension UIWindow {
- static var key: UIWindow? {
- if #available(iOS 13, *) {
- return UIApplication.shared.windows.first { $0.isKeyWindow }
- } else {
- return UIApplication.shared.keyWindow
- }
- }
-}
-#endif
diff --git a/README.md b/README.md
index 1337e00..719f3f2 100644
--- a/README.md
+++ b/README.md
@@ -1,204 +1,359 @@
-# Perseus Dark Mode
+# PerseusDarkMode — Xcode 14.2+
-[](https://github.com/perseusrealdeal/PerseusDarkMode/actions)
-
-[](/PerseusDarkMode.podspec)
-
-[](https://docs.swift.org/swift-book/RevisionHistory/RevisionHistory.html)
+[`iOS approbation app`](https://github.com/perseusrealdeal/TheOneRing) [`macOS approbation app`](https://github.com/perseusrealdeal/Arkenstone)
+
+> The light-weight darkness in Swift you can force. Hereinafter PDM stands for Perseus Dark Mode.
+
+> - To build option kinda `Night/Day/System Mode` or `On/Off/System Dark Mode`.
+> - To be awared of Dark Mode changes if you need.
+
+> `PDM` is a single author and personale solution developed in `person-to-person` relationship paradigm.
+
+[](https://github.com/perseusrealdeal/PerseusDarkMode/actions/workflows/main.yml)
+[](https://github.com/perseusrealdeal/PerseusDarkMode/actions/workflows/swiftlint.yml)
+[](/CHANGELOG.md)
+[](https://en.wikipedia.org/wiki/List_of_Apple_products)
+[](https://en.wikipedia.org/wiki/Xcode)
+[](https://www.swift.org)
[](/LICENSE)
## Integration Capabilities
-[](/PerseusDarkModeSingle.swift)
-[](https://github.com/Carthage/Carthage)
-[](/PerseusDarkMode.podspec)
+[](/PDMStar.swift)
[](/Package.swift)
-## Demo Apps and Others
+> Use Stars to adopt [`PDM`](/PDMStar.swift) for the specifics you need.
-[](https://github.com/perseusrealdeal/ios.darkmode.discovery.git)
-[](https://github.com/perseusrealdeal/macos.darkmode.discovery.git)
-[](https://github.com/perseusrealdeal/PerseusUISystemKit.git)
-[](https://github.com/perseusrealdeal/XcodeTemplateProject.git)
+# Support Code
-# In Brief
+[](/PDMSupportingStar.swift)
+[](http://unlicense.org/)
-> This library lets a developer being awared of Dark Mode via a variable `DarkMode.style`. Also, with this library it is possible to change the value of Dark Mode in runtime easily with standalone lib [DarkModeSwitching](https://gist.github.com/perseusrealdeal/11b1bab47f13134832b859f49d9af706).
+> [`PDMSupportingStar.swift`](/PDMSupportingStar.swift) is a peace of code a widly helpful in accord with PDM.
-```swift
-changeDarkModeManually(.off) // .on or .auto
-```
-Sample screen for changing Dark Mode option [here for macOS](https://github.com/perseusrealdeal/macos.darkmode.discovery) and [here for iOS](https://github.com/perseusrealdeal/ios.darkmode.discovery).
+> `PDMSupportingStar.swift` goes as an external part of `PDM`.
+
+## Approbation Matrix
-# Reqirements
+> [`A3 Environment and Approbation`](/APPROBATION.md) / [`CHANGELOG`](/CHANGELOG.md) for details.
-- Xcode 10.1+
-- Swift 4.2+
-- iOS: 9.3+, UIKit SDK
-- macOS: 10.9+, AppKit SDK
+## In brief > Idea to use, the Why
-# First-party software
+> THE DARKNESS YOU CAN FORCE.
-- [PerseusLogger](https://gist.github.com/perseusrealdeal/df456a9825fcface44eca738056eb6d5)
+
+
+ | iOS window |
+ iOS Settings bundle |
+ macOS window |
+
+
+  |
+
+
+
+ |
+  |
+
+
-# Third-party software
+## Build requirements
-- [SwiftLint Shell Script Runner](/SucceedsPostAction.sh)
-- [SwiftLint](https://github.com/realm/SwiftLint) / [0.31.0: Busy Laundromat](https://github.com/realm/SwiftLint/releases/tag/0.31.0) for macOS High Sierra
+- [macOS Monterey 12.7.6+](https://apps.apple.com/by/app/macos-monterey/id1576738294) / [Xcode 14.2+](https://developer.apple.com/services-account/download?path=/Developer_Tools/Xcode_14.2/Xcode_14.2.xip)
+
+> But as the single source code file [PDMStar.swift](/PDMStar.swift) PDM can be used even in Xcode 10.1.
+
+## First-party software
+
+- [ConsolePerseusLogger](https://github.com/perseusrealdeal/ConsolePerseusLogger) / [1.1.0](https://github.com/perseusrealdeal/ConsolePerseusLogger/releases/tag/1.1.0)
+
+## Third-party software
+
+- Style [SwiftLint](https://github.com/realm/SwiftLint) / [Shell Script](/SucceedsPostAction.sh)
+- Action [mxcl/xcodebuild@v3](https://github.com/mxcl/xcodebuild/releases/tag/v3.5.1)
+- Action [cirruslabs/swiftlint-action@v1](https://github.com/cirruslabs/swiftlint-action/releases/tag/v1.0.0)
# Installation
-> ***Using "Exact" with the Version field is strongly recommended.***
+`Step 1:` Import the Darkness either with SPM or standalone
+
+> Standalone: the single source code file [PDMStar.swift](/PDMStar.swift)
-## Step 1: Add PerseusDarkMode to a host project tree
+> Swift Package Manager: `https://github.com/perseusrealdeal/PerseusDarkMode`
-### Standalone
+## Steps for Cocoa macOS project
-Make a copy of the file [`PerseusDarkModeSingle.swift`](/PerseusDarkModeSingle.swift) then put it into a place required of a host project.
+`Step 2:` In the AppDelegate when `applicationDidFinishLaunching` call `force`
-### Carthage
+```swift
+
+import Cocoa
+import PerseusDarkMode
-Cartfile should contain:
+class AppDelegate: NSObject, NSApplicationDelegate {
+
+ func applicationDidFinishLaunching(_ aNotification: Notification) {
+ DarkModeAgent.force(DarkModeUserChoice)
+ }
+}
-```carthage
-github "perseusrealdeal/PerseusDarkMode" == 1.1.5
```
-Some Carthage usage tips placed [here](https://gist.github.com/perseusrealdeal/8951b10f4330325df6347aaaa79d3cf2).
+`Step 3:` Register the MainWindowController for Dark Mode changes
+
+```swift
-### CocoaPods
+import Cocoa
+import PerseusDarkMode
-Podfile should contain:
+class MainWindowController: NSWindowController, NSWindowDelegate {
+
+ override func windowDidLoad() {
+ super.windowDidLoad()
+
+ DarkModeAgent.register(stakeholder: self, selector: #selector(makeUp))
+ }
+
+ @objc private func makeUp() {
+
+ // Runs every time if Dark Mode changes.
+ // The current DarkMode value is reliable here.
+
+ let isDark = DarkMode.style == .dark
+ let _ = isDark ? "It's dark" : "No dark"
+
+ }
+}
-```ruby
-target "ProjectTarget" do
- use_frameworks!
- pod 'PerseusDarkMode', '1.1.4'
-end
```
-### Swift Package Manager
+## Steps for UIKit iOS project
-- As a package dependency so Package.swift should contain the following statements:
+`Step 2:` In the AppDelegate when `didFinishLaunchingWithOptions` call `force`
```swift
-dependencies: [
- .package(url: "https://github.com/perseusrealdeal/PerseusDarkMode.git",
- .exact("1.1.5"))
- ],
-```
-- As an Xcode project dependency:
+import UIKit
+import PerseusDarkMode
-`Project in the Navigator > Package Dependencies > Add Package Dependency`
+class AppDelegate: UIResponder { var window: UIWindow? }
-> ***Using "Exact" with the Version field is strongly recommended.***
+extension AppDelegate: UIApplicationDelegate {
-## Step 2: Make DarkMode ready for using
+ func application(_ application: UIApplication, didFinishLaunchingWithOptions
+ launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
-### iOS
+ // Register Settings Bundle
+ registerSettingsBundle()
-Override the following method of the first screen to let Perseus know that system's appearance changed:
+ // 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()
+
+ DarkModeAgent.force(DarkModeUserChoice)
+
+ return true
+ }
+
+ func applicationDidBecomeActive(_ application: UIApplication) {
+
+ // Actualize Dark Mode style to Settings Bundle
+ if let choice = DarkModeAgent.isDarkModeSettingsKeyChanged() {
+ DarkModeAgent.force(choice)
+ }
+ }
+}
+
+```
+
+`Step 3:` Register the MainViewController and process traitCollectionDidChange for DarkMode changes
```swift
+
+import UIKit
+import PerseusDarkMode
+
class MainViewController: UIViewController {
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ DarkModeAgent.register(stakeholder: self, selector: #selector(makeUp))
+ }
+
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
if #available(iOS 13.0, *) {
- AppearanceService.processTraitCollectionDidChange(previousTraitCollection)
+ DarkModeAgent.processTraitCollectionDidChange(previousTraitCollection)
}
}
+
+ @objc private func makeUp() {
+
+ // Runs every time if Dark Mode changes.
+ // The current DarkMode value is reliable here.
+
+ let isDark = DarkMode.style == .dark
+ let _ = isDark ? "It's dark" : "No dark"
+
+ }
}
+
```
-Also, if Dark Mode is released with Settings bundle put the statements into the app's delegate:
+# Usage
+
+## Force Dark Mode
+
+> The Dark Mode of your app can be easely forced in `.on`, `.off` or `.auto` just call method `force` of DarkModeAgent like this.
```swift
-extension AppDelegate: UIApplicationDelegate {
- func applicationDidBecomeActive(_ application: UIApplication) {
+DarkModeAgent.force(.auto) // That's all.
- // Update Dark Mode from Settings
- if let choice = isDarkModeSettingsChanged() {
- // Change Dark Mode value in Perseus Dark Mode library
- AppearanceService.DarkModeUserChoice = choice
- // Update appearance in accoring with changed Dark Mode Style
- AppearanceService.makeUp()
- }
- }
-}
```
-Used functions are distributed via standalone file [`DarkModeSwitching.swift`](https://gist.github.com/perseusrealdeal/11b1bab47f13134832b859f49d9af706).
-### iOS and macOS
+The `force` will change the appearance of your app immediately including system components and will make run all custom DarkMode code `makeUp()`.
+
+## Get awared of DarkMode Changes
+
+> To declare custom DarkMode sensitive code that runs every time if DarkMode Changes register the object or creat a DarkMode trigger:
-Call the method `AppearanceService.makeUp()` with the app's delegate if appearance changing is going to take place:
+`Use Case -` Register an object to be notified on changes
```swift
-extension AppDelegate: UIApplicationDelegate {
- func application(_ application: UIApplication, didFinishLaunchingWithOptions
- launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
-
- // ... code
-
- // Call AppearanceService.makeUp() method if AppearanceService.register(:, :)
- // is taken into account
- AppearanceService.makeUp()
+class DarkModeSensitiveObject {
+
+ init() {
+ DarkModeAgent.register(stakeholder: self, selector: #selector(makeUp))
+ }
- // ... otherwise call AppearanceService.recalculateStyleIfNeeded()
- // to load DarkMode.style from user defaults
- // AppearanceService.recalculateStyleIfNeeded()
+ @objc private func makeUp() {
+ // Runs evary time if Dark Mode changes.
}
}
+
```
-Copy the file [`DarkModeSwitching.swift`](https://gist.github.com/perseusrealdeal/11b1bab47f13134832b859f49d9af706) into a host project for having fun with manual changing Dark Mode value.
-# Usage
+`Use Case -` Creat a DarkMode trigger and give it an action
-Each time if Dark Mode changed the mentioned method `#selector(makeUp)` called, but registering is required:
```swift
-class MainViewController: UIViewController {
- // At any view controller where changing is required
+class DarkModeSensitiveObject {
- override func viewDidLoad() {
- super.viewDidLoad()
+ private var theDarknessTrigger = DarkModeObserver()
- AppearanceService.register(stakeholder: self, selector: #selector(makeUp))
+ init() {
+ theDarnessTrigger.action = { _ in
+ self.makeUp()
+ }
}
- @objc private func makeUp() {
- print("^_^ \(AppearanceService.DarkModeUserChoice)"
-
- switch DarkMode.style {
- case .light:
- // make drawings for light mode
- break
- case .dark:
- // make drawings for dark mode
- break
- }
+ private func makeUp() {
+ // Runs evary time if Dark Mode changes.
}
}
+
```
-There is another way to be notified of Dark Mode—KVO.
+## React to DarkMode Changes
+
+`Use Case -` Custom DarkMode Sensitive Color
-> [`DarkModeImageView`](https://github.com/perseusrealdeal/PerseusUISystemKit/blob/master/Sources/PerseusUISystemKit/Classes/DarkModeImageView.swift) class is an expressive sample of Dark Mode KVO usage for both macOS and iOS as well.
+```swift
+
+import PerseusDarkMode
+
+#if canImport(UIKit)
+import UIKit
+#elseif canImport(Cocoa)
+import Cocoa
+#endif
+
+#if os(iOS)
+public typealias Color = UIColor
+#elseif os(macOS)
+public typealias Color = NSColor
+#endif
+
+public func rgba255(_ red: CGFloat,
+ _ green: CGFloat,
+ _ blue: CGFloat,
+ _ alpha: CGFloat = 1.0) -> Color {
+ return Color(red: red/255, green: green/255, blue: blue/255, alpha: alpha)
+}
+
+extension Color {
+ public static var customRed: Color {
+ return DarkModeAgent.shared.style == .light ?
+ rgba255(255, 59, 48) : rgba255(255, 69, 58)
+ }
+}
+
+```
+
+> Use Custom DarkMode sensitive color.
+
+```swift
+
+// Runs every time if the DarkMode changes. Use KVO (DarkModeObserver) or be registered by DarkModeAgent.
+@objc private func makeUp() {
+ self.backgroundColor = .customRed
+}
+
+```
+
+# Points taken into account
+
+- Preconfigured Swift Package manifest [Package.swift](/Package.swift)
+- Preconfigured SwiftLint config [.swiftlint.yml](/.swiftlint.yml)
+- Preconfigured SwiftLint CI [swiftlint.yml](/.github/workflows/swiftlint.yml)
+- Preconfigured GitHub config [.gitignore](/.gitignore)
+- Preconfigured GitHub CI [main.yml](/.github/workflows/main.yml)
# License MIT
-Copyright © 7530 - 7531 Mikhail Zhigulin of Novosibirsk.
+Copyright © 7530 - 7533 Mikhail A. Zhigulin of Novosibirsk
+Copyright © 7533 PerseusRealDeal
- The year starts from the creation of the world according to a Slavic calendar.
-- September, the 1st of Slavic year.
+- September, the 1st of Slavic year. It means that "Sep 01, 2024" is the beginning of 7533.
[LICENSE](/LICENSE) for details.
-# Author
+## Credits
+
+
+
+ | Balance and Control |
+ kept by |
+ Mikhail A. Zhigulin |
+
+
+ | Source Code |
+ written by |
+ Mikhail A. Zhigulin |
+
+
+ | Documentation |
+ prepared by |
+ Mikhail A. Zhigulin |
+
+
+ | Product Approbation |
+ tested by |
+ Mikhail A. Zhigulin |
+
+
+
+- Language support: [Reverso](https://www.reverso.net/)
+- Git clients: [SmartGit](https://syntevo.com/) and [GitHub Desktop](https://github.com/apps/desktop)
-> `PerseusDarkMode` was written at Novosibirsk by Mikhail Zhigulin i.e. me, mzhigulin@gmail.com.
+# Author
-> Mostly I'd like thank my lovely parents for supporting me in all my ways.
+> Mikhail A. Zhigulin of Novosibirsk.
diff --git a/Sources/PerseusDarkMode/AppearanceService/AppearanceService.swift b/Sources/PerseusDarkMode/AppearanceService/AppearanceService.swift
deleted file mode 100644
index be16d03..0000000
--- a/Sources/PerseusDarkMode/AppearanceService/AppearanceService.swift
+++ /dev/null
@@ -1,291 +0,0 @@
-//
-// AppearanceService.swift
-// PerseusDarkMode
-//
-// Created by Mikhail Zhigulin in 7530.
-//
-// Copyright © 7530 - 7531 Mikhail Zhigulin of Novosibirsk.
-//
-// Licensed under the MIT license. See LICENSE file.
-// All rights reserved.
-//
-// swiftlint:disable file_length
-//
-
-#if canImport(UIKit)
-import UIKit
-#elseif canImport(Cocoa)
-import Cocoa
-#endif
-
-#if os(iOS)
-public typealias Responder = UIResponder
-#elseif os(macOS)
-public typealias Responder = NSResponder
-#endif
-
-/// Name of make up notification.
-public extension Notification.Name {
- static let MakeAppearanceUpNotification = Notification.Name("MakeAppearanceUpNotification")
-#if os(macOS)
- static let AppleInterfaceThemeChangedNotification =
- Notification.Name("AppleInterfaceThemeChangedNotification")
-#endif
-}
-
-// swiftlint:disable identifier_name
-
-/// Dark Mode placed to to be accessed from any screen object
-/// of iOS (Mac Catalyst) or macOS (Cocoa).
-public extension Responder {
- var DarkMode: DarkModeProtocol { return AppearanceService.shared }
-}
-// swiftlint:enable identifier_name
-
-/// Represents service giving a control of the app's appearance.
-///
-/// - This service is dedicated to handle Dark Mode changing.
-public class AppearanceService {
-
- // MARK: - Singleton
-
- /// Shared Dark Mode.
- public static var shared: DarkMode = { _ = it; return DarkMode() }()
-
- private(set) static var it = { AppearanceService() }()
- private init() {
- log.message("[\(type(of: self))].\(#function)")
-#if os(macOS)
- AppearanceService.distributedNCenter.addObserver(
- self,
- selector: #selector(modeChanged),
- name: .AppleInterfaceThemeChangedNotification,
- object: nil
- )
-#endif
- }
-
-#if os(macOS)
- @objc internal func modeChanged() {
- if #available(macOS 10.14, *) {
- AppearanceService.processAppearanceOSDidChange()
- }
- }
-
- @available(macOS 10.14, *)
- public static var defaultDarkAppearanceOS: NSAppearance.Name = .darkAqua
- public static var defaultLightAppearanceOS: NSAppearance.Name = .aqua
-#endif
-
- /// TRUE if Appearance.makeUp once called otherwise FALSE.
- ///
- /// Value is false by default and changed only once
- /// when Appearance.makeUp called for the first time, then always true in run time.
- public static var isEnabled: Bool { return hidden_isEnabled }
-
-#if DEBUG && os(macOS)
- /// Used for mocking DistributedNotificationCenter in unit testing.
- public static var distributedNCenter: NotificationCenterProtocol =
- DistributedNotificationCenter.default
-#elseif os(macOS)
- /// Default Distributed NotificationCenter.
- public static var distributedNCenter = DistributedNotificationCenter.default
-#endif
-
-#if DEBUG // Isolated for unit testing
- /// Used for mocking NotificationCenter in unit testing.
- public static var nCenter: NotificationCenterProtocol = NotificationCenter.default
- /// Used for mocking UserDefaults in unit testing.
- public static var ud: UserDefaultsProtocol = UserDefaults.standard
-#else
- /// Default NotificationCenter.
- public static var nCenter = NotificationCenter.default
- /// Default UserDefaults.
- public static var ud = UserDefaults.standard
-#endif
-
- /// User choice for Dark Mode inside the app.
- ///
- /// The service keeps the value in UserDefaults.
- /// It effects DarkMode.StyleObservable on every change.
- public static var DarkModeUserChoice: DarkModeOption {
- get {
- // Load enum Int value
-
- let rawValue = ud.valueExists(forKey: DARK_MODE_USER_CHOICE_KEY) ?
- ud.integer(forKey: DARK_MODE_USER_CHOICE_KEY) :
- DARK_MODE_USER_CHOICE_DEFAULT.rawValue
-
- // Try to cast Int value to enum
-
- if let result = DarkModeOption.init(rawValue: rawValue) { return result }
-
- return DARK_MODE_USER_CHOICE_DEFAULT
- }
- set {
- ud.setValue(newValue.rawValue, forKey: DARK_MODE_USER_CHOICE_KEY)
-
- // Used for KVO to immediately notify a change has happened
- recalculateStyleIfNeeded()
- }
- }
-
- // MARK: - Public API: register stakeholder
-
- /// Registers stakeholders of Dark Mode.
- /// - Parameters:
- /// - stakeholder: Stakeholder of Dark Mode.
- /// - selector: Point to pass a control of Dark Mode.
- public static func register(stakeholder: Any, selector: Selector) {
- nCenter.addObserver(stakeholder,
- selector: selector,
- name: .MakeAppearanceUpNotification,
- object: nil)
- }
-
- // MARK: - Public API: make the app's appearance up
-
- /// Calls all registered stakeholders for making up.
- ///
- /// First time should be called when didFinishLaunching happens and then every
- /// time when DarkModeUserChoice changes.
- public static func makeUp() {
- hidden_isEnabled = true
- hidden_changeManually = true
-
- if #available(iOS 13.0, macOS 10.14, *) { overrideUserInterfaceStyleIfNeeded() }
-
- recalculateStyleIfNeeded()
-
- nCenter.post(name: .MakeAppearanceUpNotification, object: nil)
- hidden_changeManually = false
- }
-
-#if os(iOS)
- /// Puts the app's Dark Mode in line with System Appearance Mode.
- ///
- /// Should be called when user taggles System Appearance Mode in Settings app.
- /// Call it in override func traitCollectionDidChange in the main screen.
- ///
- /// - Parameter previousTraitCollection: Used to extract userInterfaceStyle value.
- @available(iOS 13.0, *)
- public static func processTraitCollectionDidChange(
- _ previousTraitCollection: UITraitCollection?) {
- if hidden_changeManually { return }
-
- guard let previousSystemStyle = previousTraitCollection?.userInterfaceStyle,
- previousSystemStyle.rawValue != shared.systemStyle.rawValue
- else { return }
-
- hidden_systemCalledMakeUp()
- }
-#elseif os(macOS)
- @available(macOS 10.14, *)
- internal static func processAppearanceOSDidChange() {
- if hidden_changeManually { return }
- hidden_systemCalledMakeUp()
- }
-#endif
-
- // MARK: - Implementation helpers, privates and internals
-
- /// Used to make possible applying Black White approach in Screen design.
- private(set) static var hidden_isEnabled: Bool =
- false { willSet { if newValue == false { return }}}
-
- /// Used to reduce double calling of traitCollectionDidChange.
- internal static var hidden_changeManually: Bool = false
-
- /// Make up if TraitCollectionDidChange.
- internal static func hidden_systemCalledMakeUp() {
- if hidden_changeManually { return }
-
- hidden_isEnabled = true
-
- recalculateStyleIfNeeded()
- nCenter.post(name: .MakeAppearanceUpNotification, object: nil)
- }
-
- /// Updates the app's appearance style value.
- public static func recalculateStyleIfNeeded() {
- let actualStyle = DarkModeDecision.calculate(DarkModeUserChoice, shared.systemStyle)
-
- if shared.hidden_style != actualStyle { shared.hidden_style = actualStyle }
- }
-
- /// Changes the app's UserInterfaceStyle.
- ///
- /// It's matter to change the look of system user controls.
- @available(iOS 13.0, macOS 10.14, *)
- internal static func overrideUserInterfaceStyleIfNeeded() {
- if hidden_changeManually == false { return }
-#if os(iOS) && compiler(>=5)
- guard let keyWindow = UIWindow.key else { return }
- var overrideStyle: UIUserInterfaceStyle = .unspecified
-
- switch DarkModeUserChoice {
- case .auto:
- overrideStyle = .unspecified
-
- case .on:
- overrideStyle = .dark
-
- case .off:
- overrideStyle = .light
- }
-
- keyWindow.overrideUserInterfaceStyle = overrideStyle
-
-#elseif os(macOS)
- switch DarkModeUserChoice {
- case .auto:
- NSApplication.shared.appearance = nil
- case .on:
- NSApplication.shared.appearance =
- NSAppearance(named: AppearanceService.defaultDarkAppearanceOS)
- case .off:
- NSApplication.shared.appearance =
- NSAppearance(named: AppearanceService.defaultLightAppearanceOS)
- }
-#endif
- }
-}
-
-// Local helpers
-
-extension UserDefaults {
- public func valueExists(forKey key: String) -> Bool {
- return object(forKey: key) != nil
- }
-}
-
-#if os(iOS)
-extension UIWindow {
- static var key: UIWindow? {
- if #available(iOS 13, *) {
- return UIApplication.shared.windows.first { $0.isKeyWindow }
- } else {
- return UIApplication.shared.keyWindow
- }
- }
-}
-#endif
-
-// MARK: - Protocols used for unit testing
-
-public protocol NotificationCenterProtocol {
- func addObserver(_ observer: Any,
- selector aSelector: Selector,
- name aName: NSNotification.Name?,
- object anObject: Any?)
- func post(name aName: NSNotification.Name, object anObject: Any?)
-}
-
-public protocol UserDefaultsProtocol {
- func valueExists(forKey key: String) -> Bool
- func integer(forKey defaultName: String) -> Int
- func setValue(_ value: Any?, forKey key: String)
-}
-
-extension UserDefaults: UserDefaultsProtocol { }
-extension NotificationCenter: NotificationCenterProtocol { }
diff --git a/Sources/PerseusDarkMode/AppearanceService/AppearanceStyle.swift b/Sources/PerseusDarkMode/AppearanceService/AppearanceStyle.swift
deleted file mode 100644
index 3d98867..0000000
--- a/Sources/PerseusDarkMode/AppearanceService/AppearanceStyle.swift
+++ /dev/null
@@ -1,50 +0,0 @@
-//
-// AppearanceStyle.swift
-// PerseusDarkMode
-//
-// Created by Mikhail Zhigulin in 7530.
-//
-// Copyright © 7530 - 7531 Mikhail Zhigulin of Novosibirsk.
-//
-// Licensed under the MIT license. See LICENSE file.
-// All rights reserved.
-//
-
-/// Represents appearance style.
-public enum AppearanceStyle: Int, CustomStringConvertible {
-
- case light = 0
- case dark = 1
-
- /// Textual represantation.
- public var description: String {
- switch self {
- case .light:
- return ".light"
- case .dark:
- return ".dark"
- }
- }
-}
-
-/// Represents system appearance style.
-///
-/// Used to bring System Style to early iOS releases.
-public enum SystemStyle: Int, CustomStringConvertible {
-
- case unspecified = 0
- case light = 1
- case dark = 2
-
- /// Textual represantation.
- public var description: String {
- switch self {
- case .unspecified:
- return ".unspecified"
- case .light:
- return ".light"
- case .dark:
- return ".dark"
- }
- }
-}
diff --git a/Sources/PerseusDarkMode/CPLStar.swift b/Sources/PerseusDarkMode/CPLStar.swift
new file mode 100644
index 0000000..2cd1652
--- /dev/null
+++ b/Sources/PerseusDarkMode/CPLStar.swift
@@ -0,0 +1,364 @@
+//
+// CPLStar.swift
+// Version: 1.1.0
+//
+// Standalone ConsolePerseusLogger.
+//
+//
+// For iOS and macOS only. Use Stars to adopt for the specifics you need.
+//
+// DESC: USE LOGGER LIKE A VARIABLE ANYWHERE YOU WANT.
+//
+// Created by Mikhail Zhigulin in 7531.
+//
+// Copyright © 7531 - 7533 Mikhail A. Zhigulin of Novosibirsk
+// Copyright © 7531 - 7533 PerseusRealDeal
+//
+// All rights reserved.
+//
+//
+// MIT License
+//
+// Copyright © 7531 - 7533 Mikhail A. Zhigulin of Novosibirsk
+// Copyright © 7531 - 7533 PerseusRealDeal
+//
+// The year starts from the creation of the world according to a Slavic calendar.
+// September, the 1st of Slavic year.
+//
+// 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.
+//
+// swiftlint:disable file_length
+//
+
+import Foundation
+import os
+
+// swiftlint:disable type_name
+typealias log = PerseusLogger // In SPM package should be not public except TheOne.
+// swiftlint:enable type_name
+
+public typealias ConsoleObject = (subsystem: String, category: String)
+
+public let CONSOLE_APP_SUBSYSTEM_DEFAULT = "Perseus"
+public let CONSOLE_APP_CATEGORY_DEFAULT = "Logger"
+
+public class PerseusLogger {
+
+ // MARK: - Specifics
+
+ public enum Status {
+ case on
+ case off
+ }
+
+ public enum Output {
+ case standard // In Use: Swift.print("").
+ case consoleapp
+ // case outputfile
+ }
+
+ public enum Level: Int, CustomStringConvertible {
+
+ public var description: String {
+ switch self {
+ case .debug:
+ return "DEBUG"
+ case .info:
+ return "INFO"
+ case .notice:
+ return "NOTE"
+ case .error:
+ return "ERROR"
+ case .fault:
+ return "FAULT"
+ }
+ }
+
+ public var tag: String {
+ switch self {
+ case .debug:
+ return "[DEBUG]"
+ case .info:
+ return "[INFO ]"
+ case .notice:
+ return "[NOTE ]"
+ case .error:
+ return "[ERROR]"
+ case .fault:
+ return "[FAULT]"
+ }
+ }
+
+ case debug = 5
+ case info = 4
+ case notice = 3
+ case error = 2
+ case fault = 1
+ }
+
+ public enum TimeMultiply {
+ // case millisecond // -3.
+ // case microsecond // -6.
+ case nanosecond // -9.
+ }
+
+ public enum MessageFormat { // [TYPE] [DATE] [TIME] message, file: #, line: #
+
+ case short
+ // marks true, time false, directives false
+ // [DEBUG] message
+
+ // marks true, time true, directives false
+ // [DEBUG] [2025:04:17] [20:31:53:630594968] message
+
+ // marks true, time false, directives true
+ // [DEBUG] message, file: File.swift, line: 29
+
+ // marks true, time true, directives true
+ // [DEBUG] [2025:04:17] [20:31:53:630918979] message, file: File.swift, line: 29
+
+ // marks false, directives true
+ // message, file: File.swift, line: 29
+
+ // marks false, directives false
+ // message
+
+ case full
+ // [DEBUG] [2025:04:17] [20:31:53:630918979] message, file: File.swift, line: 29
+
+ case textonly
+ // message
+ }
+
+ // MARK: - Properties
+
+#if DEBUG
+ public static var turned = Status.on
+ public static var level = Level.debug
+ public static var output = Output.standard
+#else
+ public static var turned = Status.off
+ public static var level = Level.notice
+ public static var output = Output.consoleapp
+#endif
+
+ public static var subsecond = TimeMultiply.nanosecond
+ public static var format = MessageFormat.short
+
+ public static var marks = true // Controls tags [TYPE] [DATE] [TIME].
+ public static var time = false // If also and marks true adds [DATE] [TIME] to message.
+
+ public static var directives = false // File# and Line# in message.
+
+#if targetEnvironment(simulator)
+ public static var debugIsInfo = true // Shows DEBUG message as INFO in macOS Console.app.
+#endif
+
+ public static var logObject: ConsoleObject? {
+ didSet {
+
+ guard let obj = logObject else {
+
+ if #available(iOS 14.0, macOS 11.0, *) {
+ consoleLogger = nil
+ }
+
+ consoleOSLog = nil
+
+ return
+ }
+
+ if #available(iOS 14.0, macOS 11.0, *) {
+ consoleLogger = Logger(subsystem: obj.subsystem, category: obj.category)
+ } else {
+ consoleOSLog = OSLog(subsystem: obj.subsystem, category: obj.category)
+ }
+ }
+ }
+
+ public static var localTime: String {
+ return getLocalTime()
+ }
+
+ // MARK: - Internals
+
+ @available(iOS 14.0, macOS 11.0, *)
+ private(set) static var consoleLogger: Logger?
+ private(set) static var consoleOSLog: OSLog?
+
+ // MARK: - Contract
+
+ public static func message(_ text: @autoclosure () -> String,
+ _ type: Level = .debug,
+ _ file: StaticString = #file,
+ _ line: UInt = #line) {
+
+ guard turned == .on, type.rawValue <= level.rawValue else { return }
+
+ var message = ""
+
+ // Path.
+
+ let withDirectives = (format == .full) ? true : (directives && (format != .textonly))
+
+ if withDirectives {
+ let fileName = (file.description as NSString).lastPathComponent
+ message = "\(text()), file: \(fileName), line: \(line)"
+ } else {
+ message = "\(text())"
+ }
+
+ // Time.
+
+ let isTimed = (format == .full) ? true : marks && time && (format != .textonly)
+ message = isTimed ? "\(getLocalTime()) \(message)" : message
+
+ // Type.
+
+ let isTyped = (format == .full) ? true : marks && (format != .textonly)
+ message = isTyped ? "\(type.tag) \(message)" : message
+
+ // Print.
+
+ print(message, type)
+ }
+
+ // MARK: - Implementation
+
+ // swiftlint:disable:next cyclomatic_complexity
+ private static func print(_ text: String, _ type: Level) {
+
+ let message = text
+
+ if output == .standard {
+
+ Swift.print(message) // DispatchQueue.main.async { print(message) }
+
+ } else if output == .consoleapp {
+
+ if #available(iOS 14.0, macOS 11.0, *) {
+
+ let logger = consoleLogger ?? Logger(subsystem: CONSOLE_APP_SUBSYSTEM_DEFAULT,
+ category: CONSOLE_APP_CATEGORY_DEFAULT)
+
+ switch type {
+ case .debug:
+#if targetEnvironment(simulator)
+ if debugIsInfo {
+ logger.info("\(message, privacy: .public)")
+ } else {
+ logger.debug("\(message, privacy: .public)")
+ }
+#else
+ logger.debug("\(message, privacy: .public)")
+#endif
+ case .info:
+ logger.info("\(message, privacy: .public)")
+ case .notice:
+ logger.notice("\(message, privacy: .public)")
+ case .error:
+ logger.error("\(message, privacy: .public)")
+ case .fault:
+ logger.fault("\(message, privacy: .public)")
+ }
+
+ return
+ }
+
+ let consoleLog = consoleOSLog ?? OSLog(subsystem: CONSOLE_APP_SUBSYSTEM_DEFAULT,
+ category: CONSOLE_APP_CATEGORY_DEFAULT)
+
+ switch type {
+ case .debug:
+#if targetEnvironment(simulator)
+ if debugIsInfo {
+ os_log("%{public}@", log: consoleLog, type: .info, message)
+ } else {
+ os_log("%{public}@", log: consoleLog, type: .debug, message)
+ }
+#else
+ os_log("%{public}@", log: consoleLog, type: .debug, message)
+#endif
+ case .info:
+ os_log("%{public}@", log: consoleLog, type: .info, message)
+ case .notice:
+ os_log("%{public}@", log: consoleLog, type: .default, message)
+ case .error:
+ os_log("%{public}@", log: consoleLog, type: .error, message)
+ case .fault:
+ os_log("%{public}@", log: consoleLog, type: .fault, message)
+ }
+ }
+ }
+
+ private static func getLocalTime() -> String {
+
+ guard let timezone = TimeZone(secondsFromGMT: 0) else { return "TIME" }
+
+ var calendar = Calendar.current
+
+ calendar.timeZone = timezone
+ calendar.locale = Locale(identifier: "en_US_POSIX")
+
+ let current = Date(timeIntervalSince1970: (Date().timeIntervalSince1970 +
+ Double(TimeZone.current.secondsFromGMT())))
+
+ let details: Set =
+ [
+ .year, .month, .day, .hour, .minute, .second, .nanosecond
+ ]
+
+ let components = calendar.dateComponents(details, from: current)
+
+ // Parse date.
+
+ guard
+ let year = components.year,
+ let month = components.month?.inTime,
+ let day = components.day?.inTime else { return "TIME" }
+
+ let date = "[\(year)-\(month)-\(day)]"
+
+ // Parse time.
+
+ guard
+ let hour = components.hour?.inTime, // Always in 24-hour.
+ let minute = components.minute?.inTime,
+ let second = components.second?.inTime,
+ let subsecond = components.nanosecond?.multiply else { return "TIME" }
+
+ let time = "[\(hour):\(minute):\(second):\(subsecond)]"
+
+ return "\(date) \(time)"
+ }
+}
+
+// MARK: - Helpers
+
+private extension Int {
+
+ var inTime: String {
+ guard self >= 0, self <= 9 else { return String(self) }
+ return "0\(self)"
+ }
+
+ var multiply: String {
+ return String(self)
+ }
+}
diff --git a/Sources/PerseusDarkMode/DarkMode/Constants.swift b/Sources/PerseusDarkMode/DarkMode/Constants.swift
deleted file mode 100644
index 858835e..0000000
--- a/Sources/PerseusDarkMode/DarkMode/Constants.swift
+++ /dev/null
@@ -1,25 +0,0 @@
-//
-// Constants.swift
-// PerseusDarkMode
-//
-// Created by Mikhail Zhigulin in 7530.
-//
-// Copyright © 7530 - 7531 Mikhail Zhigulin of Novosibirsk.
-//
-// Licensed under the MIT license. See LICENSE file.
-// All rights reserved.
-//
-
-import Foundation
-
-/// UserDefaults Key for AppearanceService.DarkModeUserChoice variable.
-public let DARK_MODE_USER_CHOICE_KEY = "DarkModeUserChoiceOptionKey"
-
-/// Default value for AppearanceService.DarkModeUserChoice variable.
-public let DARK_MODE_USER_CHOICE_DEFAULT = DarkModeOption.auto
-
-/// Default valur for AppearanceService.shared.Style.
-public let DARK_MODE_STYLE_DEFAULT = AppearanceStyle.light
-
-/// Name of DarkMode.StyleObservable variable used in KVO
-public let OBSERVERED_VARIABLE_NAME = "styleObservable"
diff --git a/Sources/PerseusDarkMode/DarkMode/DarkMode.swift b/Sources/PerseusDarkMode/DarkMode/DarkMode.swift
deleted file mode 100644
index 8df1656..0000000
--- a/Sources/PerseusDarkMode/DarkMode/DarkMode.swift
+++ /dev/null
@@ -1,82 +0,0 @@
-//
-// DarkMode.swift
-// PerseusDarkMode
-//
-// Created by Mikhail Zhigulin in 7530.
-//
-// Copyright © 7530 - 7531 Mikhail Zhigulin of Novosibirsk.
-//
-// Licensed under the MIT license. See LICENSE file.
-// All rights reserved.
-//
-
-#if canImport(UIKit)
-import UIKit
-#elseif canImport(Cocoa)
-import Cocoa
-#endif
-
-/// Represents Dark Mode and contains the app's appearance style.
-///
-/// - KVO technique can be used to be notified on the app's appearance style changed event.
-/// - Use StyleObservable varible to create an observer.
-public class DarkMode: NSObject {
-
- // MARK: - The App's current Appearance Style
-
- /// The app's current appearance style.
- public var style: AppearanceStyle { return hidden_style }
-
- // MARK: - Observable Appearance Style Value (Using Key-Value Observing)
-
- /// Shows the same value of Style but observable.
- ///
- /// Takes place because swift doesn't support observing enums.
- @objc public dynamic var styleObservable: Int = DARK_MODE_STYLE_DEFAULT.rawValue
-
- // MARK: - System's Appearance Style
-
- /// The app's current system appearance style.
- public var systemStyle: SystemStyle {
- if #available(iOS 13.0, macOS 10.14, *) {
-#if os(iOS)
- guard let keyWindow = UIWindow.key else { return .unspecified }
-
- switch keyWindow.traitCollection.userInterfaceStyle {
- case .unspecified:
- return .unspecified
- case .light:
- return .light
- case .dark:
- return .dark
-
- @unknown default:
- return .unspecified
- }
-#elseif os(macOS)
- if let isDark = UserDefaults.standard.string(forKey: "AppleInterfaceStyle"),
- isDark == "Dark" {
- return .dark
- } else {
- return .light
- }
-#endif
- } else {
- return .unspecified
- }
- }
-
- internal var hidden_style: AppearanceStyle = DARK_MODE_STYLE_DEFAULT {
- didSet { styleObservable = style.rawValue }
- }
-}
-
-// MARK: - Protocols used for unit testing
-
-public protocol DarkModeProtocol {
- var style: AppearanceStyle { get }
- var systemStyle: SystemStyle { get }
- var styleObservable: Int { get }
-}
-
-extension DarkMode: DarkModeProtocol { }
diff --git a/Sources/PerseusDarkMode/DarkMode/DarkModeDecision.swift b/Sources/PerseusDarkMode/DarkMode/DarkModeDecision.swift
deleted file mode 100644
index b37ceb4..0000000
--- a/Sources/PerseusDarkMode/DarkMode/DarkModeDecision.swift
+++ /dev/null
@@ -1,54 +0,0 @@
-//
-// DarkModeDecision.swift
-// PerseusDarkMode
-//
-// Created by Mikhail Zhigulin in 7530.
-//
-// Copyright © 7530 - 7531 Mikhail Zhigulin of Novosibirsk.
-//
-// Licensed under the MIT license. See LICENSE file.
-// All rights reserved.
-//
-
-/// Makes a calculation of the app's appearance style.
-public class DarkModeDecision {
-
- private init() { }
-
- // MARK: - Calculating Dark Mode decision
-
- /// Calculates the current appearance style of the app.
- ///
- /// Dark Mode decision-making:
- ///
- /// | DarkModeOption
- /// -------------+-----------------------
- /// SystemStyle | auto | on | off
- /// -------------+---------+------+------
- /// .unspecified | default | dark | light
- /// .light | light | dark | light
- /// .dark | dark | dark | light
- ///
- public class func calculate(_ userChoice: DarkModeOption,
- _ systemStyle: SystemStyle) -> AppearanceStyle {
- // Calculate outputs
-
- if (systemStyle == .unspecified) && (userChoice == .auto) {
- return DARK_MODE_STYLE_DEFAULT
- }
- if (systemStyle == .unspecified) && (userChoice == .on) { return .dark }
- if (systemStyle == .unspecified) && (userChoice == .off) { return .light }
-
- if (systemStyle == .light) && (userChoice == .auto) { return .light }
- if (systemStyle == .light) && (userChoice == .on) { return .dark }
- if (systemStyle == .light) && (userChoice == .off) { return .light }
-
- if (systemStyle == .dark) && (userChoice == .auto) { return .dark }
- if (systemStyle == .dark) && (userChoice == .on) { return .dark }
- if (systemStyle == .dark) && (userChoice == .off) { return .light }
-
- // Output default value if somethings goes out of the decision table
-
- return DARK_MODE_STYLE_DEFAULT
- }
-}
diff --git a/Sources/PerseusDarkMode/DarkMode/DarkModeObserver.swift b/Sources/PerseusDarkMode/DarkMode/DarkModeObserver.swift
deleted file mode 100644
index d26c7ff..0000000
--- a/Sources/PerseusDarkMode/DarkMode/DarkModeObserver.swift
+++ /dev/null
@@ -1,80 +0,0 @@
-//
-// DarkModeObserver.swift
-// PerseusDarkMode
-//
-// Created by Mikhail Zhigulin in 7530.
-//
-// Copyright © 7530 - 7531 Mikhail Zhigulin of Novosibirsk.
-//
-// Licensed under the MIT license. See LICENSE file.
-// All rights reserved.
-//
-// swiftlint:disable block_based_kvo
-//
-
-#if canImport(UIKit)
-import UIKit
-#elseif canImport(Cocoa)
-import Cocoa
-#endif
-
-/// Responsible for making run the code if the app's appearance style changed.
-///
-/// It connects to shared Dark Mode instance using appearance service.
-/// Also, it takes action every time when the app's appearance style changed.
-///
-/// The action can be specified with initialization as the passed closue
-/// and after initialization by assigning the action property as well.
-public class DarkModeObserver: NSObject {
- /// Closure to perform if the app's appearance style changed.
- public var action: ((_ newStyle: AppearanceStyle) -> Void)?
-
- /// The reference of the object to be obsevered.
- private(set) var objectToObserve = AppearanceService.shared
-
- /// Initializer by default.
- ///
- /// It creates the instance with no action specified if the app's appearance style changed.
- ///
- /// Then, give it a closure to run the code if the app's appearance style changed.
- public override init() {
- super.init()
-
- objectToObserve.addObserver(self,
- forKeyPath: OBSERVERED_VARIABLE_NAME,
- options: .new,
- context: nil)
- }
-
- /// Initializer with parameters.
- ///
- /// Pass a closure to specify the action to be taken if the app's appearance style changed.
- public init(_ action: @escaping ((_ newStyle: AppearanceStyle) -> Void)) {
- super.init()
-
- self.action = action
- objectToObserve.addObserver(self,
- forKeyPath: OBSERVERED_VARIABLE_NAME,
- options: .new,
- context: nil)
- }
-
- /// Takes action every time when Style changes happens.
- public override func observeValue(forKeyPath keyPath: String?,
- of object: Any?,
- change: [NSKeyValueChangeKey: Any]?,
- context: UnsafeMutableRawPointer?) {
- guard
- keyPath == OBSERVERED_VARIABLE_NAME,
- let style = change?[.newKey],
- let styleRawValue = style as? Int,
- let newStyle = AppearanceStyle.init(rawValue: styleRawValue)
- else { return }
-
- action?(newStyle)
- }
-
- deinit {
- objectToObserve.removeObserver(self, forKeyPath: OBSERVERED_VARIABLE_NAME)
- }
-}
diff --git a/Sources/PerseusDarkMode/DarkMode/DarkModeOption.swift b/Sources/PerseusDarkMode/DarkMode/DarkModeOption.swift
deleted file mode 100644
index 670c7f9..0000000
--- a/Sources/PerseusDarkMode/DarkMode/DarkModeOption.swift
+++ /dev/null
@@ -1,37 +0,0 @@
-//
-// DarkModeOption.swift
-// PerseusDarkMode
-//
-// Created by Mikhail Zhigulin in 7530.
-//
-// Copyright © 7530 - 7531 Mikhail Zhigulin of Novosibirsk.
-//
-// Licensed under the MIT license. See LICENSE file.
-// All rights reserved.
-//
-
-import Foundation
-
-/// Represents a list of possible values for Dark Mode option.
-///
-/// - AUTO means the choice is made by System.
-/// - ON means the app's appearance style should be Dark at any case.
-/// - OFF means the app's appearance style should be Light at any case.
-public enum DarkModeOption: Int, CustomStringConvertible {
-
- case auto = 0
- case on = 1
- case off = 2
-
- /// Textual representation of the current value of the option.
- public var description: String {
- switch self {
- case .auto:
- return ".auto"
- case .on:
- return ".on"
- case .off:
- return ".off"
- }
- }
-}
diff --git a/Sources/PerseusDarkMode/PerseusLogger.swift b/Sources/PerseusDarkMode/PerseusLogger.swift
deleted file mode 100644
index 70c1f6a..0000000
--- a/Sources/PerseusDarkMode/PerseusLogger.swift
+++ /dev/null
@@ -1,113 +0,0 @@
-//
-// PerseusLogger.swift
-// PerseusRealDeal
-//
-// Created by Mikhail Zhigulin in 7531.
-//
-// Copyright © 7531 Mikhail Zhigulin of Novosibirsk.
-// Copyright © 7531 PerseusRealDeal.
-// All rights reserved.
-//
-//
-// MIT License
-//
-// Copyright © 7531 Mikhail Zhigulin of Novosibirsk
-// Copyright © 7531 PerseusRealDeal
-//
-// The year starts from the creation of the world according to a Slavic calendar.
-// September, the 1st of Slavic year.
-//
-// 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 notices 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.
-//
-
-// DESC: USE LOGGER LIKE A VARIABLE ANYWHERE YOU WANT
-//
-// By default logger is turned on in DEBUG, but it's off in RELEASE.
-//
-// log.message("[\(type(of: self))].\(#function)")
-//
-
-/* To disable debug messaging of the module use the following statements in the start point.
-
- import class OpenWeatherFreeClient.PerseusLogger
- typealias FreeClientLogger = OpenWeatherFreeClient.PerseusLogger
-
- FreeClientLogger.turned = .off
-
- */
-
-import Foundation
-
-// swiftlint:disable type_name
-typealias log = PerseusLogger
-// swiftlint:enable type_name
-
-public class PerseusLogger {
-
- public enum Status {
- case on
- case off
- }
-
- public enum Level: Int, CustomStringConvertible {
-
- public var description: String {
- switch self {
- case .info:
- return "INFO"
- case .debug:
- return "DEBUG"
- case .error:
- return "ERROR"
- }
- }
-
- case info = 3
- case debug = 2 // Default.
- case error = 1
- }
-
- #if DEBUG
- public static var turned = Status.on
- #else
- public static var turned = Status.off
- #endif
-
- public static var level = Level.debug
- public static var short = true
-
- public static func message(_ text: @autoclosure () -> String,
- _ type: Level = .debug,
- _ file: StaticString = #file,
- _ line: UInt = #line) {
-
- guard turned == .on, type.rawValue <= level.rawValue else { return }
-
- var message = ""
-
- if short {
- message = "\(type): \(text())"
- } else {
- let fileName = (file.description as NSString).lastPathComponent
- message = "\(type): \(text()), file: \(fileName), line: \(line)"
- }
-
- print(message) // DispatchQueue.main.async { print(message) }
- }
-}
diff --git a/Sources/PerseusDarkMode/TheDarkness.swift b/Sources/PerseusDarkMode/TheDarkness.swift
new file mode 100644
index 0000000..ff035d1
--- /dev/null
+++ b/Sources/PerseusDarkMode/TheDarkness.swift
@@ -0,0 +1,455 @@
+//
+// TheDarkness.swift
+// PerseusDarkMode
+//
+//
+// For iOS and macOS only. Use Stars to adopt for the specifics you need.
+//
+// DESC: THE DARKNESS YOU CAN FORCE.
+//
+// Created by Mikhail Zhigulin in 7530.
+//
+// Copyright © 7530 - 7533 Mikhail Zhigulin of Novosibirsk
+// Copyright © 7533 PerseusRealDeal
+//
+// All rights reserved.
+//
+//
+// MIT License
+//
+// Copyright © 7530 - 7533 Mikhail A. Zhigulin of Novosibirsk
+// Copyright © 7533 PerseusRealDeal
+//
+// The year starts from the creation of the world according to a Slavic calendar.
+// September, the 1st of Slavic year.
+//
+// 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.
+//
+// swiftlint:disable file_length
+//
+
+#if canImport(UIKit)
+import UIKit
+#elseif canImport(Cocoa)
+import Cocoa
+#endif
+
+public let APPEARANCE_DEFAULT = AppearanceStyle.light
+
+public let DARK_MODE_USER_CHOICE_KEY = "DarkModeUserChoiceOptionKey"
+public let DARK_MODE_USER_CHOICE_DEFAULT = DarkModeOption.auto
+
+#if os(iOS)
+public let DARK_MODE_SETTINGS_KEY = "DarkModeSettingsKey"
+#endif
+
+#if os(macOS)
+public var DARK_APPEARANCE_DEFAULT_IN_USE: NSAppearance {
+ guard let darkAppearanceOS = DARK_APPEARANCE_DEFAULT else {
+ if #available(macOS 10.14, *) {
+ return NSAppearance(named: .darkAqua)!
+ } // For HighSierra.
+ return NSAppearance(named: .vibrantDark)!
+ }
+ return darkAppearanceOS
+}
+public var DARK_APPEARANCE_DEFAULT: NSAppearance?
+public var LIGHT_APPEARANCE_DEFAULT_IN_USE = NSAppearance(named: .aqua)!
+#endif
+
+public extension Notification.Name {
+ static let MakeAppearanceUpNotification =
+ Notification.Name("MakeAppearanceUpNotification")
+#if os(macOS)
+ static let AppleInterfaceThemeChangedNotification =
+ Notification.Name("AppleInterfaceThemeChangedNotification")
+#endif
+}
+
+// swiftlint:disable identifier_name
+public extension NSObject {
+ var DarkMode: DarkMode { return DarkModeAgent.shared }
+ var DarkModeUserChoice: DarkModeOption { return DarkModeAgent.DarkModeUserChoice }
+}
+// swiftlint:enable identifier_name
+
+public enum AppearanceStyle: Int, CustomStringConvertible {
+
+ case light = 0
+ case dark = 1
+
+ public var description: String {
+ switch self {
+ case .light:
+ return ".light"
+ case .dark:
+ return ".dark"
+ }
+ }
+}
+
+public enum DarkModeOption: Int, CustomStringConvertible {
+
+ case auto = 0
+ case on = 1
+ case off = 2
+
+ public var description: String {
+ switch self {
+ case .auto:
+ return ".auto"
+ case .on:
+ return ".on"
+ case .off:
+ return ".off"
+ }
+ }
+}
+
+public class DarkMode: NSObject {
+
+ public var style: AppearanceStyle { return appearance }
+
+ @objc public dynamic var styleObservable: Int = APPEARANCE_DEFAULT.rawValue
+
+ internal var appearance: AppearanceStyle = APPEARANCE_DEFAULT {
+ didSet { styleObservable = style.rawValue }
+ }
+}
+
+public class DarkModeAgent {
+
+ // MARK: - Properties
+
+ public static var shared: DarkMode = { _ = instance; return DarkMode() }()
+ public static var DarkModeUserChoice: DarkModeOption {
+ return userChoice
+ }
+
+ // MARK: - Internals
+
+ private static var userChoice: DarkModeOption {
+ get {
+ let rawValue = ud.valueExists(forKey: DARK_MODE_USER_CHOICE_KEY) ?
+ ud.integer(forKey: DARK_MODE_USER_CHOICE_KEY) :
+ DARK_MODE_USER_CHOICE_DEFAULT.rawValue
+
+ if let result = DarkModeOption.init(rawValue: rawValue) { return result }
+ return DARK_MODE_USER_CHOICE_DEFAULT
+ }
+ set {
+ ud.setValue(newValue.rawValue, forKey: DARK_MODE_USER_CHOICE_KEY)
+#if os(iOS)
+ // Set DarkMode option for Settings bundle
+ ud.setValue(newValue.rawValue, forKey: DARK_MODE_SETTINGS_KEY)
+#endif
+ }
+ }
+
+#if os(macOS)
+ private static var distributedNCenter = DistributedNotificationCenter.default
+#endif
+ private static var nCenter = NotificationCenter.default
+ private static var ud = UserDefaults.standard
+
+ private var observation: NSKeyValueObservation?
+
+ // MARK: - Singletone
+
+ private static var instance = { DarkModeAgent() }()
+ private init() {
+ log.message("[\(type(of: self))].\(#function)", .info)
+#if os(macOS)
+ if #available(macOS 10.14, *) {
+ DarkModeAgent.distributedNCenter.addObserver(
+ self,
+ selector: #selector(processAppleInterfaceThemeChanged),
+ name: .AppleInterfaceThemeChangedNotification,
+ object: nil
+ )
+ observation = NSApp.observe(\.effectiveAppearance) { _, _ in
+ if DarkModeAgent.userChoice == .auto {
+
+ let effectiveAppearance = NSApplication.shared.effectiveAppearance
+ let wanted = self.getRequired(from: effectiveAppearance)
+
+ DarkModeAgent.shared.appearance = wanted
+ self.notifyAllRegistered()
+ }
+ }
+ } // For HighSierra there is no need in observation cos' system style never change.
+#endif
+ }
+
+ // MARK: - Contract
+
+ public static func register(stakeholder: Any, selector: Selector) {
+ self.nCenter.addObserver(stakeholder,
+ selector: selector,
+ name: .MakeAppearanceUpNotification,
+ object: nil)
+ }
+
+ public static func force(_ userChoice: DarkModeOption) {
+ log.message("[\(type(of: self))].\(#function) \(userChoice)", .info)
+
+ DarkModeAgent.userChoice = userChoice
+ DarkModeAgent.instance.refresh()
+
+#if os(iOS)
+ DarkModeAgent.instance.notifyAllRegistered()
+#elseif os(macOS)
+ if #unavailable(macOS 10.14) { // For HighSierra.
+ DarkModeAgent.instance.notifyAllRegistered()
+ } else if DarkModeAgent.userChoice != .auto { // If .auto observation notifies change.
+ DarkModeAgent.instance.notifyAllRegistered()
+ }
+#endif
+ }
+
+ public static func makeUp() {
+ DarkModeAgent.instance.notifyAllRegistered()
+ }
+
+ // MARK: - iOS Contract specifics
+
+#if os(iOS)
+ @available(iOS 13.0, *)
+ public static func processTraitCollectionDidChange(_ previous: UITraitCollection?) {
+ if let previous = previous?.userInterfaceStyle {
+ if UIWindow.systemStyle.rawValue != previous.rawValue {
+ DarkModeAgent.instance.processAppleInterfaceThemeChanged()
+ }
+ }
+ }
+
+ // Returns nil if DarkMode equal to User choice or no value saved otherwise new value.
+ public static func isDarkModeSettingsKeyChanged() -> DarkModeOption? {
+ let option = ud.valueExists(forKey: DARK_MODE_SETTINGS_KEY) ?
+ ud.integer(forKey: DARK_MODE_SETTINGS_KEY) : -1
+
+ guard option != -1, let settingsDarkMode = DarkModeOption.init(rawValue: option)
+ else { return nil }
+
+ return settingsDarkMode != userChoice ? settingsDarkMode : nil
+ }
+#endif
+
+ // MARK: - Implementation
+
+ @objc private func processAppleInterfaceThemeChanged() {
+ refresh()
+ notifyAllRegistered()
+ }
+
+ private func refresh() {
+ let choice = DarkModeAgent.userChoice
+#if os(iOS)
+ let current = UIWindow.systemStyle
+ let required = makeDecision(current, choice)
+
+ if #available(iOS 13.0, *) {
+ UIWindow.refreshKeyWindow()
+ }
+
+ DarkModeAgent.shared.appearance = required
+#elseif os(macOS)
+ if #available(macOS 10.14, *) {
+ switch choice {
+ case .auto:
+ NSApp.appearance = nil
+ // DarkModeAgent is informed about new appearance by observation.
+ case .on:
+ NSApp.appearance = DARK_APPEARANCE_DEFAULT_IN_USE
+ DarkModeAgent.shared.appearance = .dark
+ case .off:
+ NSApp.appearance = LIGHT_APPEARANCE_DEFAULT_IN_USE
+ DarkModeAgent.shared.appearance = .light
+ }
+ } else { // For HighSierra.
+ DarkModeAgent.shared.appearance = choice == .on ? .dark : .light
+ }
+#endif
+ }
+
+ private func notifyAllRegistered() {
+ DarkModeAgent.nCenter.post(name: .MakeAppearanceUpNotification, object: nil)
+ }
+
+#if os(macOS)
+ private func getRequired(from appearance: NSAppearance?) -> AppearanceStyle {
+ let choice = DarkModeAgent.userChoice
+
+ if #available(macOS 10.14, *) {
+ guard choice == .auto else {
+ return choice == .off ? .light : .dark
+ }
+
+ if let match = appearance?.bestMatch(from: [.darkAqua, .vibrantDark]) {
+ return [.darkAqua, .vibrantDark].contains(match) ? .dark : .light
+ }
+
+ return .light
+ } else { // For HighSierra.
+ return choice == .off ? .dark : .light
+ }
+ }
+#endif
+}
+
+// MARK: - Helpers
+
+extension UserDefaults {
+ public func valueExists(forKey key: String) -> Bool {
+ return object(forKey: key) != nil
+ }
+}
+
+public class DarkModeObserver: NSObject {
+
+ public var action: ((_ newStyle: AppearanceStyle) -> Void)?
+
+ private var objectToObserve = DarkModeAgent.shared
+ private var observation: NSKeyValueObservation?
+
+ public override init() {
+ super.init()
+ setupObservation()
+ }
+
+ public init(_ action: @escaping ((_ newStyle: AppearanceStyle) -> Void)) {
+ super.init()
+
+ self.action = action
+ setupObservation()
+ }
+
+ private func setupObservation() {
+ observation = objectToObserve.observe(\.styleObservable) { _, _ in
+ self.action?(self.objectToObserve.style)
+ }
+ }
+}
+
+#if os(iOS)
+public enum SystemStyle: Int, CustomStringConvertible {
+
+ case unspecified = 0
+ case light = 1
+ case dark = 2
+
+ public var description: String {
+ switch self {
+ case .unspecified:
+ return ".unspecified"
+ case .light:
+ return ".light"
+ case .dark:
+ return ".dark"
+ }
+ }
+}
+
+extension UIWindow {
+
+ static var key: UIWindow? {
+ if #available(iOS 13, *) {
+ return UIApplication.shared.windows.first { $0.isKeyWindow }
+ } else {
+ return UIApplication.shared.keyWindow
+ }
+ }
+
+ static var systemStyle: SystemStyle {
+ if #available(iOS 13.0, *) {
+ guard let keyWindow = UIWindow.key else { return .unspecified }
+
+ switch keyWindow.traitCollection.userInterfaceStyle {
+ case .unspecified:
+ return .unspecified
+ case .light:
+ return .light
+ case .dark:
+ return .dark
+
+ @unknown default:
+ return .unspecified
+ }
+ } else {
+ return .unspecified // For iOS 12.0 and earlier.
+ }
+ }
+}
+
+// MARK: - Calculating Dark Mode Required
+
+/// Calculates the current required appearance style of the app.
+///
+/// Dark Mode decision-making:
+///
+/// | User
+/// -------------+-----------------------
+/// System | auto | on | off
+/// -------------+---------+------+------
+/// .unspecified | default | dark | light
+/// .light | light | dark | light
+/// .dark | dark | dark | light
+///
+public func makeDecision(_ system: SystemStyle, _ user: DarkModeOption) -> AppearanceStyle {
+
+ if (system == .unspecified) && (user == .auto) { return APPEARANCE_DEFAULT }
+ if (system == .unspecified) && (user == .on) { return .dark }
+ if (system == .unspecified) && (user == .off) { return .light }
+
+ if (system == .light) && (user == .auto) { return .light }
+ if (system == .light) && (user == .on) { return .dark }
+ if (system == .light) && (user == .off) { return .light }
+
+ if (system == .dark) && (user == .auto) { return .dark }
+ if (system == .dark) && (user == .on) { return .dark }
+ if (system == .dark) && (user == .off) { return .light }
+
+ return APPEARANCE_DEFAULT
+
+}
+#endif
+
+#if os(iOS) && compiler(>=5)
+extension UIWindow {
+
+ @available(iOS 13.0, *)
+ public static func refreshKeyWindow() {
+
+ guard let keyWindow = UIWindow.key else { return }
+
+ var overrideStyle: UIUserInterfaceStyle = .unspecified
+
+ switch DarkModeAgent.DarkModeUserChoice {
+ case .auto:
+ overrideStyle = .unspecified
+ case .on:
+ overrideStyle = .dark
+ case .off:
+ overrideStyle = .light
+ }
+
+ keyWindow.overrideUserInterfaceStyle = overrideStyle
+ }
+}
+#endif
diff --git a/SucceedsPostAction.sh b/SucceedsPostAction.sh
index 3a15ccd..8a0bc58 100755
--- a/SucceedsPostAction.sh
+++ b/SucceedsPostAction.sh
@@ -3,13 +3,13 @@
# SucceedsPostAction.sh
# PerseusDarkMode
#
-# Copied and edited by Mikhail Zhigulin in 2022.
-#
+# Copied and edited by Mikhail A. Zhigulin in 2022.
+# CHANGED: Post action output file name.
# The MIT License (MIT)
# Copyright (c) 2021 Alexandre Colucci, geteimy.com
-# Copyright (c) 2022 Mikhail Zhigulin of Novosibirsk
+# Copyright (c) 2022 Mikhail A. Zhigulin of Novosibirsk
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
@@ -43,7 +43,7 @@
#
# Usage
#
-# 1. Ensure that the script has the proper permissions, i.e.
+# 1. Ensure that the script has the proper permissions, i.e.
# run chmod 755 SucceedsPostAction.sh
# 2. Launch Xcode 13.0 or later
# 3. Open Preferences > Locations and ensure that Command Line Tools
@@ -99,7 +99,7 @@ fi
# Run swiftlint
#-------------------------------------------------------------------------------
OUTPUT="${SWIFTLINT_PATH} ${PACKAGE_ROOT_FOLDER}"
-OUTPUT_FILE_PATH="${PACKAGE_ROOT_FOLDER}/swiftlint.txt"
+OUTPUT_FILE_PATH="${PACKAGE_ROOT_FOLDER}/SwiftLintOutput"
${OUTPUT} > ${OUTPUT_FILE_PATH}
#-------------------------------------------------------------------------------
diff --git a/Tests/DarkModeTests/DarkModeTests/AppearanceServiceTests.swift b/Tests/DarkModeTests/DarkModeTests/AppearanceServiceTests.swift
deleted file mode 100644
index 0daad71..0000000
--- a/Tests/DarkModeTests/DarkModeTests/AppearanceServiceTests.swift
+++ /dev/null
@@ -1,116 +0,0 @@
-//
-// AppearanceServiceTests.swift
-// DarkModeTests
-//
-// Created by Mikhail Zhigulin in 7530.
-//
-// Copyright © 7530 - 7531 Mikhail Zhigulin of Novosibirsk.
-//
-// Licensed under the MIT license. See LICENSE file.
-// All rights reserved.
-//
-
-#if canImport(UIKit)
-import UIKit
-#elseif canImport(Cocoa)
-import Cocoa
-#endif
-
-#if os(iOS)
-public typealias View = UIView
-public typealias ViewController = UIViewController
-#elseif os(macOS)
-public typealias View = NSView
-public typealias ViewController = NSViewController
-#endif
-
-import XCTest
-@testable import PerseusDarkMode
-
-final class AppearanceServiceTests: XCTestCase {
-
- func test_init() {
- XCTAssertFalse(AppearanceService.isEnabled)
- XCTAssertFalse(AppearanceService.hidden_isEnabled)
-
- XCTAssertNotNil(AppearanceService.ud)
- XCTAssertNotNil(AppearanceService.nCenter)
-
- let viewDarkMode = View().DarkMode as AnyObject
- let videwControllerDarkMode = ViewController().DarkMode as AnyObject
- let sharedDarkMode = AppearanceService.shared as AnyObject
-
-#if os(iOS)
- XCTAssertEqual(ObjectIdentifier(viewDarkMode), ObjectIdentifier(sharedDarkMode))
- XCTAssertEqual(ObjectIdentifier(videwControllerDarkMode),
- ObjectIdentifier(sharedDarkMode))
-#elseif os(macOS)
- XCTAssertEqual(viewDarkMode.objectID, sharedDarkMode.objectID)
- XCTAssertEqual(videwControllerDarkMode.objectID, sharedDarkMode.objectID)
-#endif
- }
-
- func test_method_register_called_addObserver() {
- // arrange
-
- let mock = MockNotificationCenter()
- AppearanceService.nCenter = mock
-
- // swiftlint:disable nesting
-
- class MyView: View { @objc func makeUp() { } }
-
- // swiftlint:enable nesting
-
- let view = MyView()
-
- // act
-
- AppearanceService.register(stakeholder: view, selector: #selector(view.makeUp))
-
- // assert
-
- mock.verifyRegisterObserver(observer: view, selector: #selector(view.makeUp))
- }
-
- func test_method_makeAppearanceUp_called_post_and_isEnabled_true() {
- // arrange
-
- let mock = MockNotificationCenter()
- AppearanceService.nCenter = mock
-
- // act
-
- AppearanceService.makeUp()
-
- // assert
-
- mock.verifyPost(name: .MakeAppearanceUpNotification)
- XCTAssertTrue(AppearanceService.isEnabled)
- }
-
-#if os(macOS)
- func test_Dark_Mode_called_addObserver_once() {
- // arrange
-
- let mock = MockNotificationCenter()
- AppearanceService.distributedNCenter = mock
-
- let view1 = View()
- let view2 = View()
-
- // act
-
- // _ = AppearanceService.shared
- // _ = AppearanceService.shared
-
- _ = view1.DarkMode
- _ = view2.DarkMode
-
- // assert
-
- mock.verifyRegisterObserver(observer: AppearanceService.it,
- selector: #selector(AppearanceService.modeChanged))
- }
-#endif
-}
diff --git a/Tests/DarkModeTests/DarkModeTests/DarkModeDecisionTests.swift b/Tests/DarkModeTests/DarkModeTests/DarkModeDecisionTests.swift
deleted file mode 100644
index 03a2ae4..0000000
--- a/Tests/DarkModeTests/DarkModeTests/DarkModeDecisionTests.swift
+++ /dev/null
@@ -1,168 +0,0 @@
-//
-// DarkModeDecisionTests.swift
-// DarkModeTests
-//
-// Created by Mikhail Zhigulin in 7530.
-//
-// Copyright © 7530 - 7531 Mikhail Zhigulin of Novosibirsk.
-//
-// Licensed under the MIT license. See LICENSE file.
-// All rights reserved.
-//
-
-#if canImport(UIKit)
-import UIKit
-#elseif canImport(Cocoa)
-import Cocoa
-#endif
-
-import XCTest
-@testable import PerseusDarkMode
-
-final class DarkModeDecisionTests: XCTestCase {
-
- ///
- /// Decision table for Actual Style:
- ///
- /// | DarkModeOption
- /// | auto | on | off
- /// -------------+--------------+---------+------+------
- /// System style | .unspecified | default | dark | light
- /// System style | .light | light | dark | light
- /// System style | .dark | dark | dark | light
- ///
- func test_calculateActualStyle_with_auto_and_unspecified_should_return_default() {
- // arrange
-
- let userChoice = DarkModeOption.auto
- let systemStyle = SystemStyle.unspecified
-
- // act
-
- let result = DarkModeDecision.calculate(userChoice, systemStyle)
-
- // assert
-
- XCTAssertEqual(result, DARK_MODE_STYLE_DEFAULT)
- }
-
- func test_calculateActualStyle_with_on_and_unspecified_should_return_dark() {
- // arrange
-
- let userChoice = DarkModeOption.on
- let systemStyle = SystemStyle.unspecified
-
- // act
-
- let result = DarkModeDecision.calculate(userChoice, systemStyle)
-
- // assert
-
- XCTAssertEqual(result, .dark)
- }
-
- func test_calculateActualStyle_with_off_and_unspecified_should_return_light() {
- // arrange
-
- let userChoice = DarkModeOption.off
- let systemStyle = SystemStyle.unspecified
-
- // act
-
- let result = DarkModeDecision.calculate(userChoice, systemStyle)
-
- // assert
-
- XCTAssertEqual(result, .light)
- }
-
- func test_calculateActualStyle_with_auto_and_light_should_return_light() {
- // arrange
-
- let userChoice = DarkModeOption.auto
- let systemStyle = SystemStyle.light
-
- // act
-
- let result = DarkModeDecision.calculate(userChoice, systemStyle)
-
- // assert
-
- XCTAssertEqual(result, .light)
- }
-
- func test_calculateActualStyle_with_on_and_light_should_return_dark() {
- // arrange
-
- let userChoice = DarkModeOption.on
- let systemStyle = SystemStyle.light
-
- // act
-
- let result = DarkModeDecision.calculate(userChoice, systemStyle)
-
- // assert
-
- XCTAssertEqual(result, .dark)
- }
-
- func test_calculateActualStyle_with_off_and_light_should_return_light() {
- // arrange
-
- let userChoice = DarkModeOption.off
- let systemStyle = SystemStyle.light
-
- // act
-
- let result = DarkModeDecision.calculate(userChoice, systemStyle)
-
- // assert
-
- XCTAssertEqual(result, .light)
- }
-
- func test_calculateActualStyle_with_auto_and_dark_should_return_dark() {
- // arrange
-
- let userChoice = DarkModeOption.auto
- let systemStyle = SystemStyle.dark
-
- // act
-
- let result = DarkModeDecision.calculate(userChoice, systemStyle)
-
- // assert
-
- XCTAssertEqual(result, .dark)
- }
-
- func test_calculateActualStyle_with_on_and_dark_should_return_dark() {
- // arrange
-
- let userChoice = DarkModeOption.on
- let systemStyle = SystemStyle.dark
-
- // act
-
- let result = DarkModeDecision.calculate(userChoice, systemStyle)
-
- // assert
-
- XCTAssertEqual(result, .dark)
- }
-
- func test_calculateActualStyle_with_off_and_dark_should_return_light() {
- // arrange
-
- let userChoice = DarkModeOption.off
- let systemStyle = SystemStyle.dark
-
- // act
-
- let result = DarkModeDecision.calculate(userChoice, systemStyle)
-
- // assert
-
- XCTAssertEqual(result, .light)
- }
-}
diff --git a/Tests/DarkModeTests/DarkModeTests/DarkModeTests.swift b/Tests/DarkModeTests/DarkModeTests/DarkModeTests.swift
deleted file mode 100644
index 0aba4b9..0000000
--- a/Tests/DarkModeTests/DarkModeTests/DarkModeTests.swift
+++ /dev/null
@@ -1,143 +0,0 @@
-//
-// DarkModeTests.swift
-// DarkModeTests
-//
-// Created by Mikhail Zhigulin in 7530.
-//
-// Copyright © 7530 - 7531 Mikhail Zhigulin of Novosibirsk.
-//
-// Licensed under the MIT license. See LICENSE file.
-// All rights reserved.
-//
-
-#if canImport(UIKit)
-import UIKit
-#elseif canImport(Cocoa)
-import Cocoa
-#endif
-
-import XCTest
-@testable import PerseusDarkMode
-
-final class DarkModeTests: XCTestCase {
-
- func test_DarkMode_observable() {
- // arrange
-
- var count: Int = 0
- var collection: [AppearanceStyle] = []
-
- var observer: DarkModeObserver? = DarkModeObserver()
- observer?.action = { newStyle in
-
- collection.append(newStyle)
- count += 1
- }
-
- // act
-
- AppearanceService.shared.hidden_style = AppearanceStyle.dark
- AppearanceService.shared.hidden_style = AppearanceStyle.light
-
- // assert
-
- XCTAssertEqual(count, 2)
- XCTAssertEqual(collection, [AppearanceStyle.dark, AppearanceStyle.light])
-
- // keep the room clean
-
- observer = nil
- }
-
- func test_DarkMode_not_observable() {
- // arrange
-
- var count: Int = 0
- var collection: [AppearanceStyle] = []
-
- var observer: DarkModeObserver? = DarkModeObserver()
- observer?.action = { newStyle in
-
- collection.append(newStyle)
- count += 1
- }
-
- // act
-
- observer = nil
-
- AppearanceService.shared.hidden_style = AppearanceStyle.dark
- AppearanceService.shared.hidden_style = AppearanceStyle.light
-
- // assert
-
- XCTAssertEqual(count, 0)
- XCTAssertEqual(collection, [])
- }
-
- func test_get_DarkModeUserChoice_when_valueExists_true() {
- // arrange
-
- let mockUserDefaults = MockUserDefaults()
- AppearanceService.ud = mockUserDefaults
-
- mockUserDefaults.isValueExists = true
-
- // act
-
- _ = AppearanceService.DarkModeUserChoice
-
- // assert
-
- mockUserDefaults.verifyInterger(name: DARK_MODE_USER_CHOICE_KEY)
-
- // keep it clean for the others
-
- AppearanceService.ud = UserDefaults.standard
- }
-
- func test_get_DarkModeUserChoice_when_valueExists_false() {
- // arrange
-
- let mockUserDefaults = MockUserDefaults()
- AppearanceService.ud = mockUserDefaults
-
- mockUserDefaults.isValueExists = false
-
- // act
-
- let result = AppearanceService.DarkModeUserChoice
-
- // assert
-
- mockUserDefaults.verifyInterger()
-
- XCTAssertEqual(result, DARK_MODE_USER_CHOICE_DEFAULT)
-
- // keep it clean for the others
-
- AppearanceService.ud = UserDefaults.standard
- }
-
- func test_set_DarkModeUserChoice() {
- // arrange
-
- let mockUserDefaults = MockUserDefaults()
- AppearanceService.ud = mockUserDefaults
-
- let sut = DarkModeOption.off
-
- // act
-
- AppearanceService.DarkModeUserChoice = sut
-
- // assert
-
- mockUserDefaults.verifySetValue(value: sut.rawValue,
- key: DARK_MODE_USER_CHOICE_KEY)
-
- // keep it clean for the others
-
- AppearanceService.ud = UserDefaults.standard
- }
-}
diff --git a/Tests/DarkModeTests/Helpers/MockNotificationCenter.swift b/Tests/DarkModeTests/Helpers/MockNotificationCenter.swift
deleted file mode 100644
index b166538..0000000
--- a/Tests/DarkModeTests/Helpers/MockNotificationCenter.swift
+++ /dev/null
@@ -1,118 +0,0 @@
-//
-// MockNotificationCenter.swift
-// DarkModeTests
-//
-// Created by Mikhail Zhigulin in 7530.
-//
-// Copyright © 7530 - 7531 Mikhail Zhigulin of Novosibirsk.
-//
-// Licensed under the MIT license. See LICENSE file.
-// All rights reserved.
-//
-
-#if canImport(UIKit)
-import UIKit
-#elseif canImport(Cocoa)
-import Cocoa
-#endif
-
-#if os(iOS)
-public typealias Responder = UIResponder
-#elseif os(macOS)
-public typealias Responder = NSResponder
-#endif
-
-import XCTest
-@testable import PerseusDarkMode
-
-class MockNotificationCenter: NotificationCenterProtocol {
-
- // MARK: - addObserver
-
- var registerCallCount = 0
-
- var registerArgs_observers: [Any] = []
- var registerArgs_selectors: [Selector] = []
-
- func addObserver(_ observer: Any,
- selector aSelector: Selector,
- name aName: NSNotification.Name?,
- object anObject: Any?) {
- // guard let observer = observer as? AnyObject else { return }
-
- registerCallCount += 1
-
- registerArgs_observers.append(observer)
- registerArgs_selectors.append(aSelector)
- }
-
- func verifyRegisterObserver(observer: AnyObject,
- selector: Selector,
- file: StaticString = #file,
- line: UInt = #line) {
- guard registerWasCalledOnce(file: file, line: line) else { return }
-
- XCTAssertTrue(registerArgs_observers.first! as AnyObject === observer,
- "observer", file: file, line: line)
-
- XCTAssertEqual(registerArgs_selectors.first, selector,
- "selector", file: file, line: line)
- }
-
- private func registerWasCalledOnce(file: StaticString = #file,
- line: UInt = #line) -> Bool {
- return
- verifyMethodCalledOnce(
- methodName: "register",
- callCount: registerCallCount,
- describeArguments: "name: \(registerArgs_selectors)",
- file: file,
- line: line)
- }
-
- // MARK: - post
-
- var postCallCount = 0
- var postrgs_names: [String] = []
-
- func post(name aName: NSNotification.Name, object anObject: Any?) {
- postCallCount += 1
- postrgs_names.append(aName.rawValue)
- }
-
- func verifyPost(name: NSNotification.Name,
- file: StaticString = #file,
- line: UInt = #line) {
- guard postWasCalledOnce(file: file, line: line) else { return }
-
- XCTAssertTrue(postrgs_names.first! == name.rawValue, "name", file: file, line: line)
- }
-
- private func postWasCalledOnce(file: StaticString = #file, line: UInt = #line) -> Bool {
- return
- verifyMethodCalledOnce(
- methodName: "post",
- callCount: postCallCount,
- describeArguments: "name: \(postrgs_names)",
- file: file,
- line: line)
- }
-}
-
-private func verifyMethodCalledOnce(methodName: String, callCount: Int,
- describeArguments: @autoclosure () -> String,
- file: StaticString = #file,
- line: UInt = #line) -> Bool {
- if callCount == 0 {
- XCTFail("Wanted but not invoked: \(methodName)", file: file, line: line)
- return false
- }
-
- if callCount > 1 {
- XCTFail("Wanted 1 time but was called \(callCount) times. " +
- "\(methodName) with \(describeArguments())", file: file, line: line)
- return false
- }
-
- return true
-}
diff --git a/Tests/DarkModeTests/Helpers/MockUserDefaults.swift b/Tests/DarkModeTests/Helpers/MockUserDefaults.swift
deleted file mode 100644
index 73d4bba..0000000
--- a/Tests/DarkModeTests/Helpers/MockUserDefaults.swift
+++ /dev/null
@@ -1,139 +0,0 @@
-//
-// MockUserDefaults.swift
-// DarkModeTests
-//
-// Created by Mikhail Zhigulin in 7530.
-//
-// Copyright © 7530 - 7531 Mikhail Zhigulin of Novosibirsk.
-//
-// Licensed under the MIT license. See LICENSE file.
-// All rights reserved.
-//
-
-import XCTest
-@testable import PerseusDarkMode
-
-class MockUserDefaults: UserDefaultsProtocol {
-
- var isValueExists = false
-
- func valueExists(forKey key: String) -> Bool { return isValueExists }
-
- // MARK: - getValue
-
- var intergerCallCount = 0
- var intergerArgs_names: [String] = []
-
- func integer(forKey defaultName: String) -> Int {
- intergerCallCount += 1
- intergerArgs_names.append(defaultName)
-
- return 0
- }
-
- func verifyInterger(name: String,
- file: StaticString = #file,
- line: UInt = #line) {
- guard intergerWasCalledOnce(file: file, line: line) else { return }
-
- XCTAssertTrue(intergerArgs_names.first! == name, "interger", file: file, line: line)
- }
-
- func verifyInterger(file: StaticString = #file,
- line: UInt = #line) {
- guard intergerWasNotCalled(file: file, line: line) else { return }
-
- XCTAssertTrue(intergerArgs_names.isEmpty, "interger", file: file, line: line)
- }
-
- private func intergerWasCalledOnce(file: StaticString = #file,
- line: UInt = #line) -> Bool {
- return
- verifyMethodCalledOnce(
- methodName: "interger",
- callCount: intergerCallCount,
- describeArguments: "name: \(intergerArgs_names)",
- file: file,
- line: line)
- }
-
- private func intergerWasNotCalled(file: StaticString = #file, line: UInt = #line) -> Bool {
- return
- verifyMethodNotCalled(
- methodName: "interger",
- callCount: intergerCallCount,
- describeArguments: "name: \(intergerArgs_names)",
- file: file,
- line: line)
- }
-
- // MARK: - setValue
-
- var setValueCallCount = 0
-
- var setValueArgs_values: [Int] = []
- var setValueArgs_keys: [String] = []
-
- func setValue(_ value: Any?, forKey key: String) {
- guard let value = value as? Int else { return }
-
- setValueCallCount += 1
-
- setValueArgs_values.append(value)
- setValueArgs_keys.append(key)
- }
-
- func verifySetValue(value: Int,
- key: String,
- file: StaticString = #file,
- line: UInt = #line) {
- guard setValueWasCalledOnce(file: file, line: line) else { return }
-
- XCTAssertTrue(setValueArgs_values.first! == value,
- "value", file: file, line: line)
-
- XCTAssertEqual(setValueArgs_keys.first, key,
- "key", file: file, line: line)
- }
-
- private func setValueWasCalledOnce(file: StaticString = #file,
- line: UInt = #line) -> Bool {
- return
- verifyMethodCalledOnce(
- methodName: "setValue",
- callCount: setValueCallCount,
- describeArguments: "keys: \(setValueArgs_keys)",
- file: file,
- line: line)
- }
-}
-
-private func verifyMethodCalledOnce(methodName: String, callCount: Int,
- describeArguments: @autoclosure () -> String,
- file: StaticString = #file,
- line: UInt = #line) -> Bool {
- if callCount == 0 {
- XCTFail("Wanted but not invoked: \(methodName)", file: file, line: line)
- return false
- }
-
- if callCount > 1 {
- XCTFail("Wanted 1 time but was called \(callCount) times. " +
- "\(methodName) with \(describeArguments())", file: file, line: line)
- return false
- }
-
- return true
-}
-
-private func verifyMethodNotCalled(methodName: String, callCount: Int,
- describeArguments: @autoclosure () -> String,
- file: StaticString = #file,
- line: UInt = #line) -> Bool {
- if callCount != 0 {
- XCTFail("Don't wanted but invoked: \(methodName)", file: file, line: line)
- return false
- }
-
- return true
-}
diff --git a/Tests/UnitTests/PDMStartTests.swift b/Tests/UnitTests/PDMStartTests.swift
new file mode 100644
index 0000000..09aef9b
--- /dev/null
+++ b/Tests/UnitTests/PDMStartTests.swift
@@ -0,0 +1,25 @@
+//
+// PDMStartTests.swift
+// UnitTests
+//
+// Created by Mikhail Zhigulin in 7530.
+//
+// Copyright © 7530 - 7533 Mikhail A. Zhigulin of Novosibirsk
+// Copyright © 7533 PerseusRealDeal
+//
+// Licensed under the MIT license. See LICENSE file.
+// All rights reserved.
+//
+
+import XCTest
+@testable import PerseusDarkMode
+
+final class FunctionalTests: XCTestCase {
+
+ // func test_zero() { XCTFail("Tests not yet implemented in \(type(of: self)).") }
+
+ func test_the_first_success() {
+ log.time = true
+ log.message(#function)
+ }
+}