GithubHelp home page GithubHelp logo

erichoracek / motif Goto Github PK

View Code? Open in Web Editor NEW
880.0 18.0 64.0 8.65 MB

Lightweight and customizable stylesheets for iOS

License: MIT License

Objective-C 98.28% Ruby 1.41% C++ 0.31%
objective-c stylesheet stylesheets theme style

motif's Introduction

Motif

Build Status Carthage compatible Coverage Status

Lightweight and customizable stylesheets for iOS

What can it do?

  • Declare rules defining your app's visual appearance separately from your UI component implementations to promote separation of concerns in your codebase
  • Dynamically change your app's appearance at runtime from a user setting, as an premium feature, or even from the screen's brightness like Tweetbot:

Brightness Theming

  • Reload your app's appearance when you edit your theme files (without rebuilding), saving loads of time when styling your interface:

Live Reloading

Why should I use it?

You have an app. Maybe even a family of apps. You know about CSS, and how it enables web developers to write a set of declarative classes to style elements throughout their site, creating composable interface definitions that are entirely divorced from page content. You'll admit that you're a little jealous that things aren't quite the same on iOS.

To style your app today, maybe you have a MyAppStyle singleton that vends styled interface components that's a dependency of nearly every view controller in your app. Maybe you use Apple's UIAppearance APIs, but you're limited to a frustratingly small subset of the appearance APIs. Maybe you've started to subclass some UIKit classes just to set a few defaults to create some styled components. You know this sucks, but there just isn't a better way to do things in iOS.

Well, things are about to change. Take a look at the example below to see what Motif can do for you:

An example

The following is a simple example of how you'd create a pair of styled buttons with Motif. To follow along, you can either continue reading below or clone this repo and run the ButtonsExample target within Motif.xcworkspace.

The design

Horizontal Layout

Your designer just sent over a spec outlining the style of a couple buttons in your app. Since you'll be using Motif to create these components, that means it's time to create a theme file.

Theme files

A theme file in Motif is just a simple dictionary encoded in a markup file (YAML or JSON). This dictionary can have two types of key/value pairs: classes or constants:

  • Classes: denoted by a leading period (e.g. .Button) and encoded as a key with a value of a nested dictionary, a class is a collection of named properties corresponding to values that together define the style of an element in your interface. Class property values can be of any type, or alternatively a reference to another class or constant.
.Class:
    property: value
  • Constants: denoted by a leading dollar sign (e.g. $RedColor) and encoded as a key-value pair, a constant is a named reference to a value. Constant values can be of any type, or alternatively a reference to another class or constant.
$Constant: value

To create the buttons from the design spec, we've written the following theme file:

Theme.yaml

$RedColor: '#f93d38'
$BlueColor: '#50b5ed'
$FontName: AvenirNext-Regular

.ButtonText:
    fontName: $FontName
    fontSize: 16

.Button:
    borderWidth: 1
    cornerRadius: 5
    contentEdgeInsets: [10, 20]
    tintColor: $BlueColor
    borderColor: $BlueColor
    titleText: .ButtonText

.WarningButton:
    _superclass: .Button
    tintColor: $RedColor
    borderColor: $RedColor

We've started by defining three constants: our button colors as $RedColor and $BlueColor, and our font name as $FontName. We choose to define these values as constants because we want to be able to reference them later in our theme file by their names—just like variables in Less or Sass.

We now declare our first class: .ButtonText. This class describes the attributes of the text within the button—both its font and size. From now on, we'll use this class to declare that we want text to have this specific style.

You'll notice that the value of the fontName property on the .ButtonText class is a reference to the $FontName constant we declared above. As you can probably guess, when we use this class, its font name will have the same value as the $FontName constant. This way, we're able to have one definitive place that our font name exists so we don't end up repeating ourselves.

The last class we declare is our .WarningButton class. This class is exactly like our previous classes except for one key difference: it inherits its properties from the .Button class via the _superclass directive. As such, when the .WarningButton class is applied to an interface component, it will have identical styling to that of .Button for all attributes except tintColor and borderColor, which it overrides to be $RedColor instead.

Property appliers

Next, we'll create the property appliers necessary to apply this theme to our interface elements. Most of the time, Motif is able to figure out how to apply your theme automatically by matching Motif property names to Objective-C property names. However, in the case of some properties, we have to declare how its value should be applied ourselves. To do this, we'll register our necessary theme property appliers in the load method of a few categories.

The first set of property appliers we've created is on UIView:

UIView+Theming.m

