摘要
ThemeKit 是一个轻量级主题库,完全用 Swift 编写,为 Swift 和 Objective-C macOS 应用程序提供主题功能。
ThemeKit 是由以下人士贡献的:
快速入门
- 下载 ThemeKit 示例 二进制文件并进行尝试!
- 阅读 使您的 macOS 应用程序可主题化 文章(简单教程)。
- 检查 ThemeKit 文档。
目录
功能
- 使用Swift 4.2编写
- 可选配置,无需任何配置
- 忽略性能影响
- 自动主题化窗口(可配置)
- 主题
LightTheme
(默认macOS外观)DarkTheme
SystemTheme
(默认主题)。动态解析为ThemeManager.lightTheme
或ThemeManager.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
-
将其添加到
Podfile
use_frameworks! target '[YOUR APP TARGET]' do pod 'macOSThemeKit', '~> 1.2.0' end
当使用 CocoaPods 时,ThemeKit 模块名为
macOSThemeKit
import macOSThemeKit
-
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 兼容的
ThemeManager.shared.theme
ThemeManager.shared.effectiveTheme
ThemeManager.shared.themes
ThemeManager.shared.userThemes
示例
// 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.themeIfCompliantWithWindowThemePolicy()
如果符合
ThemeManager.shared.windowThemePolicy
(并且外观需要更新),则主题化窗口。 -
主题化所有符合
ThemeManager.shared.windowThemePolicy
(并且外观需要更新)的窗口。 -
任何窗口特定的主题。这通常是指
nil
,这意味着将使用当前的全局主题。请注意,当使用窗口特定的主题时,只有相关的NSAppearance
将自动设置。所有主题感知资产(ThemeColor
、ThemeGradient
和ThemeImage
)应该调用返回已解析颜色的方法(这意味着它们不会随着主题更改而更改,您需要手动观察主题更改并之后设置颜色)ThemeColor.color(for view:, selector:)
ThemeGradient.gradient(for view:, selector:)
ThemeImage.image(for view:, selector:)
此外,请注意,系统覆盖的颜色(
NSColor.*
)将始终使用全局主题。 -
返回当前的当前有效主题(只读)。
-
NSWindow.windowEffectiveThemeAppearance
返回当前的当前外观(只读)。
主题感知资产
ThemeColor
、ThemeGradient
和ThemeImage
分别提供动态随着当前主题变化的颜色、渐变和图像。
此外,定义在ThemeColor
子类扩展上的NSColor
类中的命名颜色将覆盖系统颜色,提供主题感知颜色。
例如,项目定义了ThemeColor.brandColor
颜色。这将根据运行时选择的主题解析为不同的颜色
ThemeColor.brandColor
将在选择了浅色主题时解析为NSColor.blue
ThemeColor.brandColor
在选择深色主题时会解析为NSColor.white
ThemeColor.brandColor
在某些用户自定义主题下(比如UserTheme
)会解析为rgba(100, 50, 0, 0.5)
同样地,定义一个 ThemeColor.labelColor
会覆盖 NSColor.labelColor
(ThemeColor
是 NSColor
的子类),并且 ThemeKit 还允许多对每种主题进行 labelColor
的自定义。
在覆盖 ThemeColor
扩展中的颜色时,NSColor 扩展 可能很有用。
后备资源
当在当前主题中查找资源时,ThemeKit 提供了一个简单的后备机制。它将按照以下顺序搜索资源:
- 主题中定义的资产名称(例如,
myBackgroundColor
) - 根据资产是前景/背景颜色、渐变还是图像,分别在主题中定义的
fallbackForegroundColor
、fallbackBackgroundColor
、fallbackGradient
或fallbackImage
- 根据资产是前景/背景颜色、渐变还是图像,分别在内定义的
defaultFallbackForegroundColor
、defaultFallbackBackgroundColor
、fallbackGradient
或defaultFallbackImage
然而,对于覆盖的系统命名颜色,后备机制有所不同,更为简单
- 主题中定义的资产名称(例如,
labelColor
) - 系统中原定义的原始资产(例如,
NSColor.labelColor
)
有关更多信息,请参阅 ThemeColor
、ThemeGradient
和 ThemeImage
创建主题
本地主题
要创建附加的主题,您只需要创建一个符合 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文档?
窗口标题栏/工具栏/标签栏可以主题化吗?
是的 - 请在Demo项目中查看一种方法。基本原理是在窗口标题栏的下方添加了一个TitleBarOverlayView视图,正如在WindowController控制器中所示。
控件可以有不同的颜色着色吗?
除了继承外观时设定的颜色(亮色背景上的深文字或暗色背景上的浅文字)之外,在本地无法指定控件(如按钮、弹出窗口等)文本和/或背景填充的不同颜色。
对于简单情况,覆盖NSColor
就足够了:例如,NSColor.labelColor
是一个用于文本标签的命名颜色;覆盖它将允许所有标签相应地主题化。您可以使用NSColor.colorMethodNames()
获取所有可覆盖的命名颜色(类方法名称)列表。
对于更复杂的情况,如具有自定义绘制的视图/控件,请参阅下一个问题。
我可以制作主题感知的自定义绘制视图/控件吗?
是的,你可以!使用 主题感知资源(ThemeColor
和 ThemeGradient
)实现你自己的自定义控件绘制,这样你的控件绘制将始终自动适应当前主题...
情况需要时(例如,如果绘制被缓存),您可以观察主题变化来刷新 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
}
我遇到字体平滑问题!
当您使用未设置背景颜色的文本时,可能会遇到字体平滑问题。关键是,始终在使用/绘制文本时指定/绘制背景。
-
对于类似
NSTextField
、NSTextView
等在控件上指定背景颜色。例如:
control.backgroundColor = NSColor.black
-
对于自定义文本渲染
首先绘制背景填充,然后启用字体平滑并渲染文本。例如:
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()
-
对于自定义
NSButton
。这有点复杂,因为你需要重写私有方法。如果你要将你的应用程序发布到Mac App Store,你必须首先检查这是否允许。
a) 重写私有方法
_backgroundColorForFontSmoothing
以返回你的按钮背景颜色。b) 如果(a)不足以解决问题,你还需要重写
_textAttributes
并改变从super
调用返回的字典,为键NSBackgroundColorAttributeName
提供你的背景颜色。
许可证
ThemeKit 在MIT许可证下可用。有关更多信息,请参阅 LICENSE 文件。