An unintrusive & light-weight iOS app-theming library with support for animated theme switching.

Overview

jumbotron

Gestalt

Gestalt is an unintrusive and light-weight framework for application theming with support for animated theme switching.

screencast

Usage

Let's say you want to theme a view controller with a single label:

import Gestalt

struct Theme: Gestalt.Theme {
    let view: ViewTheme = .init()

    static let light: Theme = .init(view: .light)
    static let dark: Theme = .init(view: .dark)
}

struct ViewTheme: Gestalt.Theme {
    let font = UIFont.preferredFont(forTextStyle: .headline)
    let color: UIColor
    let backgroundColor: UIColor

    static let light: Theme = .init(
        color: UIColor.black
        backgroundColor: UIColor.white
    )

    static let dark: Theme = .init(
        color: UIColor.white
        backgroundColor: UIColor.black
    )
}

// In `AppDelegate.application(_:didFinishLaunchingWithOptions:)`
// assign a default theme (or user's choice from user defaults):
ThemeManager.default.theme = Theme.light

class ViewController: UIViewController {
    @IBOutlet var label: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.observe(theme: \Theme.view)
    }
}

extension ViewController: Themeable {

    typealias Theme = ViewTheme

    func apply(theme: Theme) {
        self.view.backgroundColor = theme.backgroundColor
        self.label.textColor = theme.color
        self.label.font = theme.font
    }
}

The call self.observe(theme: \Theme.view) registers the receiver for theme observation on ThemeManager.default for future theme changes and then calls it once immediately. The initial call is not animated, any further changes however are animated.

To change the current theme (even while the app is running) simply assign a different theme to your given ThemeManager in use:

ThemeManager.default.theme = Theme.dark

This will cause all previously registered closures on the given ThemeManager to be called again.

See the GestaltDemo target for a more realistic/elaborate usage example.

Note:

  1. It is generally sufficient to use ThemeManager.default. It is however possible to create dedicated ThemeManagers via let manager = ThemeManager().

Usage in App Extensions

The use appearance proxies after a view has already been loaded this library uses a hack that removes and re-adds the root view of the application from the main window to activate the proxies. This is not possible in app extensions, such as a today widget, because the extension safe API restricts access to the main window. So to use this library in app extensions you need to manually trigger the reload of the root view by adding something like this to your root view controller after you set up your themes.

ThemeManager.default.observe(theme: Theme.self) { [weak self] _ in
        if let strongSelf = self, let superview = strongSelf.view.superview {
            strongSelf.view.removeFromSuperview()
            superview.addSubview(strongSelf.view)
        }
    }

Important:

  1. The body of func apply(theme: Theme) should be idempotent to avoid unwanted side-effects on repeated calls.

Installation

The recommended way to add Gestalt to your project is via Carthage:

github 'regexident/Gestalt' ~> 2.0.0

or via Cocoapods:

pod 'Gestalt', '~> 2.0.0'

or via Swift Package Manager:

let package = Package(
    name: "GestaltDemo",
    dependencies: [
        .package(url: "https://github.com/regexident/Gestalt.git", from: "2.0.0")
    ],
    targets: [
        .target(name: "GestaltDemo", dependencies: [ "Gestalt" ])
    ]
)

License

Gestalt is available under the MPL-2.0 license. See the LICENSE file for more info.