+ (void)load {
    [self
        mtf_registerThemeProperty:@"borderWidth"
        requiringValueOfClass:NSNumber.class
        applierBlock:^(NSNumber *width, UIView *view, NSError **error) {
            view.layer.borderWidth = width.floatValue;
            return YES;
        }];

    [self
        mtf_registerThemeProperty:@"borderColor"
        requiringValueOfClass:UIColor.class
        applierBlock:^(UIColor *color, UIView *view, NSError **error) {
            view.layer.borderColor = color.CGColor;
            return YES;
        }];
}

Here we've added two property appliers to UIView: one for borderWidth and another for borderColor. Since UIView doesn't have properties for these attributes, we need property appliers to teach Motif how to apply them to the underlying CALayer.

Applier type safety

If we want to ensure that we always apply property values a of specific type, we can specify that an applier requires a value of a certain class by using requiringValueOfClass: when registering an applier. In the case of the borderWidth property above, we require that its value is of class NSNumber. This way, if we ever accidentally provide a non-number value for a borderWidth property, a runtime exception will be thrown so that we can easily identify and fix our mistake.

Now that we've defined these two properties on UIView, they will be available to apply any other themes with the same properties in the future. As such, whenever we have another class wants to specify a borderColor or borderWidth to a UIView or any of its descendants, these appliers will be able to apply those values as well.

Next up, we're going to add an applier to UILabel to style our text:

UILabel+Theming.m

+ (void)load {
    [self
        mtf_registerThemeProperties:@[
            @"fontName",
            @"fontSize"
        ]
        requiringValuesOfType:@[
            NSString.class,
            NSNumber.class
        ]
        applierBlock:^(NSDictionary<NSString *, id> *properties, UILabel *label, NSError **error) {
            NSString *name = properties[ThemeProperties.fontName];
            CGFloat size = [properties[ThemeProperties.fontSize] floatValue];
            UIFont *font = [UIFont fontWithName:name size:size];

            if (font != nil) {
                label.font = font;
                return YES;
            }

            return [self
                mtf_populateApplierError:error
                withDescriptionFormat:@"Unable to create a font named %@ of size %@", name, @(size)];
        }];
}

Compound property appliers

The above applier is a compound property applier. This means that it's a property applier that requires multiple property values from the theme class for it to be applied. In this case, we need this because we can not create a UIFont object without having both the size and name of the font beforehand.

The final applier that we'll need is on UIButton to style its titleLabel:

UIButton+Theming.m

+ (void)load {
    [self
        mtf_registerThemeProperty:@"titleText"
        requiringValueOfClass:MTFThemeClass.class
        applierBlock:^(MTFThemeClass *class, UIButton *button, NSError **error) {
            return [class applyTo:button.titleLabel error:error];
        }];
}

Properties with MTFThemeClass values

Previously, we created a style applier on UILabel that allows us specify a custom font from a theme class. Thankfully, we're able to use this applier to style our UIButton's titleTabel as well. Since the titleText property on the .Button class is a reference to the .ButtonText class, we know that the value that comes through this applier will be of class MTFThemeClass. MTFThemeClass objects are used to represent classes like .TitleText from theme files, and are able to apply their properties to an object directly. As such, to apply the fontName and fontSize from the .TitleText class to our button's label, we simply invoke applyToObject: on titleLabel with the passed MTFThemeClass.

Putting it all together

NSError *error;
MTFTheme *theme = [MTFTheme themeFromFileNamed:@"Theme" error:&error];
NSAssert(theme != nil, @"Error loading theme %@", error);

[theme applyClassWithName:@"Button" to:saveButton error:NULL];
[theme applyClassWithName:@"WarningButton" to:deleteButton error:NULL];

We now have everything we need to style our buttons to match the spec. To do so, we must instantiate a MTFTheme object from our theme file to access our theme from our code. The best way to do this is to use themeFromFileNamed:, which works just like imageNamed:, but for MTFTheme instead of UIImage.

When we have our MTFTheme, we want to make sure that there were no errors parsing it. We can do so by asserting that our pass-by-reference error is still non-nil. By using NSAssert, we'll get a runtime crash when debugging informing us of our errors (if there were any).

To apply the classes from Theme.yaml to our buttons, all we have to do is invoke the application methods on MTFTheme on our buttons. When we do so, all of the properties from the theme classes are applied to our buttons in just one simple method invocation. This is the power of Motif.

Using Motif in your project

The best way to use Motif in your project is with either Carthage or CocoaPods.

Carthage

Add the following to your Cartfile:

github "EricHoracek/Motif"

CocoaPods

Add the following to your Podfile:

pod 'Motif'

Dynamic theming

One of Motif's most powerful features is its ability to dynamically theme your application's interface at runtime. While adopting dynamic theming is simple, it does require you to use Motif in a slightly different way from the above example.

