macOSThemeKit 1.4.0

macOSThemeKit 1.4.0

测试已测试
语言语言 SwiftSwift
许可证 MIT
发布最后发布2021年7月
SPM支持 SPM

Nuno GriloMicha MazaheriŽymantas Paulas维护。




  • Paw 和 Nuno Grilo

ThemeKit

macOS Swift4 Release MIT CocoaPods Carthage

摘要

ThemeKit 是一个轻量级主题库,完全用 Swift 编写,为 Swift 和 Objective-C macOS 应用程序提供主题功能。

ThemeKit 是由以下人士贡献的:❤️Nuno GriloPaw 团队。

ThemeKit Animated Demo

快速入门

目录

功能

  • 使用Swift 4.2编写
  • 可选配置,无需任何配置
  • 忽略性能影响
  • 自动主题化窗口(可配置)
  • 主题
    • LightTheme(默认macOS外观)
    • DarkTheme
    • SystemTheme(默认主题)。动态解析为 ThemeManager.lightThemeThemeManager.darkTheme,取决于 "系统偏好设置 > 一般 > 外观"
    • 支持自定义主题(Theme
    • 支持用户定义的主题(UserTheme
  • 主题感知资源
    • ThemeColor:随主题动态变化的颜色
    • ThemeGradient:随主题动态变化的渐变
    • ThemeImage:随主题动态变化的图片
    • 可选覆盖具有命名颜色的NSColor(例如,labelColor)以动态与主题更改

安装

ThemeKit 版本 Swift 版本
1.0.0 3.0
1.1.0 4.0
1.2.0 4.1
1.2.3 4.2

有多种方式可以在项目中包含 ThemeKit

  • CocoaPods

    将其添加到 Podfile

    use_frameworks!
    target '[YOUR APP TARGET]' do
        pod 'macOSThemeKit', '~> 1.2.0'
    end
    

    当使用 CocoaPods 时,ThemeKit 模块名为 macOSThemeKit

    import macOSThemeKit
    
  • Carthage

    github "luckymarmot/ThemeKit"
    

    然后使用以下命令导入 ThemeKit 模块:

    import ThemeKit
    
  • 手动

    • 可以将 ThemeKit.framework 添加到项目,或者从 ThemeKit\ 文件夹手动将源文件添加到项目中。
    • 如果导入到 Objective-C 项目中,还需要包含所有相关的 Swift 框架(如此处所述)

    然后使用以下命令导入 ThemeKit 模块:

    import ThemeKit
    

使用

简单使用

在最简单的用法中,应用程序可以使用单行命令进行主题化

在Swift中
func applicationWillFinishLaunching(_ notification: Notification) {
	
	/// Apply the dark theme
	ThemeManager.darkTheme.apply()
	
	/// or, the light theme
	//ThemeManager.lightTheme.apply()
	
	/// or, the 'system' theme, which dynamically changes to light or dark, 
	/// respecting *System Preferences > General > Appearance* setting.
	//ThemeManager.systemTheme.apply()
	
}
在Objective-C中
- (void)applicationWillFinishLaunching:(NSNotification *)aNotification {
	
	// Apply the dark theme
	TKDarkTheme *darkTheme = TKThemeManager.darkTheme;
	[[TKThemeManager sharedManager] setTheme:darkTheme];
	
}

高级用法

以下代码将定义哪些窗口应该自动主题化(WindowThemePolicy)并添加对用户主题(UserTheme)的支持

在Swift中
func applicationWillFinishLaunching(_ notification: Notification) {

	/// Define default theme.
	/// Used on first run. Default: `SystemTheme`.
	/// Note: `SystemTheme` is a special theme that resolves to `ThemeManager.lightTheme` or `ThemeManager.darkTheme`,
	/// depending on the macOS preference at 'System Preferences > General > Appearance'.
	ThemeManager.defaultTheme = ThemeManager.lightTheme
	
	/// Define window theme policy.
	ThemeManager.shared.windowThemePolicy = .themeAllWindows
	//ThemeManager.shared.windowThemePolicy = .themeSomeWindows(windowClasses: [MyWindow.self])
	//ThemeManager.shared.windowThemePolicy = .doNotThemeSomeWindows(windowClasses: [NSPanel.self])
	//ThemeManager.shared.windowThemePolicy = .doNotThemeWindows
	    
	/// Enable & configure user themes.
	/// Will use folder `(...)/Application Support/{your_app_bundle_id}/Themes`.
	let applicationSupportURLs = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true)
	let thisAppSupportURL = URL.init(fileURLWithPath: applicationSupportURLs.first!).appendingPathComponent(Bundle.main.bundleIdentifier!)
	let userThemesFolderURL = thisAppSupportURL.appendingPathComponent("Themes")
	ThemeManager.shared.userThemesFolderURL = userThemesFolderURL
	
	/// Change the default light and dark theme, used when `SystemTheme` is selected.
	//ThemeManager.lightTheme = ThemeManager.shared.theme(withIdentifier: PaperTheme.identifier)!
	//ThemeManager.darkTheme = ThemeManager.shared.theme(withIdentifier: "com.luckymarmot.ThemeKit.PurpleGreen")!
	
	/// Apply last applied theme (or the default theme, if no previous one)
	ThemeManager.shared.applyLastOrDefaultTheme()
	 
}    
在Objective-C中
- (void)applicationWillFinishLaunching:(NSNotification *)aNotification {

    /// Define default theme.
    /// Used on first run. Default: `SystemTheme`.
    /// Note: `SystemTheme` is a special theme that resolves to `ThemeManager.lightTheme` or `ThemeManager.darkTheme`,
    /// depending on the macOS preference at 'System Preferences > General > Appearance'.
    [TKThemeManager setDefaultTheme:TKThemeManager.lightTheme];
    
    /// Define window theme policy.
    [TKThemeManager sharedManager].windowThemePolicy = TKThemeManagerWindowThemePolicyThemeAllWindows;
    //[TKThemeManager sharedManager].windowThemePolicy = TKThemeManagerWindowThemePolicyThemeSomeWindows;
    //[TKThemeManager sharedManager].themableWindowClasses = @[[MyWindow class]];
    //[TKThemeManager sharedManager].windowThemePolicy = TKThemeManagerWindowThemePolicyDoNotThemeSomeWindows;
    //[TKThemeManager sharedManager].notThemableWindowClasses = @[[NSPanel class]];
    //[TKThemeManager sharedManager].windowThemePolicy = TKThemeManagerWindowThemePolicyDoNotThemeWindows;
    
    /// Enable & configure user themes.
    /// Will use folder `(...)/Application Support/{your_app_bundle_id}/Themes`.
    NSArray<NSString*>* applicationSupportURLs = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
    NSURL* thisAppSupportURL = [[NSURL fileURLWithPath:applicationSupportURLs.firstObject] URLByAppendingPathComponent:[NSBundle mainBundle].bundleIdentifier];
    NSURL* userThemesFolderURL = [thisAppSupportURL URLByAppendingPathComponent:@"Themes"];
    [TKThemeManager sharedManager].userThemesFolderURL = userThemesFolderURL;
    
    /// Change the default light and dark theme, used when `SystemTheme` is selected.
    //TKThemeManager.lightTheme = [[TKThemeManager sharedManager] themeWithIdentifier:PaperTheme.identifier];
    //TKThemeManager.darkTheme = [[TKThemeManager sharedManager] themeWithIdentifier:@"com.luckymarmot.ThemeKit.PurpleGreen"];
    
    /// Apply last applied theme (or the default theme, if no previous one)
    [[TKThemeManager sharedManager] applyLastOrDefaultTheme];
    
}

请检查演示应用程序的源代码,以了解 ThemeKit 的更完整使用示例。

观察主题更改

ThemeKit 提供以下通知

  • Notification.Name.willChangeTheme 当当前主题即将更改时发送
  • Notification.Name.didChangeTheme 当当前主题更改时发送
  • Notification.Name.didChangeSystemTheme 当系统主题更改时发送(系统偏好设置 > 系统)

示例

// Register to be notified of theme changes
NotificationCenter.default.addObserver(self, selector: #selector(changedTheme(_:)), name: .didChangeTheme, object: nil)

@objc private func changedTheme(_ notification: Notification) {
	// ...
}

此外,以下属性是 KVO 兼容的

示例

// Register for KVO changes on ThemeManager.shared.effectiveTheme
ThemeManager.shared.addObserver(self, forKeyPath: "effectiveTheme", options: NSKeyValueObservingOptions.init(rawValue: 0), context: nil)

public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
	if keyPath == "effectiveTheme" {
		// ...
   }
}

手动主题化窗口

如果情况(WindowThemePolicy)未设置为.themeAllWindows,您可能需要手动主题化一个窗口。您可以为此使用我们的NSWindow扩展

NSWindow 扩展
  • NSWindow.theme()

    如果外观需要更新,则主题化窗口。不检查策略合规性。

  • NSWindow.themeIfCompliantWithWindowThemePolicy()

    如果符合ThemeManager.shared.windowThemePolicy(并且外观需要更新),则主题化窗口。

  • NSWindow.themeAllWindows()

    主题化所有符合ThemeManager.shared.windowThemePolicy(并且外观需要更新)的窗口。

  • NSWindow.windowTheme

    任何窗口特定的主题。这通常是指nil,这意味着将使用当前的全局主题。请注意,当使用窗口特定的主题时,只有相关的NSAppearance将自动设置。所有主题感知资产(ThemeColorThemeGradientThemeImage)应该调用返回已解析颜色的方法(这意味着它们不会随着主题更改而更改,您需要手动观察主题更改并之后设置颜色)

    • ThemeColor.color(for view:, selector:)
    • ThemeGradient.gradient(for view:, selector:)
    • ThemeImage.image(for view:, selector:)

    此外,请注意,系统覆盖的颜色(NSColor.*)将始终使用全局主题。

  • NSWindow.windowEffectiveTheme

    返回当前的当前有效主题(只读)。

  • NSWindow.windowEffectiveThemeAppearance

    返回当前的当前外观(只读)。

主题感知资产

ThemeColorThemeGradientThemeImage分别提供动态随着当前主题变化的颜色、渐变和图像。

此外,定义在ThemeColor子类扩展上的NSColor类中的命名颜色将覆盖系统颜色,提供主题感知颜色。

例如,项目定义了ThemeColor.brandColor颜色。这将根据运行时选择的主题解析为不同的颜色

  • ThemeColor.brandColor将在选择了浅色主题时解析为NSColor.blue
  • ThemeColor.brandColor 在选择深色主题时会解析为 NSColor.white
  • ThemeColor.brandColor 在某些用户自定义主题下(比如 UserTheme)会解析为 rgba(100, 50, 0, 0.5)

同样地,定义一个 ThemeColor.labelColor 会覆盖 NSColor.labelColorThemeColorNSColor 的子类),并且 ThemeKit 还允许多对每种主题进行 labelColor 的自定义。