Comments
  • Extension Safe API compatibility

    Extension Safe API compatibility

    This fixes #15 and thus makes this library usable in App Extensions.

    It's simple, but it seems to work pretty well. If have test for a few days now and I didn't notice any adverse effects. Changes to defaults in the main app even render in the widget almost instantly.

    opened by heilerich 10
  • Changing appearance causes broken keyboard/toolbar

    Changing appearance causes broken keyboard/toolbar

    Hi there,

    there is a strange thing going on with the keyboard / toolbar when switching the theme which is apparently caused by this "hack" in ThemeManager https://github.com/regexident/Gestalt/blob/master/Gestalt/ThemeManager.swift#L259

    It causes the toolbar to "detach" from the keyboard and slide in and out from the top of the screen. I assume the keyboard view itself breaks when being removed and added again afterwards.

    Do you know if there is a better way to change the appearance or is this still the only possible "solution"?

    Thanks in advance :)

    It's a bit hard to see on the screenshot but I also included the sample project. Bildschirmfoto 2020-12-09 um 14 53 18

    Sample project: BrokenKeyboard.zip

    opened by patrickniepel 4
  • Detected redundant observation of

    Detected redundant observation of

    I use gestalt same as you describe in Usage part. I don't use storyboard or xib (entire project is in code). I have custom views in my controller and every view has it's own theme also controller. When i push or present controller for first time everything is fine, but if i pop or dissmiss and push again i get "Detected redundant observation" from some view or controller. I looked up in your demo project and in appdelegate there is var disposables: [Disposable]?. I haven't implement that stuff. I check for retain cycle all views and controller deinitialized properly. Any help for Detected redundant observation??

    opened by devkokodev 3
  • Window hack not extension safe

    Window hack not extension safe

    I'd like to use your library in a dynamic library which is shared between the main app and a today widget (which is also themeable). The problem is that your code won't compile when used in a target with extension safe API, because UIApplication.shared.windows is not available in app extensions and you are using it here:

    https://github.com/regexident/Gestalt/blob/7e9093b1c821ec6e1edda17674ea34ec4faeef4d/Gestalt/ThemeManager.swift#L226-L235

    Could you explain to me what this part of the code is doing? As I understand it from the comment this covers some kind of edge case? Is this strictly necessary?

    I could provide code (and a PR) that will execute the above snippet only in environments where it is allowed and ignore it in environments where extension safe API is required. Thus, it would make your library usable in targets where only extension safe API is allowed. In my case this would mean using the same dynamic library would only execute the above snippet in my main app and not in the extension. Would this break anything?

    opened by heilerich 3
  • Cocoapods

    Cocoapods

    Hi, thanks for this great library!

    Cocoapods currently point to earlier version (1.1.0) can you please update it to point to the latest one (2.0) ?

    https://cocoapods.org/pods/Gestalt

    Thanks!

    opened by ranhsd 3
  • Initial theme apply?

    Initial theme apply?

    I'm probably misunderstood something but how do I apply the styling initially?

    As soon as a view registers the observer in its viewDidLoad method the theme switching obviously works, but how should the theme be applied initially?

    I first thought to simply call apply initially the same way as observe but that results in an error:

    self.apply(theme: \MyTheme.view)
    self.observe(theme: \MyTheme.view)
    

    And storing the current theme somewhere and applying it from there seems wrong, like im missing the intended implementation:

    self.apply(theme: Somewhere.currenTheme.view)
    

    Kind regards Mario

    opened by mario-deluna 3
  • MPL License and the App Store

    MPL License and the App Store

    Hi Vincent,

    I've been using Gestalt in an app and recently noticed that it was licensed MPL. I wasn't sure what that means with respect to shipping to the App Store and my source code. My app is for commercial purposes, and I plan to keep it closed source, but I'm still a bit confused as to what I need to do to be MPL compliant.

    I was hoping you could clarify what needs to be done with my app (open source, display the copyright notice prominently, not be able to ship it at all, etc) to know whether I can use Gestalt.

    Thanks a lot!

    opened by mergesort 2
  • Fix broken UITextEffectsWindow after theme change

    Fix broken UITextEffectsWindow after theme change

    This PR adds a new property to the ThemeManager that allows to set a list of window class names, which are then checked during the appearance hack.

    This change fixes a problem where the appearance hack would change the views of the UITextEffectsWindow, which then leads to side-effects when a keyboard is shown.

    These changes here are purely additive to not break any existing integrations.

    The property will be set through the ThemeManager like follows

    ThemeManager.default.allowedWindowClasses = [UIWindow.classForCoder()]
    

    Closes #25

    opened by EmDee 1
  • How To Observe Themes Dynamically?

    How To Observe Themes Dynamically?

    In my app I change the currently selected theme, and save it to disk like so:

    ThemeManager.default.theme = OfflineService.AppThemer.currentlySelectedColorScheme
    // OfflineService.AppThemer.currentlySelectedColorScheme is a `ColorScheme` that conforms to `Theme`
    

    The themes are dynamically downloaded from CloudKit, stored in the OfflineService, and then accessed when the user manually changes their theme.

    Before when I used to be able to setup my app like this, and it would propagate whenever a change occurred:

    ThemeManager.default.apply(theme: ColorScheme.self, to: self, animated: true) { viewController, colorScheme in
        self.view.backgroundColor = colorScheme.backgroundColor
    }
    

    Now I'm using the new API in version 2.0 like so, and changes aren't propagating when a theme change occurs:

    func apply(theme: ColorScheme) {
        self.view.backgroundColor = theme.backgroundColor
    }
    

    I understand there needs to be some registration, such as self.observe(theme: \ApplicationTheme.custom.stageDesign) in the sample project, but since my themes don't get stored in source code, they're generated by downloading the themes, I'm not sure how to properly call self.observe.

    I tried a few ideas, including self.observe(theme: \OfflineService.appThemer.currentlySelectedColorScheme), but none of them seemed to work.

    I can't seem to figure out if there's a way to observe the theme changes for something that doesn't exist in the source, and was wondering if you had any ideas.

    Thanks a lot!

    opened by mergesort 1
  • Invalid Bundle Version String

    Invalid Bundle Version String

    First of all, thanks for creating this awesome library! I adopted it quickly in my app. The current version however has an invalid CFBundleShortVersionString of 2.0.0-b2

    Alpha-numeric appendix is not allowed.

    Please update the info.plist so that the library can be directly used with Cocoapods/Carthage

    Apple states:

    Invalid or Non-Increasing CFBundleShortVersionString - The value specified in the bundle's Info.plist file for the key CFBundleShortVersionString must be a string consisting of at most three dot-separated components, where each component is composed only of the digits 0 through 9. For example, any of the following are syntactically valid values for CFBundleShortVersionString: "1.0", "4.2.1", "3.46", "1.112.0"; whereas the following are all syntactically invalid: "1.4.0.0.0.0.5", "GX5", "3.4.2b6", "2.6GM", "1.0 (Gold)", "-3.6". Additionally, each updated version of the same application must have a CFBundleShortVersionString that increases relative to that of the previous version that was actually made available for sale on the iTunes Store. For example, if a previously-available version had a CFBundleShortVersionString of "1.4", then any of the following would be acceptable as the next update: "1.4.1", "1.4.332", "1.5"; but all of the following (though syntactically valid) would be unacceptable: "1.4", "1.3", "1.3.9", "0.9". For more information about the CFBundleShortVersionString key and the Info.plist file, see Apple's Runtime Configuration Guidelines.

    opened by TobiasRe 1
  • Feature/swift package manager

    Feature/swift package manager

    Add support for Swift Package Manager. Builds and exports the "Gestalt" SPM library.

    To actually use it, you need a UI/AppKit enabled SPM, like SwiftXcode.

    opened by helje5 1
  • Migration from 1.x to 2.0.0

    Migration from 1.x to 2.0.0

    Using Gestalt 1.x, there are hundreds of files in my project have lines like this:

    ThemeManager.default.apply(theme: Theme.self, to: self) { themeable, theme in
        ...
    }
    

    But in 2.0.0, ThemeManager.default.apply(...) is gone, is there an easy way to not change every file in my project to migrate to 2.0.0? (eg. write an extension to ThemeManager to get this method back)

    opened by superk589 0
  • Request for Changelog

    Request for Changelog

    Hi there,

    I was wondering if it'd be possible to add a changelog so it's more easily visible what's changed between versions. I generally read between the commits, but it's hard to tell exactly what's changing between version 1.2.0 and 2.0.

    Thanks!

    opened by mergesort 0