For an example of dynamic theming in practice, clone this repo and run the DynamicThemingExample target within Motif.xcworkspace.

Dynamic theme appliers

To enable dynamic theming, where you would normally use the theme class application methods on MTFTheme for a single theme, you should instead use the identically-named methods on MTFDynamicThemeApplier when you wish to have more than one theme. This enables you to easily re-apply a new theme to your entire interface just by changing the theme property on your MTFDynamicThemeApplier.

// We're going to default to the light theme
MTFTheme *lightTheme = [MTFTheme themeFromFileNamed:@"LightTheme" error:NULL];

// Create a dynamic theme applier, which we will use to style all of our
// interface elements
MTFDynamicThemeApplier *applier = [[MTFDynamicThemeApplier alloc] initWithTheme:lightTheme];

// Style objects the same way as we did with an MTFTheme instance
[applier applyClassWithName:@"InterfaceElement" to:anInterfaceElement error:NULL];

Later on...

// It's time to switch to the dark theme
MTFTheme *darkTheme = [MTFTheme themeFromFileNamed:@"DarkTheme" error:NULL];

// We now change the applier's theme to the dark theme, which will automatically
// re-apply this new theme to all interface elements that previously had the
// light theme applied to them
[applier setTheme:darkTheme error:NULL];

"Mapping" themes

While you could maintain multiple sets of divergent theme files to create different themes for your app's interface, the preferred (and easiest) way to accomplish dynamic theming is to largely share the same set of theme files across your entire app. An easy way to do this is to create a set of "mapping" theme files that map your root constants from named values describing their appearance to named values describing their function. For example:

Colors.yaml

$RedDarkColor: '#fc3b2f'
$RedLightColor: '#fc8078'

DarkMappings.yaml

$WarningColor: $RedLightColor

LightMappings.yaml

$WarningColor: $RedDarkColor

Buttons.yaml

.Button:
    tintColor: $WarningColor

We've created a single constant, $WarningColor, that will change its value depending on the mapping theme file that we create our MTFTheme instances with. As discussed above, the constant name $WarningColor describes the function of the color, rather than the appearance (e.g. $RedColor). This redefinition of the same constant allows us to us to conditionally redefine what $WarningColor means depending on the theme we're using. As such, we don't have to worry about maintaining multiple definitions of the same .Button class, ensuring that we don't repeat ourselves and keep things clean.

With this pattern, creating our light and dark themes is as simple as:

MTFTheme *lightTheme = *theme = [MTFTheme
    themeFromFileNamed:@[
        @"Colors",
        @"LightMappings",
        @"Buttons"
    ] error:NULL];

MTFTheme *darkTheme = *theme = [MTFTheme
    themeFromFileNamed:@[
        @"Colors",
        @"DarkMappings",
        @"Buttons"
    ] error:NULL];

This way, we only have to create our theme classes one time, rather than once for each theme that we want to add to our app. For a more in-depth look at this pattern, clone this repo and read the source of the DynamicThemingExample target within Motif.xcworkspace.

Generating theme symbols

In the above examples, you might have noticed that Motif uses a lot of stringly types to bridge class and constant names from your theme files into your application code. If you've dealt with a system like this before (Core Data's entity and attribute names come to mind as an example), you know that over time, stringly-typed interfaces can become tedious to maintain. Thankfully, just as there exists Mogenerator to alleviate this problem when using Core Data, there also exists a similar solution for Motif to address this same problem: the Motif CLI.

Motif CLI

Motif ships with a command line interface that makes it easy to ensure that your code is always in sync with your theme files. Let's look at an example of how to use it in your app:

You have a simple YAML theme file, named Buttons.yaml:

$RedColor: '#f93d38'
$BlueColor: '#50b5ed'
$FontName: AvenirNext-Regular

.ButtonText:
    fontName: $FontName
    fontSize: 16

.Button:
    borderWidth: 1
    cornerRadius: 5
    contentEdgeInsets: [10, 20]
    tintColor: $BlueColor
    borderColor: $BlueColor
    titleText: .ButtonText

.WarningButton:
    _superclass: .Button
    tintColor: $RedColor
    borderColor: $RedColor

To generate theme symbols from this theme file, just run:

motif --theme Buttons.yaml

This will generate the a pair of files named ButtonSymbols.{h,m}. ButtonSymbols.h looks like this:

extern NSString * const ButtonsThemeName;

extern const struct ButtonsThemeConstantNames {
    __unsafe_unretained NSString *BlueColor;
    __unsafe_unretained NSString *FontName;
    __unsafe_unretained NSString *RedColor;
} ButtonsThemeConstantNames;

