Need help with Motif?
Click the “chat” button below for chat support from the developer who created it, or find similar developers for support.

About the developer

erichoracek
879 Stars 62 Forks MIT License 493 Commits 11 Opened issues

Description

Lightweight and customizable stylesheets for iOS

Services available

!
?

Need anything else?

Contributors list

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 *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
.

We use cookies. If you continue to browse the site, you agree to the use of cookies. For more information on our use of cookies please see our Privacy Policy.