Owner
Vincent Esche
Vincent Esche
Powerful animated gradientView in swift 🌈

FancyGradient is a UIView subclass which let's you animate gradients in your iOS app. It is purely written in Swift. Quickstart Static gradient let fa

Konstantinos Nikoloutsos 62 Jul 14, 2022
Color framework for Swift & Objective-C (Gradient colors, hexcode support, colors from images & more).

Swift 3 To use the Swift 3 version, add this to your Podfile (until 2.2 or higher is released): pod 'ChameleonFramework/Swift', :git => 'https://githu

Vicc Alexander 12.5k Dec 27, 2022
iOS app for Technex, IIT(BHU) Varanasi. This project is closed before completion. You can use this app for learning purpose. You can use this app as a templet of any event related app.

technex-ios iOS app for Technex, IIT(BHU) Varanasi. This project is closed before completion for some reasons. You can use this app for learning purpo

Jogendra 12 May 9, 2022
PrettyColors is a Swift library for styling and coloring text in the Terminal.

PrettyColors is a Swift library for styling and coloring text in the Terminal. The library outputs ANSI escape codes and conforms to ECMA Standard 48.

J.D. Healy 171 Aug 13, 2022
UIGradient - A simple and powerful library for using gradient layer, image, color

UIGradient is now available on CocoaPods. Simply add the following to your project Podfile, and you'll be good to go.