extern const struct ButtonsThemeClassNames {
    __unsafe_unretained NSString *Button;
    __unsafe_unretained NSString *ButtonText;
    __unsafe_unretained NSString *WarningButton;
} ButtonsThemeClassNames;

extern const struct ButtonsThemeProperties {
    __unsafe_unretained NSString *borderColor;
    __unsafe_unretained NSString *borderWidth;
    __unsafe_unretained NSString *contentEdgeInsets;
    __unsafe_unretained NSString *cornerRadius;
    __unsafe_unretained NSString *fontName;
    __unsafe_unretained NSString *fontSize;
    __unsafe_unretained NSString *tintColor;
    __unsafe_unretained NSString *titleText;
} ButtonsThemeProperties;

Now, when you add the above pair of files to your project, you can create and apply themes using these symbols instead:

#import "ButtonSymbols.h"

NSError *error;
MTFTheme *theme = [MTFTheme themeFromFileNamed:ButtonsThemeName error:&error];
NSAssert(theme != nil, @"Error loading theme %@", error);

[theme applyClassWithName:ButtonsThemeClassNames.Button to:saveButton error:NULL];
[theme applyClassWithName:ButtonsThemeClassNames.WarningButton to:deleteButton error:NULL];

As you can see, there's now no more stringly-typing in your application code. To delve further into an example of how to use the Motif CLI, check out any of the examples in Motif.xcworkspace.

Installation

To install the Motif CLI, simply build and run the MotifCLI target within Motif.xcworkspace. This will install the motif CLI to your /usr/local/bin directory.

As a "Run Script" build phase

To automate the symbols generation, just add the following to a run script build phase to your application. This script assumes that all of your theme files end in Theme.yaml, but you can modify this to your liking.

export PATH="$PATH:/usr/local/bin/"
export CLI_TOOL='motif'

which "${CLI_TOOL}"

if [ $? -ne 0  ]; then exit 0; fi

export THEMES_DIR="${SRCROOT}/${PRODUCT_NAME}"

find "${THEMES_DIR}" -name '*Theme.yaml' |  sed 's/^/-t /' | xargs "${CLI_TOOL}" -o "${THEMES_DIR}"

This will ensure that your symbols files are always up to date with your theme files. Just make sure the this run script build phase is before your "Compile Sources" build phase in your project. For an example of this in practice, check out any of the example projects within Motif.xcworkspace.

Live Reload

To enable live reloading, simply replace your MTFDynamicThemeApplier with an MTFLiveReloadThemeApplier when debugging on the iOS Simulator:

#if TARGET_IPHONE_SIMULATOR && defined(DEBUG)

self.themeApplier = [[MTFLiveReloadThemeApplier alloc]
    initWithTheme:theme
    sourceFile:__FILE__];

#else

self.themeApplier = [[MTFDynamicThemeApplier alloc]
    initWithTheme:theme];

#endif

Live reloading will only work on the iOS Simulator, as editable theme source files do not exist on your iOS Device. For a more in-depth look at how to implement live reloading, clone this repo and read the source of the DynamicThemingExample target within Motif.xcworkspace.

motif's People

Contributors

arjunmehta avatar ekurutepe avatar erichoracek avatar jlawton avatar sdkdimon 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  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

motif's Issues

motif-cli

Hello,

I'm really new at iOS development, so forgive me if I'm saying something noobish.

I've added motif pod to my PodFile and it installed as it should;
I need to generate my ThemeSymbols. So, after looking everywhere, I could not find motif-cli to generate it.
I'm getting: "-bash: motif: command not found"

Any thoughts?

Two points of contact

I'm looking at how a button is styled in the button example:

- (UIButton *)saveButton
{
    if (!_saveButton) {
        self.saveButton = ({
            UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
            [button aut_applyThemeClassWithName:ThemeClassNames.PrimaryButton fromTheme:self.aut_theme];
            [button setTitle:NSLocalizedString(@"Save", nil) forState:UIControlStateNormal];
            button;
        });
    }
    return _saveButton;
}

Especially, the line

[button aut_applyThemeClassWithName:ThemeClassNames.PrimaryButton fromTheme:self.aut_theme];

There are two parts of the library in use here. I usually consider that suspicious unless it's providing a clear point of flexibility. And even then, it's a good opportunity for a convenience method that "just works". So I suggest something like

[self.aut_theme applyClassName:ThemeClassNames.PrimaryButton toButton:button];

or

[self.aut_theme applyClassName:ThemeClassNames.PrimaryButton toObject:button];

But maybe applying a class shouldn't be the responsibility of a theme. I see there is an AUTThemeApplier object. So maybe this is just a problem with the button example.

