Base types for theming an app.
CostumeKit provides a set of Swift protocols for building app themes (costumes). It supports both UIKit and SwiftUI, with full platform coverage for iOS, macOS, tvOS, and watchOS.
Featured in Little Bites of Cocoa Bite #270: Implementing Theming with CostumeKit
Add CostumeKit to your project via Xcode:
- File β Add Packages...
- Enter:
https://github.com/jakemarsh/CostumeKit - Select the version you want
Or add it to your Package.swift:
dependencies: [
.package(url: "https://github.com/jakemarsh/CostumeKit", from: "2.0.0")
]import CostumeKit
public enum AppColors: String, ColorPalette {
case background = "FFFFFF"
case primary = "3CB39E"
case secondary = "216055"
case text = "000000"
}public class AppCostume: Costume {
public var name: String { "Default" }
public var description: String { "The default app theme" }
// Define your styling methods
public func styleHeadline(_ label: UILabel) {
label.font = SystemFont(size: .textStyle(.title1), weight: .bold).fontValue
label.textColor = AppColors.primary.colorValue
}
public func styleBody(_ label: UILabel) {
label.font = SystemFont(size: .textStyle(.body)).fontValue
label.textColor = AppColors.text.colorValue
}
public init() {}
}UIKit:
let costume = AppCostume()
costume.styleHeadline(titleLabel)
costume.styleBody(descriptionLabel)SwiftUI:
struct ContentView: View {
var body: some View {
VStack {
Text("Hello")
.foregroundColor(AppColors.primary.swiftUIColor)
Text("World")
.foregroundColor(AppColors.secondary.swiftUIColor)
}
.costume(AppCostume())
}
}Color is a String typealias that supports hex color values.
// Direct hex string usage
let color: Color = "FF0000"
let uiColor = color.colorValue // UIColor or NSColor
// Or use UIColor/NSColor directly
let red = UIColor(hexString: "FF0000")
let green = UIColor(hexString: "#00FF00") // # is optional
let blue = UIColor(hex: 0x0000FF)
let semiTransparent = UIColor(hexString: "FF0000", alpha: 0.5)Define type-safe color palettes using enums:
public enum AppColors: String, ColorPalette {
case white = "FFFFFF"
case lightTeal = "3CB39E"
case forestGreen = "216055"
case black = "000000"
}
// Usage
let color = AppColors.forestGreen.colorValue // UIColor/NSColor
let swiftUIColor = AppColors.forestGreen.swiftUIColor // SwiftUI ColorAny type conforming to ColorConvertible can be converted to platform colors:
let darkened = AppColors.primary.darkened(amount: 0.3)
let lightened = AppColors.primary.lightened(amount: 0.3)The Font protocol supports dynamic type with custom fonts:
public struct AppFont: Font {
public var size: FontSize
public init(size: FontSize = .textStyle(.body)) {
self.size = size
}
public var fontValue: UIFont {
UIFont(name: "CustomFont-Regular", size: pointSize)!
}
}Supports both dynamic text styles and fixed sizes with accessibility scaling:
// Dynamic (responds to user's text size preference)
let bodySize = FontSize.textStyle(.body)
let titleSize = FontSize.textStyle(.title1)
// Fixed (still adjusts for accessibility)
let fixedSize = FontSize.fixed(14)A built-in implementation for system fonts:
let regular = SystemFont(size: .textStyle(.body))
let bold = SystemFont(size: .textStyle(.headline), weight: .bold)
let fixed = SystemFont(size: .fixed(18), weight: .medium)
// UIKit
label.font = bold.fontValue
// SwiftUI
Text("Hello").font(bold.swiftUIFont)Define SVG asset metadata:
enum AppSVGs: SVG {
case clockGlyph
case bird
func metadata() -> SVGMetadata {
switch self {
case .clockGlyph:
return SVGMetadata(name: "clock", size: CGSize(width: 100, height: 100), fullColor: false)
case .bird:
return SVGMetadata(name: "bird", size: CGSize(width: 58, height: 28), fullColor: true)
}
}
}What you do with SVGMetadata is up to you. We recommend SwiftSVG for parsing and rendering.
The main protocol for defining themes:
public class DarkCostume: Costume {
public var name: String { "Dark" }
public var description: String { "Dark mode theme" }
public let backgroundColor = AppColors.black
public let primaryColor = AppColors.lightTeal
public let textColor = AppColors.white
public func styleBackground(_ view: UIView) {
view.backgroundColor = backgroundColor.colorValue
}
public func styleTitle(_ label: UILabel) {
label.font = SystemFont(size: .textStyle(.title1), weight: .bold).fontValue
label.textColor = primaryColor.colorValue
}
public init() {}
}CostumeKit integrates with SwiftUI's environment system:
// Set costume at root
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.costume(AppCostume())
}
}
}
// Access in child views
struct ThemedView: View {
@Environment(\.costume) var costume
var body: some View {
if let appCostume = costume?.as(AppCostume.self) {
Text(appCostume.name)
}
}
}// From ColorPalette
Text("Hello")
.foregroundColor(AppColors.primary.swiftUIColor)
// From hex string
Text("World")
.foregroundColor(Color(hex: "3CB39E"))let font = SystemFont(size: .textStyle(.title1), weight: .bold)
Text("Hello")
.font(font.swiftUIFont)| Platform | Minimum Version |
|---|---|
| iOS | 15.0 |
| macOS | 12.0 |
| tvOS | 15.0 |
| watchOS | 8.0 |
CostumeKit 2.0 modernizes the API while keeping the same concepts:
| 1.x | 2.0 |
|---|---|
UIFontWeightRegular |
UIFont.Weight.regular |
UIFontTextStyle.body |
UIFont.TextStyle.body |
hex.characters.count |
hex.count |
| UIKit only | UIKit + SwiftUI |
All protocols (Costume, ColorPalette, Font, SVG) remain unchanged.
MIT License. See LICENSE for details.
Jake Marsh (@jakemarsh)