Đinh Quang Hiếu 247 Dec 1, 2022
A pure Swift library for using ANSI codes. Basically makes command-line coloring and styling very easy!

Colors A pure Swift library for using ANSI codes. Basically makes command-line coloring and styling very easy! Note: Colors master requires Xcode 7.3

Chad Scira 27 Jun 3, 2021
A pure Swift library that allows you to easily convert SwiftUI Colors to Hex String and vice versa.

iOS · macOS · watchOS · tvOS A pure Swift library that allows you to easily convert SwiftUI Colors to Hex String and vice versa. There is also support

Novem 3 Nov 20, 2022
System Color Picker - The macOS color picker as an app with more features

System Color Picker The macOS color picker as an app with more features Download Requires macOS 11 or later. Features Quickly copy, paste, and convert

Sindre Sorhus 758 Dec 24, 2022
An open-source colour picker app for macOS

An open-source colour picker app for macOS

Charlie Gleason 1.1k Dec 27, 2022
SheetyColors is an action sheet styled color picker for iOS

?? Based on UIAlertController: The SheetyColors API is based on UIKit's UIAlertController. Simply add buttons to it as you would for any other Action Sheet by defining UIAlertAction instances. Therefore, it nicely integrates with the look & feel of all other native system dialogs. However, you can also chose to use the color picker it self without an action sheet.

Christoph Wendt 102 Nov 10, 2022
A tool to calculate the color ratio of UIImage in iOS.

UIImageColorRatio A tool to calculate the color ratio of UIImage in iOS. How to use UIImageColorRatio Get the color ratio of UIImage. let image = ...

Yanni Wang 王氩 34 Jan 1, 2023
Colour blindness simulation and testing for iOS

Color Deficiency Snapshot Tests This package makes it easier for you to understand the implications of your app's design on users with various types o

James Sherlock 69 Sep 29, 2022
Conical (angular) gradient for iOS written in Swift

AEConicalGradient Conical (angular) gradient in Swift I hope that somebody will find this useful. And nice. Usage AEConicalGradient is a minion which

Marko Tadić 82 Dec 27, 2022
ImagePalette - Swift/iOS port of Android's Palette

ImagePalette - Swift/iOS port of Android's Palette

Shaun Harrison 54 Sep 23, 2022
A beautiful set of predefined colors and a set of color methods to make your iOS/OSX development life easier.

Installation Drag the included Colours.h and Colours.m files into your project. They are located in the top-level directory. You can see a demo of how

Ben Gordon 3.1k Dec 28, 2022
A powerful and easy to use live mesh gradient renderer for iOS.

MeshKit A powerful and easy to use live mesh gradient renderer for iOS. This project wouldn't be possible without the awesome work from Moving Parts a

Ethan Lipnik 51 Jan 1, 2023
An Adobe .ase (Adobe Swatch Exchange File), .aco (Photoshop swatch file) reader/writer package for Swift (macOS, iOS, tvOS, macCatalyst)

ColorPaletteCodable A palette reader/editor/writer package for iOS, macOS, watchOS and tvOS, supporting the following formats Adobe Swatch Exchange (.

Darren Ford 11 Nov 29, 2022
iTunes 11 Style Color Art Detection for iOS

iTunes 11 Style Color Art Detection for iOS. Original implementation from Fred Leitz Port of ColorArt code from OS X to iOS. Usage #include <ColorArt/

Vinh Nguyen 132 Dec 14, 2021
🔥 🔥 🔥Support for ORM operation,Customize the PQL syntax for quick queries,Support dynamic query,Secure thread protection mechanism,Support native operation,Support for XML configuration operations,Support compression, backup, porting MySQL, SQL Server operation,Support transaction operations.

?? ?? ??Support for ORM operation,Customize the PQL syntax for quick queries,Support dynamic query,Secure thread protection mechanism,Support native operation,Support for XML configuration operations,Support compression, backup, porting MySQL, SQL Server operation,Support transaction operations.

null 60 Dec 12, 2022
A light-weight UITextView subclass that adds support for placeholder.

RSKPlaceholderTextView A light-weight UITextView subclass that adds support for placeholder. Installation Using Swift Package Manager To add the RSKPl

Ruslan Skorb 220 Dec 17, 2022