But I don't see signatures in the theme applier that are what I would want:

self.themeApplier = [AUTThemeApplier applierWithTheme:AUTTheme];
...
[self.themeApplier applyClassName:ThemeClassNames.PrimaryButton toObject:button];

or

self.themeApplier = [AUTThemeApplier applierWithTheme:AUTTheme];
...
stylePrimaryButton = [AUTThemeApplier applierBlockForClassName:ThemeClassNames.PrimaryButton];
stylePrimaryButton(button);

What some of these methods that I'm looking for amount to a loose object-oriented method currying. I'll think about what kind of interface I'd like to use, and how I'd like to use it some more before going on about that...

custom mapping of properties in initialize() in swift

Hey :)

I am trying to to map ThemeProperty to a subclass of UIView in swift. My problem is that in the initialize() function the properties of that instance are not accessible yet.

The compiler complains: Instance member 'titleTextStyle' cannot be used on type 'Subclass'

What would be the best practise to deal with this ?

e.g
import UIKit
import Motif

extension Subclass{

override class func initialize() {
    super.initialize()

    guard self === Subclass.self else { return }

    self.mtf_registerThemeProperty(ThemeProperties.titleFontSize.rawValue) { titleFontSize in
      self.titleTextStyle =
     [
        NSFontAttributeName : UIFont.systemFontOfSize(titleFontSize),
        NSForegroundColorAttributeName : UIColor.whiteColor()
    ]
    }
}

}

Live reloading

  • Create a MTFDynamicThemeApplier subclass that live reloads whenever any of its MTFTheme JSON source files is edited

This theme applier should:

  • Locate the theme files with equivalent names in the source root of the project that the bundle was built from by using __FILE__, -[NSString stringByDeletingLastPathComponent], -[NSFileManager subpathsOfDirectoryAtPath:error:], and -[MTFTheme filenames]
  • Set up live reloading using a dispatch_source on all of the relevant theme files once they are discovered
  • Recreate its MTFTheme from these updated files using -[MTFTheme initWithJSONFiles:error:]

Apply theme to a class

Sorry for stupid question, but is it possible to apply theme for ex. for all UIView's? Like [[UIView appearance] setTheme...]

I'm asking because I can't figure out how to apple theme for IASKAppSettingsViewController for example... Or just for all my views in the whole app.

Thanks.

Memory leaks

Hello erichoracek,
I ran the demo (DynamicThemingExample) on iPhone5s(iOS 12.2),I see lots of memory leaks.
Examples of excerpts are as follows:

0 libsystem_malloc.dylib calloc
1 libobjc.A.dylib cache_t::reallocate(unsigned int, unsigned int)
2 libobjc.A.dylib cache_fill
3 libobjc.A.dylib lookUpImpOrForward
4 libobjc.A.dylib _objc_msgSend_uncached
5 Motif -[MTFThemeClass applyTo:error:] /data/UIDemos/Motif/Motif/Core/MTFThemeClass.m:188
6 Motif -[MTFTheme applyClassWithName:to:error:] /data/UIDemos/Motif/Motif/Core/MTFTheme.m:162
7 Motif -[MTFDynamicThemeApplier applyClassWithName:to:error:] /data/UIDemos/Motif/Motif/Core/MTFDynamicThemeApplier.m:78
8 Motif -[MTFDynamicThemeApplier applyTheme:toApplicants:error:] /data/UIDemos/Motif/Motif/Core/MTFDynamicThemeApplier.m:59
9 Motif -[MTFDynamicThemeApplier setTheme:error:] /data/UIDemos/Motif/Motif/Core/MTFDynamicThemeApplier.m:47
10 DynamicThemingExample -[AppDelegate toggleTheme] /data/UIDemos/Motif/Examples/DynamicThemingExample/AppDelegate.m:137
11 UIKitCore -[UIApplication sendAction:to:from:forEvent:]
12 UIKitCore __45-[_UIButtonBarTargetAction _invoke:forEvent:]_block_invoke
13 UIKitCore -[_UIButtonBarTargetAction _invoke:forEvent:]
14 UIKitCore -[UIApplication sendAction:to:from:forEvent:]
15 UIKitCore -[UIControl sendAction:to:forEvent:]
16 UIKitCore -[UIControl _sendActionsForEvents:withEvent:]
17 UIKitCore -[UIControl touchesEnded:withEvent:]
18 UIKitCore -[UIWindow _sendTouchesForEvent:]
19 UIKitCore -[UIWindow sendEvent:]
20 UIKitCore -[UIApplication sendEvent:]
21 UIKitCore __dispatchPreprocessedEventFromEventQueue
22 UIKitCore __handleEventQueueInternal
23 UIKitCore __handleHIDEventFetcherDrain
24 CoreFoundation CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION
25 CoreFoundation __CFRunLoopDoSource0
26 CoreFoundation __CFRunLoopDoSources0
27 CoreFoundation __CFRunLoopRun
28 CoreFoundation CFRunLoopRunSpecific
29 GraphicsServices GSEventRunModal
30 UIKitCore UIApplicationMain
31 DynamicThemingExample main /data/UIDemos/Motif/Examples/DynamicThemingExample/main.m:14
32 libdyld.dylib start

