Coder Social home page Coder Social logo

regexident / gestalt Goto Github PK

View Code? Open in Web Editor NEW
324.0 6.0 22.0 3.58 MB

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

License: Mozilla Public License 2.0

Ruby 13.19% Objective-C 1.01% Swift 85.79%
ios theme theme-switcher darkmode

gestalt's Introduction

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.

gestalt's People

Contributors

heilerich avatar helje5 avatar regexident avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

gestalt's Issues

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)

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??

UIAppearance not working.

I may be seeing things wrong but how does the UIAppearance work?

I get Type 'UINavigationBar' has no member 'apply'

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!

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:

#if os(iOS)
// HACK: apparently the only way to
// change the appearance of existing instances:
for window in UIApplication.shared.windows {
for view in window.subviews {
view.removeFromSuperview()
window.addSubview(view)
}
}
#endif

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?

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!

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

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!

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.

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

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.