Motif 0.3.8

Motif 0.3.8

测试已测试
语言语言 CC
许可证 MIT
发布最新发布2017 年 12 月

Eric HoracekEric Horacek 维护。



Motif 0.3.8

  • 作者:
  • Eric Horacek

Motif

Build Status Carthage compatible Coverage Status

iOS 轻量级且可定制的样式表

它能做什么?

  • 独立于您的 UI 组件实现,声明定义您的应用程序外观的规则,以在您的代码库中促进关注点的分离
  • 根据用户设置、高级功能或甚至屏幕亮度(例如 Tweetbot)动态更改应用程序的外观

Brightness Theming

  • 当您编辑主题文件时重新加载应用程序的外观 (无需重新构建),在布置界面时节省大量时间

Live Reloading

为什么我应该使用它?

您有一个应用程序。也许甚至是一系列应用程序。您了解 CSS,以及它是如何使网络开发人员能够编写一组声明性类来装饰其网站中的元素,从而创建与页面内容完全分离的可组合界面定义。您会承认您有点嫉妒在 iOS 上事情并不是完全相同。

要装饰您今天的应用程序,您可能有一个 MyAppStyle 单例,它提供装饰过的界面组件,并且几乎成为您的应用程序中每个视图控制器的依赖项。也许您使用了 Apple 的 UIAppearance API,但您限于非常小的外观 API 的子集。也许您已经开始对 UIKit 类进行子类化,只是为了设置一些默认值以创建一些装饰过的组件。您知道这很糟糕,但在 iOS 中没有更好的做事方式。

好吧,事情即将改变。请看下面的示例,看看 Motif 可以为您做些什么

示例

以下是一个使用 Motif 创建一对装饰按钮的简单示例。要继续阅读,您可以继续阅读下面或克隆此仓库,然后在 Motif.xcworkspace 中运行 ButtonsExample 目标。

设计

Horizontal Layout

您的设计师刚刚发送了一个关于应用程序中几个按钮样式的规范。由于您将使用 Motif 来创建这些组件,这意味着是时候创建一个主题文件了。

主题文件

Motif 中的主题文件只是一种简单的以标记文件(YAML 或 JSON)编码的字典。此字典可以包含两种类型的键/值对:常量

  • 类:以句点开头(例如 .Button),编码为键,带有值为嵌套字典的值,类是一组命名的属性,对应于一起定义您的界面中元素风格的值。类属性的值可以是任何类型,或者是另一个类或常量的引用。
.Class:
    property: value
  • 常量:以美元符号为前缀(例如:$RedColor)并以键值对形式编码,常量是对值的命名引用。常量值可以是任何类型,或者指向前一个类或常量的引用。
$Constant: value

为了创建设计规范中的按钮,我们已经编写了以下主题文件

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

我们首先定义了三个常量:按钮颜色为$RedColor$BlueColor,字体名称为$FontName。我们选择将这些值定义为常量,因为我们要能够在我们的主题文件中通过其名称引用它们——就像Less或Sass中的变量。

我们现在声明第一个类:.ButtonText。该类描述了按钮中文字的属性——包括其字体和大小。从现在起,我们将使用这个类来声明我们希望文本拥有这种特定的样式。

您会注意到.ButtonText类中的fontName属性的值是对之前声明的$FontName常量的引用。正如您可以猜到的,当我们使用这个类时,它的字体名称将具有与$FontName常量相同的值。这样,我们就能有一个明确的字体名称存在的地方,避免了重复。

最后一个声明的类是.WarningButton类。这个类与之前的类完全相同,只有一个关键的区别:它通过_superclass指令从.Button类继承其属性。因此,当.WarningButton类应用于接口组件时,它将具有与.Button类相同的样式,除了tintColorborderColor,这些属性它覆盖以$RedColor代替。

属性应用器

接下来,我们将创建应用此主题到我们接口元素所必需的属性应用器。大多数情况下,Motif能够通过将Motif属性名称与Objective-C属性名称相匹配来自动确定如何应用您的主题。然而,在某些属性的情况下,我们必须自己声明它们的值应该如何应用。为此,我们将在一些分类的load方法中注册我们的必要主题属性应用器。

我们创建的第一组属性应用器是在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;
        }];
}

在这里,我们在UIView上添加了两个属性应用器:一个用于borderWidth,另一个用于borderColor。由于UIView没有这些属性的属性,我们需要属性应用器来教Motif如何将它们应用到底层的CALayer上。

应用器类型安全性

如果我们想确保我们始终将属性值应用于特定类型,我们可以在注册应用器时使用requiringValueOfClass:来指定应用器需要一定类别的值。在上述borderWidth属性的情况下,我们需要其值为NSNumber类。这样,如果我们意外地为borderWidth属性提供一个非数字值,将会抛出一个运行时异常,这样我们就可以轻松地识别并修复错误。

现在,我们在UIView上定义了这两个属性,它们将在未来对具有相同属性的其他主题可用。因此,每当另一个类想在UIView或其任何子类上指定一个borderColorborderWidth时,这些应用器也将能够应用这些值。

接下来,我们将在UILabel上添加一个应用器以风格化文本

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)];
        }];
}

复合属性应用器

上述应用器是一个复合属性应用器。这意味着它需要一个主题类的多个属性值来应用。在这种情况下,我们之所以需要这样做,是因为我们无法在没有预先拥有字体大小和名称的情况下创建UIFont对象。

我们需要最后的将在UIButton上应用以美化其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];
        }];
}

具有MTFThemeClass值的属性