0 libsystem_malloc.dylib calloc
1 libobjc.A.dylib cache_t::reallocate(unsigned int, unsigned int)
2 libobjc.A.dylib cache_fill
3 libobjc.A.dylib lookupMethodInClassAndLoadCache
4 libobjc.A.dylib object_cxxDestructFromClass(objc_object*, objc_class*)
5 libobjc.A.dylib objc_destructInstance
6 libobjc.A.dylib object_dispose
7 libobjc.A.dylib -[NSObject release]
8 libobjc.A.dylib _object_set_associative_reference
9 Motif -[NSObject(ThemeClassName) mtf_setThemeClass:] /data/UIDemos/Motif/Motif/Core/NSObject+ThemeClass.m:52
10 Motif -[MTFThemeClass applyTo:error:] /data/UIDemos/Motif/Motif/Core/MTFThemeClass.m:294
11 DynamicThemingExample __25+[UIButton(Theming) load]_block_invoke /data/UIDemos/Motif/Examples/DynamicThemingExample/ThemingCategories/UIButton+Theming.m:24
12 Motif -[MTFThemeClassValueClassPropertyApplier applyClass:to:error:] /data/UIDemos/Motif/Motif/Core/MTFThemeClassPropertyApplier.m:89
13 Motif -[MTFThemeClass applyTo:error:] /data/UIDemos/Motif/Motif/Core/MTFThemeClass.m:73
14 Motif -[MTFThemeClass applyTo:error:] /data/UIDemos/Motif/Motif/Core/MTFThemeClass.m:203
15 Motif -[MTFTheme applyClassWithName:to:error:] /data/UIDemos/Motif/Motif/Core/MTFTheme.m:162
16 Motif -[MTFDynamicThemeApplier applyClassWithName:to:error:] /data/UIDemos/Motif/Motif/Core/MTFDynamicThemeApplier.m:78
17 Motif -[MTFDynamicThemeApplier applyTheme:toApplicants:error:] /data/UIDemos/Motif/Motif/Core/MTFDynamicThemeApplier.m:59
18 Motif -[MTFDynamicThemeApplier setTheme:error:] /data/UIDemos/Motif/Motif/Core/MTFDynamicThemeApplier.m:47
19 DynamicThemingExample -[AppDelegate toggleTheme] /data/UIDemos/Motif/Examples/DynamicThemingExample/AppDelegate.m:139
20 UIKitCore -[UIApplication sendAction:to:from:forEvent:]
21 UIKitCore __45-[_UIButtonBarTargetAction _invoke:forEvent:]_block_invoke
22 UIKitCore -[_UIButtonBarTargetAction _invoke:forEvent:]
23 UIKitCore -[UIApplication sendAction:to:from:forEvent:]
24 UIKitCore -[UIControl sendAction:to:forEvent:]
25 UIKitCore -[UIControl _sendActionsForEvents:withEvent:]
26 UIKitCore -[UIControl touchesEnded:withEvent:]
27 UIKitCore -[UIWindow _sendTouchesForEvent:]
28 UIKitCore -[UIWindow sendEvent:]
29 UIKitCore -[UIApplication sendEvent:]
30 UIKitCore __dispatchPreprocessedEventFromEventQueue
31 UIKitCore __handleEventQueueInternal
32 CoreFoundation CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION
33 CoreFoundation __CFRunLoopDoSource0
34 CoreFoundation __CFRunLoopDoSources0
35 CoreFoundation __CFRunLoopRun
36 CoreFoundation CFRunLoopRunSpecific
37 GraphicsServices GSEventRunModal
38 UIKitCore UIApplicationMain
39 DynamicThemingExample main /data/UIDemos/Motif/Examples/DynamicThemingExample/main.m:14
40 libdyld.dylib start

PS:I tested on iOS simulator, there is no memory leak on iOS 11.0.1 and iOS 12.0. The iOS simulator12.2 also has memory leaks.

Evaluate expressions in theme files

NSExpression would make it easy to evaluate expressions within a Motif theme file. Expressions that follow the = delimiter could be evaluated as if they were expressions, e.g. =<expression>, just like a spreadsheet. Some examples would be:

  • = $Constant1 - $Constant2
  • = $Constant * 2 - 30