在覆盖 ThemeColor 扩展中的颜色时,NSColor 扩展 可能很有用。

后备资源

当在当前主题中查找资源时,ThemeKit 提供了一个简单的后备机制。它将按照以下顺序搜索资源:

  • 主题中定义的资产名称(例如,myBackgroundColor
  • 根据资产是前景/背景颜色、渐变还是图像,分别在主题中定义的 fallbackForegroundColorfallbackBackgroundColorfallbackGradientfallbackImage
  • 根据资产是前景/背景颜色、渐变还是图像,分别在内定义的 defaultFallbackForegroundColordefaultFallbackBackgroundColorfallbackGradientdefaultFallbackImage

然而,对于覆盖的系统命名颜色,后备机制有所不同,更为简单

  • 主题中定义的资产名称(例如,labelColor
  • 系统中原定义的原始资产(例如,NSColor.labelColor

有关更多信息,请参阅 ThemeColorThemeGradientThemeImage

创建主题

本地主题

要创建附加的主题,您只需要创建一个符合 Theme 协议并扩展 NSObject 的类。

示例主题

import Cocoa
import ThemeKit
	
class MyOwnTheme: NSObject, Theme {
    
    /// Light theme identifier (static).
    public static var identifier: String = "com.luckymarmot.ThemeKit.MyOwnTheme"
    
    /// Unique theme identifier.
    public var identifier: String = MyOwnTheme.identifier
    
    /// Theme display name.
    public var displayName: String = "My Own Theme"
    
    /// Theme short display name.
    public var shortDisplayName: String = "My Own"
    
    /// Is this a dark theme?
    public var isDarkTheme: Bool = false
    
    /// Description (optional).
    public override var description : String {
        return "<\(MyOwnTheme.self): \(themeDescription(self))>"
    }
    
    // MARK: -
    // MARK: Theme Assets
    
    // Here you can define the instance methods for the class methods defined 
    // on `ThemeColor`, `ThemeGradient` and `ThemeImage`, if any. Check
    // documentation of these classes for more details.
}

用户主题

ThemeKit也支持使用简单的文本文件(.theme文件)定义附加主题。以下是一个非常基础的.theme文件示例:

// ************************* Theme Info ************************* //
displayName = My Theme 1
identifier = com.luckymarmot.ThemeKit.MyTheme1
darkTheme = true

// ********************* Colors & Gradients ********************* //
# define color for `ThemeColor.brandColor`
brandColor = $blue
# define a new color for `NSColor.labelColor` (overriding)
labelColor = rgb(11, 220, 111)
# define gradient for `ThemeGradient.brandGradient`
brandGradient = linear-gradient($orange.sky, rgba(200, 140, 60, 1.0))
 
// ********************* Images & Patterns ********************** //
# define pattern image from named image "paper" for color `ThemeColor.contentBackgroundColor`
contentBackgroundColor = pattern(named:paper)
# define pattern image from filesystem (relative to user themes folder) for color `ThemeColor.bottomBackgroundColor`
bottomBackgroundColor = pattern(file:../some/path/some-file.png)
# define image using named image "apple"
namedImage = image(named:apple)
# define image using from filesystem (relative to user themes folder)
fileImage = image(file:../some/path/some-file.jpg)

// *********************** Common Colors ************************ //
blue = rgb(0, 170, 255)
orange.sky = rgb(160, 90, 45, .5)

// ********************** Fallback Assets *********************** //
fallbackForegroundColor = rgb(255, 10, 90, 1.0)
fallbackBackgroundColor = rgb(255, 200, 190)
fallbackGradient = linear-gradient($blue, rgba(200, 140, 60, 1.0))

要启用对用户主题的支持,只需设置它们的位置即可。

// Setup ThemeKit user themes folder
ThemeManager.shared.userThemesFolderURL = //...

有关更多信息,请参阅UserTheme

常见问题解答(FAQ)

我可以在哪里找到API文档?

文档可以在这里找到。您还可以在Dash上安装它。

窗口标题栏/工具栏/标签栏可以主题化吗?

是的 - 请在Demo项目中查看一种方法。基本原理是在窗口标题栏的下方添加了一个TitleBarOverlayView视图,正如在WindowController控制器中所示。

控件可以有不同的颜色着色吗?

除了继承外观时设定的颜色(亮色背景上的深文字或暗色背景上的浅文字)之外,在本地无法指定控件(如按钮、弹出窗口等)文本和/或背景填充的不同颜色。

对于简单情况,覆盖NSColor就足够了:例如,NSColor.labelColor是一个用于文本标签的命名颜色;覆盖它将允许所有标签相应地主题化。您可以使用NSColor.colorMethodNames()获取所有可覆盖的命名颜色(类方法名称)列表。

对于更复杂的情况,如具有自定义绘制的视图/控件,请参阅下一个问题。

我可以制作主题感知的自定义绘制视图/控件吗?

是的,你可以!使用 主题感知资源ThemeColorThemeGradient)实现你自己的自定义控件绘制,这样你的控件绘制将始终自动适应当前主题...

情况需要时(例如,如果绘制被缓存),您可以观察主题变化来刷新 UI 或执行任何与主题相关的操作。请查看上面的 使用 部分中的“观察主题变化”。

在主题化后,NSTableView的单元格会获得背景颜色!

请检查 这个问题。此主题问题仅影响基于视图的 NSTableView,当在 macOS < 10.14 的 sheets 上放置时。请查看 这个评论 了解如何修复此问题的简要说明,以及一个演示问题和修复的小项目。

在暗色主题中滚动条显示全白色!

如果用户在 系统偏好设置 中选择始终显示滚动条,则在暗色主题中可能会渲染为全白色。为了绕过这个问题,我们需要观察主题变化并直接更改其背景颜色。例如:

scrollView?.backgroundColor = ThemeColor.myBackgroundColor
scrollView?.wantsLayer = true
NotificationCenter.default.addObserver(forName: .didChangeTheme, object: nil, queue: nil) { (note) in
  scrollView?.verticalScroller?.layer?.backgroundColor = ThemeColor.myBackgroundColor.cgColor
}

我遇到字体平滑问题!

当您使用未设置背景颜色的文本时,可能会遇到字体平滑问题。关键是,始终在使用/绘制文本时指定/绘制背景。

  1. 对于类似 NSTextFieldNSTextView

    在控件上指定背景颜色。例如:

    control.backgroundColor = NSColor.black
  2. 对于自定义文本渲染

    首先绘制背景填充,然后启用字体平滑并渲染文本。例如:

    let context = NSGraphicsContext.current()?.cgContext
    NSColor.black.set()
    context?.fill(frame)
    context?.saveGState()
    context?.setShouldSmoothFonts(true)
    
    // draw text...
    
    context?.restoreGState()

    作为最后的解决方案 - 如果你真的无法绘制背景颜色,你可以禁用字体平滑,这可以略微改善文本渲染。

    let context = NSGraphicsContext.current()?.cgContext
    context?.saveGState()
    context?.setShouldSmoothFonts(false)
    
    // draw text...
    
    context?.restoreGState()
  3. 对于自定义 NSButton

    这有点复杂,因为你需要重写私有方法。如果你要将你的应用程序发布到Mac App Store,你必须首先检查这是否允许。

    a) 重写私有方法 _backgroundColorForFontSmoothing 以返回你的按钮背景颜色。

    b) 如果(a)不足以解决问题,你还需要重写 _textAttributes 并改变从 super 调用返回的字典,为键 NSBackgroundColorAttributeName 提供你的背景颜色。

许可证

ThemeKit 在MIT许可证下可用。有关更多信息,请参阅 LICENSE 文件。