之前,我们在UILabel上创建了一个样式应用器,允许我们从主题类指定自定义字体。幸好事后我们能够使用这个应用器来美化我们的UIButtontitleTabel。因为.Button类的titleText属性是一个对.ButtonText类的引用,我们知道通过这个应用器传入的值将属于MTFThemeClass类。MTFThemeClass对象用于表示从主题文件中像.TitleText这样的类,并且能够直接将它们的属性应用到对象上。因此,要应用来自.TitleText类的fontNamefontSize到我们的按钮的标签上,我们只需在传递的MTFThemeClass上对titleLabel调用applyToObject:

整合一切

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];

我们现在有风格的按钮以匹配规范所需的一切。为此,我们必须从主题文件实例化一个MTFTheme对象以从我们的代码中访问我们的主题。最佳做法是使用themeFromFileNamed:,它就像imageNamed:一样工作,但是用于MTFTheme而不是UIImage

当我们拥有我们的MTFTheme时,我们想确保没有错误解析它。我们可以通过断言我们的按引用error仍非nil来实现。通过使用NSAssert,当我们在调试中时会收到有关我们错误的运行时崩溃通知(如有错误的话)。

要将Theme.yaml中的类应用到我们的按钮上,我们只需在按钮上调用MTFTheme的应用方法。当我们这样做时,所有主题类的属性都会应用到一个简单的调用中。这就是Motif的核心。

在您的项目中使用Motif

在您的项目中使用Motif的最佳方式是使用CarthageCocoaPods

Carthage

将以下内容添加到您的Cartfile

github "EricHoracek/Motif"

CocoaPods

将以下内容添加到您的Podfile

pod 'Motif'

动态主题

Motif最强大的功能之一是它能够在运行时动态地为主题应用程序的用户界面。虽然采用动态主题很简单,但这确实需要您以不同于上述示例的方式使用Motif。

关于实际应用动态主题的示例,请克隆此存储库,然后在Motif.xcworkspace中运行DynamicThemingExample目标。

动态主题应用器

要启用动态主题,您通常会使用MTFTheme上的主题类应用方法应用于单个主题,但在您希望有多个主题时,您应使用相同名称的MTFDynamicThemeApplier上的方法。这样可以轻松地将新的主题重新应用到整个界面,只需更改MTFDynamicThemeApplier上的theme属性即可。

// 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];

随后...

// 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];

“映射”主题

虽然您可以通过维护多套不同的主题文件来为应用程序界面创建不同的主题,但实现动态主题的最佳(也是最简单)方法是通过整个应用程序大量共享相同的主题文件集合。实现这一点的一个简单方法是创建一组所谓的“映射”主题文件,这些文件将您的根常量从描述其外观的命名值映射到描述其功能的命名值。例如

Colors.yaml

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

DarkMappings.yaml

$WarningColor: $RedLightColor

LightMappings.yaml

$WarningColor: $RedDarkColor

Buttons.yaml

.Button:
    tintColor: $WarningColor

我们创建了一个单个常量,$WarningColor,其值将根据我们用其创建MTFTheme实例的映射主题文件而变化。如上所述,常量名称$WarningColor描述了该颜色的功能,而不是其外观(例如,$RedColor)。这种相同常量的重新定义使我们能够基于所使用的主题有条件地重定义$WarningColor的含义。因此,我们不必担心维护多个相同.Button类的定义,确保我们不重复自己,并保持代码整洁。

采用这种模式,创建浅色和深色主题就像这样

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

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

这样,我们只需为应用程序中要添加的每个主题创建一次主题类,而不是为每个主题都创建一次。要深入了解这种模式,请克隆此存储库,并阅读Motif.xcworkspaceDynamicThemingExample目标的源代码。

生成主题符号

在上例中,您可能已经注意到,Motif使用了很多字符串类型来将类和常量名称从您的主题文件桥接到应用程序代码中。如果您以前处理过类似系统(例如Core Data的实体和属性名称),您知道随着时间的推移,字符串类型接口会变得难以维护。幸运的是,正如存在Mogenerator来在处理Core Data时缓解此问题一样,也存在类似解决方案来解决Motif中相同的问题:Motif CLI。

Motif CLI

Motif附带了命令行界面,该界面可以轻松确保您的代码始终与主题文件同步。让我们看看如何在您的应用程序中使用它的一个例子

您有一个简单的YAML主题文件,命名为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

要从此主题文件生成主题符号,只需运行以下命令:

motif --theme Buttons.yaml

这将生成名为ButtonSymbols.{h,m}的一对文件。ButtonSymbols.h看起来是这样的:

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;

现在,当您将上述一对文件添加到项目中后,您可以使用这些符号创建和应用主题:

#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];

如您所见,应用程序代码中现在不再需要字符串类型。要深入了解如何使用Motif CLI的示例,请查看Motif.xcworkspace中的任何示例。

安装

要安装Motif CLI,只需在Motif.xcworkspace中构建并运行MotifCLI目标。这将安装motif CLI到您的/usr/local/bin目录。

作为“运行脚本”构建阶段

要自动化符号生成,请在应用程序的运行脚本构建阶段中添加以下脚本。此脚本假定所有主题文件都以Theme.yaml结尾,但您可以根据需要对其进行修改。

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}"

这将确保您的符号文件始终与主题文件同步。只需确保此运行脚本构建阶段位于项目中“编译源”构建阶段之前即可。要看到实际示例,请查看Motif.xcworkspace中的任何示例项目。

实时刷新

要启用实时刷新,请在iOS模拟器上进行调试时,只需将您当前的MTFDynamicThemeApplier替换为MTFLiveReloadThemeApplier即可。

#if TARGET_IPHONE_SIMULATOR && defined(DEBUG)

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

#else

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

#endif

实时重新加载只能在iOS模拟器上工作,因为您的iOS设备上不存在可编辑的主题源文件。要深入了解如何实现实时重新加载,请克隆此仓库并阅读位于Motif.xcworkspace中的DynamicThemingExample目标的源代码。