Problem with imports in angle brackets

I installed the latest version of Motif (0.3.1) through cocoapods but I'm seeing the following error when building:

.../Pods/Motif/Carthage/Checkouts/libyaml/src/yaml_private.h:6:10: 'yaml.h' file not found with <angled> include; use "quotes" instead

This header is marked as Public in the produced framework, should it really be so? (the name seems to indicate otherwise :) )

The same thing also happens with the config.h import in the same file. If I change both to quotes as the error message suggests, it works.

Add support for YAML themes in addition to JSON

Benefits:

  • Only occasionally need to quote things
  • Allows comments
  • Way more readable

Drawbacks:

$BlueColor: "#50b5ed"
$FontName: AvenirNext-Regular
$RedColor: "#f93d38"
$WhiteColor: "#f1efeb"

# This is the root button class
.Button:
    borderColor: $BlueColor
    borderWidth: 1
    contentEdgeInsets: "{10, 20, 10, 20}"
    cornerRadius: 5
    tintColor: $BlueColor
    titleText: .ButtonText

.ButtonText:
    fontName: $FontName
    fontSize: 16

.ButtonsView:
    _superclass: .ContentBackground
    deleteButton: .WarningButton
    saveButton: .Button

.ContentBackground:
    backgroundColor: $WhiteColor

.WarningButton:
    _superclass: .Button
    borderColor: $RedColor
    tintColor: $RedColor

Incorrect path to code coverage files when executing test suite

When we execute our test suite built against Motif, we are seeing the following output: (I'm assuming this is the path to the code coverage files located on your workstation).

profiling: /Users/erh/Library/Developer/Xcode/DerivedData/Motif-fymrucbnitjyghdhgxaviyllghzs/Build/Intermediates/Motif.build/Release-iphonesimulator/Motif-iOS.build/Objects-normal/x86_64/NSURL+LastPathComponentWithoutExtension.gcda: cannot open: No such file or directory profiling: /Users/erh/Library/Developer/Xcode/DerivedData/Motif-fymrucbnitjyghdhgxaviyllghzs/Build/Intermediates/Motif.build/Release-iphonesimulator/Motif-iOS.build/Objects-normal/x86_64/NSURL+LastPathComponentWithoutExtension.gcda: cannot open: No such file or directory profiling: /Users/erh/Library/Developer/Xcode/DerivedData/Motif-fymrucbnitjyghdhgxaviyllghzs/Build/Intermediates/Motif.build/Release-iphonesimulator/Motif-iOS.build/Objects-normal/x86_64/MTFScreenBrightnessThemeApplier.gcda: cannot open: No such file or directory profiling: /Users/erh/Library/Developer/Xcode/DerivedData/Motif-fymrucbnitjyghdhgxaviyllghzs/Build/Intermediates/Motif.build/Release-iphonesimulator/Motif-iOS.build/Objects-normal/x86_64/MTFScreenBrightnessThemeApplier.gcda: cannot open: No such file or directory

---- (rest left out in the interest of brevity)

Naming

/cc @fcanas

I'm trying to think of the best naming scheme for the project. Right now it's the following:

  • AUTTheme Container of:
    • AUTThemeConstant - Named constant mapped to values
    • AUTThemeClass - Named container that maps named properties to values (The property → value mappings are internally instances of AUTThemeConstant)
  • AUTThemeApplier Responsible for applying a theme to objects, via the applyClassWithName:toObject: method
  • <AUTThemeClassApplicable> Protocol defining a generic interface for applying a AUTThemeClass to an object. A set of these are used internally by AUTThemeApplier to apply a theme to an object
    • AUTThemeClassApplier Applier invoked when a class is applied to an object
    • AUTThemeClassPropertyApplier Applier invoked with a specific property is applied to an object
    • AUTThemeClassPropertiesApplier Applier invoked when a set of properties are applied to an object

Swift: UIEdgeInsets property applier

How do I add property applier for UIEdgeInsets in Swift? If I understood correctly, Objective-C version of property applier for UIEdgeInsets would be something like this:

[self mtf_registerThemeProperty:@"titleEdgeInsets" requiringValueOfObjCType:@encode(UIEdgeInsets) applierBlock:^(NSValue *value, UIButton *button) {
        button.titleEdgeInsets = value.UIEdgeInsetsValue;
}];

But Swift doesn't have @encode compiler directive.

Issue building Motif with Xcode 12

We are currently importing Motif 0.3.8 via Carthage
When building with Xcode 12, there is a compilation error on line 434 and line 436 of UIColor+HTMLColors.m

{
    float f = 0.0;
    if ([self scanFloat:&f]) {
        if ([self scanString:@"%" intoString:NULL]) {
            f *= 0.01;
        } else {
            f *= scale;
        }
        if (value) {
            *value = f;
        }
        return YES;
    }
    return NO;
}

Compilation error: Implicit conversion when assigning computation result loses floating-point precision: 'double' to 'float'

Mutability

/cc @fcanas

I'm unhappy with the current state of mutability in the project

  • AUTTheme should be entirely immutable to consumers. I want to think of a good syntax for initializing a theme from a set of JSON files that isn't cumbersome to use but is able to provide errors as well. Basically addAttributesFromThemeAtURL:error: should not exist in the public interface, and it instead should occur on an initWith-stye initialization.

Chameleon Support ?

Thanks for the great library ... Any guidance on how this could integrate with Flat Color frameworks like Chameleon ? is it even possible ?

Thanks

Conform MTFTheme and MTFDynamicThemeApplier to a protocol with shared methods

This will allow consumers to only pass around objects that conform to this protocol, rather than typed references to one of them. As such, users will be able to swap out a MTFTheme with an MTFDynamicThemeApplier (or vice versa) to quickly convert code to use dynamic theming or a single theme with much less work.

Convenience applier categories would be appreciated

Support for properties could be included with Motif.

Example transformers from the readme would be really useful if they were default for UIKit classes:

+ (void)load
{
    [self
        mtf_registerThemeProperty:@"textColor"
        valueTransformerName:MTFColorFromStringTransformer
        applier:^(UIColor *textColor, UIButton *button) {
            [button setTextColor:textColor forState:UIControlStateNormal];
    }];

    [self
        mtf_registerThemeProperties:@[
            @"fontName",
            @"fontSize"
        ] valueTransformersNamesOrRequiredClasses:@[
            [NSString class],
            [NSNumber class]
        ] applier:^(NSDictionary *properties, UIButton *button) {
            NSString *name = properties[@"fontName"];
            CGFloat size = [properties[@"fontSize"] floatValue];
            button.titleLabel.font = [UIFont fontWithName:name size:size];
        }];
}

I can imaging it as an optional subspec in CocoaPods so that it can be more easily platform-specific, and even ignored. I'm not sure what it would look like via Carthage (a separate framework?).

What do you think?

Default classes + defining class from storyboard

Great work on this library!

There are two features which would be particularly useful for me and that I'd like to tackle, but I just wanted to confirm if they are not already supported or have been considered and discarded:

  • The ability to specify a default class for a specific UIView subclass (all UIButtons get the "DefaultButton" class by default, for instance).
  • The ability to specify the class in the storyboard file through an @IBInspectable property.

The second one would require a way for the user to set a default global theme beforehand.

Also, a big downside is that these would definitely involve swizzling, so it would be best to leave them in a subspec or something.

What do you think? If you like the idea I'll get started right away and submit a PR when done.

Allow overriding of existing constants with identical names by registering new constants.

Great work on the framework. We just started using Motif and really like it. When coming up with internal standards on how best to adopt it we ran into a scenario where we would like the ability to define constants and have the ability to override as needed.

We need the ability to switch colors for a few items at startup depending on a customer preference.
Our original idea was to use load 2 different themes (lets call it Theme A and Theme B).

Theme A (loads classes and constants from A.json)
Theme B (loads A.json and B.json where B.json contains overrides of constant values)

We were then going to use the MTFDynamicThemeApplier to switch between the two.

We see that the overriding of existing constants and classes is prohibited in the MTFThemeParser and generates a parsing error if attempted.

We would love the ability to do it (as described in our above scenario) maybe through a configurable property (so as not to impact current users).

Would y'all be opposed to to this idea? If not, we are happy to submit a pull request for this.

Edit theme on the phone

Its possible to edit the themes on the phone? I've a project that requires that the user can customise some UI elements and will be cool if its possible to do with Motif.

Recommended approach for styling tableview cells

Hi Eric, very impressive library; the stylesheets are particularly clean and elegant.

I'm looking at using Motif in a project and trying to determine the best way to style UITableViewCells. Before I reinvent the wheel poorly, I thought I'd see if you had any recommendations. I'm sure you've used Motif in projects with table views.

I've prototyped an applier for UITableViewController that sets the appearance of cells it contains. This works thus far, but it feels like I could be going down the wrong path. I need to develop solution(s) that handle both dynamic and static table views.

You've eschewed using UIAppearance, and I'd guess that's because you've had problems with it (it definitely has its quirks). Almost feels like Motif is partially a means to avoid UIAppearance 😉

Thanks!

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.