布局 0.7.0

布局 0.7.0

测试测试过
语言语言 SwiftSwift
许可 MIT
发布最后发布2019年10月
SPM支持 SPM

Nick Lockwood 维护。



布局 0.7.0

  • 作者:
  • Nick Lockwood

Travis Coveralls Platform Swift Swift License CocoaPods Compatible Carthage Compatible

Layout

Layout 是一个本机的 Swift 框架,用于使用 XML 模板文件和运行时评估表达式实现 iOS 用户界面。它被设计为 Nibs 和 Storyboards 的替代品,但提供了一些优点,如人类可读的模板和实时编辑。

Screenshot

简介

为什么?

布局试图解决许多问题,这些问题使得Storyboard不适用于大型、协作的项目,包括

  • 专有且未文档化的格式
  • 较差的可组合性和可重用性
  • 难以应用常见的样式元素和度量值,而无需复制粘贴
  • 难以被人阅读,因此难以解决合并冲突
  • 有限的所见即所得功能

布局还包括AutoLayout的替代品,旨在

  • 对基本布局更加简单易用
  • 对复杂布局更直观易懂
  • 更确定性和易于调试
  • 性能更高(至少理论上是这样 :-))

要了解更多关于我们为什么构建布局以及它解决哪些问题,请查阅这篇文章

如何?

布局引入了一种新的节点层次结构来管理视图,类似于React Native使用的"虚拟DOM"。

与使用NSCoding进行序列化的UIView不同,布局节点可以从不轻量级、可读的XML格式中反序列化,并且还提供了简洁的API,用于在代码中 programmatically 生成视图布局,而不想使用单独的资源文件。

使用表达式指定视图属性,这些表达式是存储为字符串的纯函数,在运行时评估。现在,我知道你在想什么 - 字符串类型代码很糟糕! - 但布局的表达式是强类型的,并且旨在尽早失败,提供详细的错误消息来帮助您进行调试。

布局旨在与普通UIKit组件一起工作,而不仅仅是替换或重新发明它们。基于布局的视图可以嵌入到Nib和Storyboard中,以及基于Nib和Storyboard的视图可以嵌入到基于布局的视图和控制器的视图中,因此如果您想尝试使用布局,无需重写整个应用程序。

用法

安装

布局以独立的Swift框架的形式提供,您可以在您的应用程序中使用它。它与Swift 3.2和4.0兼容,且不绑定到任何特定的包管理解决方案。

要使用CocoaPods安装Layout,请将以下内容添加到您的Podfile中

pod 'Layout', '~> 0.6'

要使用Carthage安装,请在Cartfile中添加以下内容

github "schibsted/Layout" ~> 0.6

依赖项

Layout没有外部依赖。它内部使用ExpressionSprinter框架,但这些框架作为源分发的一部分已包含在Layout模块中,因此不需要单独包含它们。

由于Expression和Sprinter在Layout命名空间内,因此您可以在已使用这些框架之一的另一个副本的项目中安全地使用Layout。

整合

Layout公开的主要API是LayoutNode类。创建布局节点的方式如下

let node = LayoutNode(
    view: UIView.self,
    expressions: [
        "width": "100%",
        "height": "100%",
        "backgroundColor": "#fff",
    ],
    children: [
        LayoutNode(
            view: UILabel.self,
            expressions: [
                "width": "100%",
                "top": "50% - height / 2",
                "textAlignment": "center",
                "font": "Courier bold 30",
                "text": "Hello World",
            ]
        )
    ]
)

此示例代码在具有白色背景的UIView内创建了一个居中的UILabel,在挂载后将根据其父视图展开。

对于简单视图,直接在代码中创建布局是方便的解决方案,可以避免需要外部文件。但Layout框架的真正力量来自能够使用外部XML文件指定布局的能力,因为它允许实时重新加载,这可以显著减少开发时间。

上面布局的等效XML标记为

<UIView
    width="100%"
    height="100%"
    backgroundColor="#fff">

    <UILabel
        width="100%"
        top="50% - height / 2"
        textAlignment="center"
        font="Courier bold 30"
        text="Hello World"
    />
</UIView>

大多数内置iOS视图在与LR2一起使用作为LR XML元素时应该可以正常工作。对于自定义视图,您可能需要做出一些小的改动以实现完整的LR兼容性。有关详细信息,请参阅下面的自定义组件部分。

要在视图或视图控制器中挂载LayoutNode,最简单的方法是创建一个UIViewController子类并添加LayoutLoading协议。然后,您可以使用以下三种选项中的任何一种来加载您的布局

class MyViewController: UIViewController, LayoutLoading {

    public override func viewDidLoad() {
        super.viewDidLoad()

        // Option 1 - create a layout programmatically
        self.layoutNode = LayoutNode( ... )

        // Option 2 - load a layout synchronously from a bundled XML file
        self.loadLayout(named: ... )

        // Option 3 - load a layout asynchronously from an XML file URL
        self.loadLayout(withContentsOfURL: ... ) { error in
            ...   
        }
    }
}

对于在代码中生成的布局,请使用选项1。对于位于应用程序资源包内的XML布局文件,请使用选项2。

选项3可以用于从任意URL加载布局,这可以是本地文件或远程托管。如果您需要在设备上直接开发,这很有用,因为您可以将布局文件托管在您的Mac上,然后从设备连接到它以允许在不重新编译应用程序的情况下重新加载更改。它还可以在生产中对某些类型的CMS系统中托管布局提供潜在用途。

注意: loadLayout(withContentsOfURL:)方法在缓存等方面提供的控制有限,因此如果您打算远程托管布局文件,可能最好先将其下载到本地缓存位置,然后再从那里加载它。

编辑器支持

您可以直接在Xcode中编辑布局XML文件,但可能没有视图属性的自动完成功能。目前Xcode中没有提供自动完成支持,但布局现在支持流行的Sublime Text编辑器。

在Sublime Text中安装Layout自动完成

  1. 转到《首选项 > 浏览包…,这将在Finder中打开Package目录
  2. 将Layout存储库中的layout.sublime-completions文件复制到Packages/User

Standard UIKit视图、视图控制器和属性的自动完成现在将可用,用于在Sublime Text中编辑的xml文件。

目前没有自动生成自定义视图或属性自动完成建议的方法,但您可以手动将这些添加到layout.sublime-completions文件中。

我们希望在未来添加对其他编辑器的支持。如果您想为此贡献力量,请在GitHub上创建问题进行讨论。

实时重载

布局提供了一些有助于提高您开发生产力的功能,其中最著名的是红框调试器实时重载功能。

当你在iOS模拟器中加载XML布局文件时,布局框架将试图找到布局的原始源XML文件,并加载该文件,而不是加载打包进编译应用程序的静态版本。

这意味着你可以修改你的XML文件并重新加载它而不需要重新编译应用程序或重启模拟器。

注意:如果有多个源文件与打包的文件名匹配,你将需要选择要加载的文件。如果需要从搜索过程中排除某些文件,请参阅下面的忽略文件部分。

您可以通过在模拟器中按Cmd-R来随时重新加载您的XML文件(不是在Xcode本身,因为这将会重新编译应用程序)。布局将检测到这个键组合,并重新加载XML。

注意:这仅适用于您对布局XML文件或Localizable.strings文件所做的更改,不适用于您的视图控制器中的Swift代码更改或其他资源,如图像。

实时重载功能和优雅的错误处理意味着,你可以在不需要重新编译应用程序的情况下进行绝大多数界面开发。

调试

如果在XML解析、安装或更新过程中布局框架抛出错误,则会显示红色框,这是一个全屏叠加层,显示错误消息和重新加载按钮。

对于非关键错误(例如,使用过时的API)布局将在屏幕底部显示黄色警告栏,可以通过点击来忽略。

多亏了实时重新加载功能,许多错误(例如语法错误或命名属性错误)可以在不重新编译应用程序的情况下修复。一旦修复了错误,按下重新加载(或Cmd-R),将消除任何警告或错误并重新加载布局XML文件。

红色框界面由LayoutConsole单例管理。这公开了用于显示和隐藏控制台的静态方法,以及一个isEnabled属性,可以程序化地启用或禁用控制台。默认情况下,控制台在调试构建时启用,在发布时禁用,但您可以在运行时覆盖此设置。

如果禁用了LayoutConsole,错误和警告将打印到Xcode控制台。

常量

静态XML非常好,但大多数应用程序内容都是动态的。字符串、图像甚至布局本身都需要根据用户生成的内容、当前区域设置等在运行时更改。

LayoutNode提供了两种机制将动态数据传递到布局中,然后可以在表达式内部引用它们:常量状态

常量——顾名思义——是在LayoutNode生命周期内保持恒定值的值。这些值不需要在应用程序的生命周期内保持恒定,但更改它们意味着从头开始重新创建LayoutNode及其关联的视图层次结构。常量字典传递给LayoutNode初始化器,并可以被该节点或其任何子节点中的任何表达式引用。

常量的良好用途是本地化字符串,或者像应用程序UI主题中使用的颜色或字体这样的东西。这些都是在其应用程序生命周期中几乎(或很少)改变的事情,所以如果必须为了重置它们而拆除视图层次结构,这是可以接受的。

以下是您如何将一些常量传递到基于XML的布局中

loadLayout(
    named: "MyLayout.xml",
    constants: [
        "title": NSLocalizedString("homescreen.title", message: ""),
        "titleColor": UIColor.primaryThemeColor,
        "titleFont": UIFont.systemFont(ofSize: 30),
    ]
)

以及您如何在XML中引用它们

<UIView ... >
    <UILabel
        width="100%"
        textColor="titleColor"
        font="{titleFont}"
        text="{title}"
    />
</UIView>

您可能会注意到,titletitleFont常量被{...}花括号包围,但titleColor常量没有。这将在下面的字符串字体子部分中解释。

您可能会发现某些常量是应用程序中每个布局共有的,例如如果您的常量代表标准间距度量、字体或颜色。在默认情况下,重复这些内容可能会很烦人,但Swift(截至版本3.0)缺乏合并字典的方便方法,这使得使用静态常用常量字典变得痛苦。

因此,LayoutNode's初始化器的constants参数实际上是变长参数,允许您传递多个字典,这些字典将自动合并。这使将全局常量字典与一些自定义值组合更加愉快。

let extraConstants: [String: Any] = [
    ...
]

loadLayout(
    named: "MyLayout.xml",
    constants: globalConstants, extraConstants, [
        "title": NSLocalizedString("homescreen.title", message: ""),
        "titleColor": UIColor.primaryThemeColor,
        "titleFont": UIFont.systemFont(ofSize: 30),
    ]
)

状态

对于更动态的布局,您可能有一些需要频繁更改的属性(或许甚至在动画过程中),重新创建整个视图层次结构来更改这些属性并不高效。对于这些属性,您可以使用状态。状态与常数的工作方式非常相似,不过您可以在LayoutNode初始化后更新状态。

loadLayout(
    named: "MyLayout.xml",
    state: [
        "isSelected": false,
        ...
    ],
    constants: [
        "title": ...
    ]
)

func setSelected() {
    self.layoutNode?.setState([
        "isSelected": true
    ])
}

请注意,您可以在同一个布局中使用常数和状态。如果一个状态变量的名称与一个常量相同,则状态变量具有优先权。与常数一样,状态变量可以输入到层次结构的根节点,并由任何子节点访问。如果层次结构中的子节点有自己的常数或状态变量,则这些值将优先于在父节点上设置的值。

尽管状态可以动态更新,但布局中引用的所有状态变量都必须在LayoutNode首次挂载/更新之前给予值。通常,在首次初始化节点时设置所有状态变量的默认值是个好主意。

在创建后的LayoutNode上调用setState()将触发更新。更新会导致该节点及其子节点的所有表达式重新评估。将来可能会检测到父节点是否间接受到子节点状态变更的影响,并对其进行更新,但当前尚未实现。

在上面的示例中,我们使用字典来存储状态,但LayoutNode支持使用任意对象作为状态。对于具有复杂状态要求的布局,使用struct是个非常好的主意。当您使用structclass设置状态时,Layout将使用Swift的反射功能来比较更改,并确定是否需要更新。

内部上,LayoutNode仍然将struct视为键/值对的字典,但您可以在程序的其他部分以编程方式操作状态时利用编译时的类型验证。

struct LayoutState {
    let isSelected: Bool
}

loadLayout(
    named: "MyLayout.xml",
    state: LayoutState(isSelected: false),
    constants: [
        "title": ...
    ]
)

func setSelected() {
    self.layoutNode?.setState(LayoutState(isSelected: true))
}

当使用状态字典时,您不需要在每次设置状态时传递每个属性。如果您只更新属性的一部分,传递仅包含这些键/值对的字典是可以的。(如果使用struct则不适用,但不必担心——这仅仅是一个方便的功能,对性能的影响很小或没有影响。)

loadLayout(
  named: "MyLayout.xml",
  state: [
    "value1": 5,
    "value2": false,
  ]
)

func setSelected() {
    self.layoutNode?.setState(["value1": 10]) // value2 retains its previous value
}

操作

对于任何非平凡的视图,您需要将视图层次结构中的控件操作绑定到视图控制器,并将用户操作传达回视图。

您可以在XML中使用actionName="methodName"在任何UIControl子类上定义操作,例如

<UIButton touchUpInside="wasPressed"/>

不需要指定目标——操作将自动绑定到响应链中的第一个匹配方法。如果没有找到匹配的方法,Layout将显示错误。

注意:错误将在节点挂载时显示,而不是在按钮按下时延迟显示,就像使用Interface Builder绑定的操作那样。

func wasPressed() {
    ...
}

操作的名称遵循Objective-C选择器语法规则,因此如果您想将按钮本身作为发送者传递,请在方法名称末尾使用尾随冒号。

<UIButton touchUpInside="wasPressed:"/>

然后可以相应地实现方法:

func wasPressed(_ button: UIButton) {
    ...
}

操作表达式被视为字符串,与其他字符串表达式一样,它们可以包含逻辑,根据布局常数或状态产生不同的值。如果您想切换操作到不同的方法,这非常有用。

<UIButton touchUpInside="{isSelected ? 'deselect:' : 'select:'}"/>

在这种情况下,按钮将根据isSelected状态变量的值调用select(_:)deselect(_:)方法。

端点

在Nib或Storyboard中创建视图时,您通常会通过在视图控制器中使用标记为@IBOutlet属性来创建对个别视图的引用,布局可以利用相同系统从代码中引用层次结构中的个别视图。

要为布局节点创建端点绑定,请在其视图控制器中声明正确类型的属性,然后使用LayoutNodeoutlet构造函数参数引用它。

class MyViewController: UIViewController, LayoutLoading {

    @objc var labelNode: LayoutNode? // outlet

    public override func viewDidLoad() {
        super.viewDidLoad()

        self.layoutNode = LayoutNode(
            view: UIView.self,
            children: [
                LayoutNode(
                    view: UILabel.self,
                    outlet: #keyPath(self.labelNode),
                    expressions: [ ... ]
                )
            ]
        )
    }
}

在本例中,我们将包含UILabelLayoutNode绑定到labelNode属性。以下是一些需要注意的事项:

  • 不需要为outlet属性使用@IBOutlet属性,但如果您认为这可以使目的更明确,则可以这样做。如果不使用@IBOutlet,则需要使用@objc以确保在运行时Layout能够看到该属性。
  • outlet属性的类型可以是LayoutNode或与节点管理的视图兼容的UIView子类。两种情况下的语法相同 - 类型将在运行时进行检查,如果不匹配,将会抛出错误。
  • 在上述示例中,我们使用了Swift的#keyPath语法来指定outlet值,以获得更好的静态验证。这是推荐的,但不是必需的。
  • 示例中的labelNode端点已被标记为可选类型。在定义IB端点时使用隐式解包可选(IUOs)是很常见的,Layout也可以使用它,但如果在XML中出错并尝试访问端点,将会导致程序崩溃。使用常规可选类型意味着XML错误可以在不重启应用程序的情况下捕获和修复。

在使用XML模板时,使用outlet属性来指定端点绑定。

<UIView>
    <UILabel
        outlet="labelNode"
        text="Hello World"
    />
</UIView>

在这种情况下,我们失去了由#keyPath提供的静态验证,但布局仍然执行运行时检查,并且如果发生拼写错误或类型不匹配的情况,将以优雅的方式抛出错误,而不是崩溃。

端点也可以使用表达式而不是字面值来设置。如果您希望将端点作为参数传递给模板,例如,这非常有用:

<UIView>
    <param name="labelOutlet" type="String"/>

    <UILabel
        outlet="{labelOutlet}"
        text="Hello World"
    />
</UIView>

在这种情况下,参数的类型必须是String,而不是您可能期望的UILabel。这背后的原因是因为端点是一个引用布局所有者(通常是视图控制器)属性的关键路径的键路径,而不是对视图本身的直接引用。

注意:输出表达式必须使用常量或字面值设置,一旦设置后无法更改。尝试使用状态变量或其他动态值设置输出将导致错误。

代理

iOS中还广泛使用的一种功能是代理模式。布局也支持这种模式,但如果用户没有预料到,可能会觉得这种方式有些难以理解。

当加载布局XML文件或将程序创建的LayoutNode层次结构加载到视图控制器时,会扫描视图的代理属性,并在符合指定协定的条件下自动将它们绑定到控制器。

例如,如果您的布局中包含UIScrollView,并且您的视图控制器符合UIScrollViewDelegate协议,那么视图控制器将自动成为视图控制器的代理。

class MyViewController: UIViewController, LayoutLoading, UITextFieldDelegate {
    var labelNode: LayoutNode!

    public override func viewDidLoad() {
        super.viewDidLoad()

        self.layoutNode = LayoutNode(
            view: UIView.self,
            children: [
                LayoutNode(
                    view: UITextField.self, // delegate is automatically bound to MyViewController
                    expressions: [ ... ]
                )
            ]
        )
    }

    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        textField.resignFirstResponder()
        return false
    }
}

然而,这里还有一些注意事项

  • 当前机制仅适用于名为“delegate”或“dataSource”,或以“Delegate”或“DataSource”结尾(例如“dragDelegate”)的属性。这是UIKit组件使用的标准约定,但如果您的自定义控件使用不同的命名约定,则它们不会被自动绑定,您需要手动绑定。

  • 绑定机制依赖于Objective-C运行时协议检测,因此它不适用于不遵循@objc的Swift协议。

  • 如果您在布局中有多个视图,都使用相同的代理协议,比如几个UIScrollView或多个UITextField,那么它们都会绑定到视图控制器。如果您只对一些视图的某个事件感兴趣,您可以在代理方法内添加逻辑来确定是哪个视图调用了它们,或者显式禁用这些视图的delegate属性,将它们设置为nil

<UITextField delegate="nil"/>

您也可以通过传递一个引用到特定的对象(作为状态变量或常量)然后在您的代理表达式中引用它来设置代理为该特定对象。

self.layoutNode = LayoutNode(
    view: UIView.self,
    constants: [
        "fieldDelegate": someDelegate
    ],
    children: [
        LayoutNode(
            view: UITextField.self,
            expressions: [
                "delegate": "fieldDelegate"
            ]
        )
    ]
)

注意:目前没有安全的方法可以显式地将代理绑定到布局控制节点的拥有类。尝试将self作为常量或状态变量传递将导致保留循环(这就是为什么绑定是隐式而不是手动完成的原因)。

动画

UIKit对动画有着很好的支持,因此您会在基于布局的界面中加入动画,那么您如何在布局中处理动画呢?

iOS中有三种基本的动画类型:

  1. 基于块级的动画,使用UIView.animate()。通常,您会在UIKit中将视图属性和/或AutoLayout约束设放在一个动画块内。在布局中,您应该在一个动画块内调用setState()来隐式地动画化任何由于状态更改而产生的任何变化。
UIView.animate(withDuration: 0.4) {
    self.layoutNode?.setState([...])
}
  1. 动画属性设置器。某些UIView的属性有动画设置器变体,在调用时自动应用动画。例如,调用UISwitch.setOn(_:animated:)会动画化开关的状态,而直接设置on属性将立即更新它。布局不将在XML中公开setOn(_:animated:)方法,但如果你有一个<UISwitch isOn="onState"/>的表示式,你可以通过调用setState(_:animated:)使其带动画地更新。
self.layoutNode?.setState(["onState": true], animated: true)

通过使用setState()animated参数将隐式调用更新影响的任何属性的动画设置器变体。不支持动画的属性将按常规设置。

  1. 用户驱动的动画。一些动画效果由用户拖动或滚动控制。例如,在滚动时你可能会有一个视差效果,使几个视图以与滚动同步的方向或速度移动。要在布局中实现这类动画,请在滚动或手势处理程序中调用setState(),传递定位动画视图所需的任何参数。你可以在Swift中实现动画逻辑并将结果作为状态传递,或者使用你在布局XML中的表达式计算动画状态 - whichever works best for your use-case,例如。
<UIView alpha="max(0, min(1, (position - 50) / 100))"/>
func scrollViewDidScroll(_ scrollView: UIScrollView) {
    self.layoutNode?.setState(["position": scrollView.contentOffset.y])
}

安全区域衬垫

iOS 11引入了“安全区域”的概念——这是对之前提供的顶部和底部布局辅助器的概括,用于将内容内嵌以考虑状态、导航和工具/标签栏。

为了避免你在模板中包含条件编译逻辑,Layout使得iOS 11的safeAreaInsets属性在所有iOS版本中都可用(对于iOS 10及更早版本使用布局辅助器作为底层实现进行回退)。

要在一个视图的父级安全区域中定位视图,你可能会编写

<UIView
    top="parent.safeAreaInsets.top"
    left="parent.safeAreaInsets.left"
    bottom="100% - parent.safeAreaInsets.bottom"
    right="100% - parent.safeAreaInsets.right"
/>

注意: Layout公开的safeAreaInsets值的值与UIView.safeAreaInsets的文档化行为略有不同。

Apple表示,safeAreaInsets值考虑了状态栏和其他UI(如导航或工具栏),但这仅适用于视图控制器的根视图。对于子视图,内边距只反映由这些栏覆盖的视图部分,因此对于屏幕中间的小视图,内边距始终为零,因为工具栏或iPhone X的刘海永远不会与该视图重叠。

对于Layout,这种方法会引发问题,因为你的视图框架可能取决于safeAreaInsets值,而该值又会反过来影响框架,从而产生循环依赖关系。而不是试图递归地解决这个问题,Layout始终返回相对于当前视图控制器的内边距,因此即使对于不与屏幕边缘重叠的子视图,safeAreaInsets的值也将与根视图相同。

UIScrollView在iOS 11上自动推导其内边距,但该行为与iOS 10不同。为了实现一致的行为,可以将contentInsetAdjustmentBehavior属性设置为never,然后手动设置contentInset

<UIScrollView
    contentInsetAdjustmentBehavior="never"
    contentInset="parent.safeAreaInsets"
    scrollIndicatorInsets.top="parent.safeAreaInsets.top"
    scrollIndicatorInsets.bottom="parent.safeAreaInsets.bottom"
/>

为了简化向后兼容性,类似于safeAreaInsets属性本身,Layout允许你在任何iOS版本上设置contentInsetAdjustmentBehavior,但是在iOS 11之前的版本中值会被忽略。

旧版布局模式

在代码或文档中,您可能看到对 LayoutNode.useLegacyLayoutMode 的引用。在布局的原设计中,rightbottom 表达式是相对于视图的左上角指定的,而不是像您可能期望的那样相对于各自的边缘。

布局 0.6.22 版本引入了一种新布局模式,其中 bottomright 表达式相对于 bottomright 边缘,这对于熟悉 CSS 或 AutoLayout 的用户来说更为直观,并且也与 leadingtrailing 表达式的工作方式保持一致。

为了避免与现有的布局项目兼容性问题,您必须在应用程序代码中明确选择加入新布局模式,通过设置 LayoutNode.useLegacyLayoutMode = false。这是一个全局属性,所以只需要设置一次。一个好的做法是在您的 AppDelegateapplication(_:didFinishLaunchingWithOptions:) 方法中完成此操作。

import UIKit
import Layout

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?

    func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        
        // enable Layout's new layout mode
        LayoutNode.useLegacyLayoutMode = false
        
        // other setup code
        ...
    }
}

在将来版本的布局中,新的布局模式将成为默认模式,旧版布局模式最终将删除,因此现在开始迁移您的模板是一个好主意!

表达式

LayoutNode 类最重要的特性是其内置的表达式解析和评估支持。此功能的实现建立在 Expression 框架之上,但布局添加了多个扩展,以支持 UIKit 类型以及特定于布局的逻辑。

表达式可以是简单的硬编码值,例如 "10",也可以是更复杂的表达式,例如 "width / 2 + someConstant"。在表达式中使用可用运算符和函数取决于所表示属性的名称和类型,但所有表达式都支持在大多数 C 语言家族编程语言中找到的标准十进制数学运算符、布尔运算符和函数。您还可以通过以下内容扩展布局以使用自定义函数(参见下面的 函数 部分)。

LayoutNode 中的表达式可以引用传递给节点或其任何父节点的不变量和状态。它们还可以引用节点上定义的任何其他表达式的值或任何支持的可视属性。

5 + width / 3
isSelected ? blue : gray
min(width, height)
a >= b ? a : b
pi / 2

此外,一个节点可以使用 parent.someProperty 引用其父节点的属性,或使用 previous.somePropertynext.someProperty 引用其相邻同层级节点的属性。

<UIView>
    <UILabel text="Foo"/>
    
    <!-- this label will be 20pts below its previous sibling -->
    <UILabel
        top="previous.bottom + 20"
        text="Bar"
    />
</UIView>

要引用不是直接同层级的节点,您可以给节点一个 id 属性,然后使用 # 后跟 id 引用该节点。

<UIView>
    <UILabel id="first" left="20" text="Foo"/>
    <UILabel right="20" text="Bar"/>
    
    <!-- this label will be aligned with the first label -->
    <UILabel
        left="#first.left"
        top="previous.bottom + 20"
        text="Bar"
    />
</UIView>

布局属性

LayoutNode可用的可表达属性集合取决于视图类型,但每个节点至少支持以下属性

top
left
bottom
right
leading
trailing
width
height
center.x
center.y

注意:有关rightbottom布局属性的重要说明,请参阅 Legacy Layout Mode部分。

这些是指定视图框架的数值(以屏幕点为单位)。除了标准运算符之外,所有这些属性都允许指定百分比值

<UIView right="50%"/>

百分比值相对于父LayoutNode的宽度或高度(如果没有父节点,则为父视图)。上面的表达式通常等效于以下写法(除非父视图为UIScrollView,在这种情况下还会考虑contentInset和安全区域)

<UIView right="parent.width / 2">

此外,widthheight属性可以利用一个名为auto的虚拟变量。该auto变量等于节点的内容宽度和高度,这由以下三者组合决定

  • 原生视图的intrinsicContentSize属性(如果指定了)
  • 视图的子视图(非Layout管理的)应用到的任何AutoLayout约束
  • 节点的所有子节点的边界框。

如果一个节点没有子节点且没有固有大小,auto通常等同于100%,具体情况取决于视图类型。

尽管完全用Swift编写,但布局库大量使用Objective-C运行时来自动生成任何类型视图的属性绑定。因此,可用的属性取决于传递给LayoutNode构造函数的视图类型(如果你使用XML布局,则为XML节点的名称)。

只有对Objective-C运行时可见的类型才能自动检测。幸运的是,由于UIKit是Objective-C框架,大多数视图属性都能正常工作。对于不适用的情况,可以通过在视图上使用扩展来手动公开这些属性(这将在高级话题中介绍)。

由于可以通过常量状态传递任意值,布局库支持在表达式中引用几乎任何类型的价值,即使无法将其表示为字面量。

表达式是强类型的,因此将错误的类型值传递给函数或运算符或从表达式中返回错误的类型将导致错误。在可能的情况下,这些类型检查在节点最初安装时执行,以便立即显示错误。

以下类型属性得到特殊处理,以便更容易使用表达式字符串指定它们

几何形状

由于布局自动管理视图框架,不允许直接操作视图的framebounds - 应该使用topleftbottomrightleadingtrailingwidthheightcenter.xcenter.y表达式。然而,还有一些几何属性不会直接影响框架,并且其中许多属性是可以通过表达式设置的。例如:

  • contentSize
  • contentInset
  • layer.transform

这些属性不是简单的数字,而是包含多个打包值的结构。那么,如何使用布局表达式来操作这些属性呢?

首先,几乎可以任何属性类型都可以使用常量或状态变量设置,即使无法在表达式中为它定义字面值。例如,以下代码将设置layer.transform,尽管布局无法在表达式中指定字面值的CATransform3D结构。

loadLayout(
    named: "MyLayout.xml",
    state: [
        "flipped": true
    ],
    constants: [
        "identityTransform": CATransform3DIdentity,
        "flipTransform": CATransform3DMakeScale(1, 1, -1)
    ]
)
<UIView layer.transform="flipped ? flipTransform : identityTransform"/>

其次,对于许多几何结构类型,如CGPointCGSizeCGRectCGAffineTransformUIEdgeInsets,Layout内置了对表达式直接引用成员属性的支持。要为UIScrollView设置顶部contentInset值,可以使用:

<UIScrollView contentInset.top="safeAreaInsets.top + 10"/>

要显式设置contentSize,可以使用:

<UIScrollView
    contentSize.width="200%"
    contentSize.height="auto + 20"
/>

注意:widthheight一样,在contentSize.widthcontentSize.height内部允许使用%auto。但百分比指的是视图自身的框架大小,而不是其父视图。在UIScrollView内部设置的百分比大小也会考虑到contentInset,因此100%应填充视图的内容区域而不滚动。

Layout还支持用于操作CATransform3D的虚拟键路径属性(如在此处文档所述,并且为CGAffineTransform提供了等效属性。这意味着您可以直接在布局XML中执行旋转或缩放视图的操作,而无需进行任何矩阵运算。

<UIView transform.rotation="pi / 2"/>

<UIView transform.scale="0.5"/>

<UIView layer.transform.translation.z="500"/>

字符串

通常需要在表达式中使用字面字符串,由于表达式本身通常用引号括起来,所以每次都必须使用嵌套引号会是一件很麻烦的事情。因此,字符串表达式默认被视为字面字符串,所以在这个例子中...

<UILabel text="title"/>

...标签的text属性已被赋予字面值“title”,而不是您可能预期的名为“title”的常量值。

要在字符串属性中使用表达式,请用大括号{ ... }括号中的值进行转义。所以,要用名为title的常量或变量替换字面值“title”,您将写成这样:

<UILabel text="{title}"/>

您可以在大括号表达式块中使用任意逻辑,包括数学和布尔比较。表达式的值不必是字符串,因为结果将被转换为字符串。您可以在单个字符串表达式中使用多个表达式块,并将表达式块与文本段落混合。

<UILabel text="Hello {name}, you have {n + 1} new messages"/>

如果要使用表达式中字面字符串,则可以使用单引号来转义它。

<UILabel text="Hello {hasName ? name : 'World'}"/>

如果您要显示字面字符{}大括号,可以按照如下方式转义:

<UILabel text="Open brace: {'{'}. Close brace: {'}'}."/>

Layout支持在表达式中操作字符串字面和变量。要连接字符串,可以在单个字符串属性内部使用多个表达式子句,也可以在单个表达式中使用+运算符。

<UILabel text="{'foo'}{'bar'}"/>

<UILabel text="{'foo' + 'bar'}"/>

您可以使用Swift风格的索引语法来引用单个字符或子字符串。通常,Swift要求String索引使用String.Index类型的值,但为了方便,Layout同时支持整数索引和范围。这些都是从0开始的,并引用Character索引(而不是字节数或Unicode标量)

<!-- Displays 'e' -->
<UILabel text="{'Hello World'[1]}"/>

<!-- Displays 'foo' -->
<UILabel text="{'foobar'[0..<3]}"/>

<!-- Displays 'bar' -->
<UILabel text="{'foobar'[3...]}"/>

尝试引用原始字符串范围之外的字符串不会崩溃,但会显示红色错误框。目前,除非您实现自定义的count()函数或等效函数,否则无法在表达式内部检查字符串的界限(有关详细信息,请参阅下面的函数部分)。

如果您的应用已本地化,则需要使用常量而不是直接使用模板中的所有字符串。本地化所有这些字符串并将它们作为独立的常量传递将会很繁琐,因此Layout提供了一些替代方案。

strings.开头的前缀假定是本地化字符串,它们将在应用的Localizable.strings文件中查找。例如,如果您的Localizable.strings文件包含以下条目:

"Signup.NameLabel" = "Name";

那么您可以直接在XML中引用它,而不必在代码中创建显式的常量

<UILabel text="{strings.Signup.NameLabel}"/>

iOS上通常将英文文本用作本地化字符串的键,这通常可能包含空格或标点符号,使其作为标识符无效。在这些情况下,您可以使用反引号来转义键,如下所示:

<UILabel text="{`strings.Some text with spaces and punctuation!`}"/>

本地化字符串可能包含占位符令牌用于运行时值。在iOS上,这个惯例是使用printf %转义序列来替换这些占位符,这些占位符将程序性地进行替换。Layout通过将参数化的字符串常量作为函数处理来支持这种机制。例如,对于以下本地化字符串:

"Messages.Title" = "Hello %s, you have %i new messages";

您可以在模板内部直接显示格式化后的字符串,如下所示(假设namemessageCount是有效的状态变量):

<UILabel text="{strings.Messages.Title(name, messageCount)}"/>

Layout检查格式字符串中的占位符,如果传递的参数数量或类型不正确,则会显示错误。Layout的格式字符串处理是由Sprinter框架驱动的,并完全支持IEEE printf规范,因此您可以在本地化字符串中使用如%1.3f%3$0x之类的标志来控制参数顺序和格式。

除了减少样板代码外,直接从XML引用的字符串也将利用实时重新加载,因此您可以在模拟器中通过按Cmd-R更改Localizable.strings文件,而无需重新编译应用。

属性字符串

属性字符串与常规正则表达式的工作方式非常相似,只是您可以使用内联属性字符串常量来创建样式化文本。

loadLayout(
    named: "MyLayout.xml",
    constants: [
        "styledText": NSAttributedString(string: "styled text", attributes: ...)
    ]
)
<UILabel text="This is some {styledText} embedded in unstyled text" />

属性字符串表达式内置的一个额外且巧妙的特性是支持内联(X)HTML标记。

LayoutNode(
    view: UILabel.self,
    expressions: [
        "text": "I <i>can't believe</i> this <b>actually works!</b>"
    ]
)

在XML属性中使用此功能很尴尬,因为标签必须使用 &gt;&lt; 进行转义,所以布局允许您在视图节点中嵌套HTML,并且它将被自动分配给视图的适当属性文本(有关详细信息,请参阅正文文本部分)。

<UILabel>This is a pretty <b>bold</b> solution</UILabel>

HTML支持依赖于内置的 NSMutableAttributedString HTML解析器,它不识别内联CSS样式或脚本,并且仅支持HTML的最小子集。以下标签经验证可以正常工作,但其他标签可能无法正常工作,具体取决于iOS版本。

<p>, // paragraph
<h1> ... <h6> // heading
<b>, <strong> // bold
<i>, <em> // italic
<u> // underlined
<strike> // strikethrough
<ol>, <li> // ordered list
<ul>, <li> // unordered list
<br/> // linebreak
<sub> // subscript
<sup> // superscript
<center> // centered text

与常规文本属性一样,内联HTML可以包含嵌入式表达式,这些表达式可以包含属性字符串变量或常量或非属性字符串变量或常量。

<UILabel>Hello <b>{name}</b></UILabel>

URL

URL表达式被视为字面字符串,所以动态逻辑(如对常量或变量的引用)必须使用 { ... } 转义。

<!-- literal url -->
<MyView url="index.html"/>

<!-- url constant or variable -->
<MyView url="{someURL}"/>

不包含方案的URL默认是本地文件路径引用。不含起始 / 的路径假定为应用程序资源包中的相对路径,而以 ~/ 开头的路径假定为用户目录中的相对路径。

<!-- remote url -->
<MyView url="http://example.com"/>

<!-- app resource bundle url -->
<MyView url="images/foo.jpg"/>

<!-- user document url -->
<MyView url="~/Documents/report.pdf"/>

字体

类似于字符串和URL表达式,字体表达式也被视为字面字符串,因此对常量或变量的引用必须使用 { ... } 进行转义。一个字体表达式可以编码多个不同的数据,由空格分隔。

UIFont 类封装了字体家族、大小、重量和样式,因此一个字体表达式可以包含以下空格分隔的任何或所有属性,以任何顺序

<font-name>
<font-traits>
<font-weight>
<font-style>
<font-size>

未指定的任何字体属性都将设置为系统默认值 - 目前为iOS 11的San Francisco 17点。

<font-name> 是一个字符串。它是不区分大小写的,可以表示一个确切的字体名称,或一个字体家族名称。字体名称可以包含空格,并且可以可选地用单引号括起来。如果您想使用系统字体,请使用 "System" 作为字体名称(尽管如果没有指定名称,这是默认字体)。您还可以使用 "SystemBold" 和 "SystemItalic"。以下是一些示例

<UILabel font="Courier"/>

<UILabel font="helvetica neue"/>

<UILabel font="'times new roman'"/>

<UILabel font="SystemBold"/>

<font-traits>UIFontDescriptorSymbolicTraits 类型的值。以下特性受到支持

italic
condensed
expanded
monospace

给定的字体表达式可以包含多个特征。请注意,bold 特性不受支持,因为 bold 被视为 <font-weight> 值。如果出于某种原因,您希望指定 bold 特性而不是 bold 重量,您可以通过在花括号内使用完全限定的特性名称来做此操作

<UILabel text="Font with bold trait" font="{UIFontDescriptorSymbolicTraits.traitBold}"/>

<UILabel text="Font with bold weight" font="bold"/>

<font-weight> 是以下列表中的 UIFont.Weight 常量

ultraLight
thin
light
regular
medium
semibold
bold
heavy
black

示例

<UILabel font="Courier bold"/>

<UILabel font="System black"/>

<UILabel font="helvetica neue ultraLight"/>

注意: “SystemBold” 与 “System bold” 不相同。前者相当于Swift中的 UIFont.boldSystemFont(ofSize: 17),使用的是 特性 (如上所述),而后者相当于 UIFont.systemFont(ofSize: 17, weight: .bold),使用的是 重量,这会产生不同的结果。

<font-style> 是以下列表中的 UIFontTextStyle 常量

title1
title2
title3
headline
subheadline
body
callout
footnote
caption1
caption2

指定这些值之一会将字体大小设置为与用户对此样式的字体大小设置匹配,并启用动态文本大小调整,以便更改字体大小设置会自动更新字体。

<font-size>可以是数字或百分比。如果您使用百分比值,则它将相对于默认字体大小(17点)或已经在字体表达式中指定的任何大小。例如,如果表达式中包含字体样式常量,大小将相对于该常量。以下是一些更多示例

<UILabel font="Courier 150%"/>

<UILabel font="Helvetica 30 italic"/>

<UILabel font="helvetica body bold 120%"/>

可以通过内联表达式使用UIFont常量或变量。要使用名称为themeFontUIFont常量,但覆盖其大小和粗细,您可以编写:

<UILabel font="{themeFont} 25 bold"/>

您还可以通过扩展UIFont定义自定义命名字体,并布局将自动检测它们

extension UIFont {
    @objc static let customFont = UIFont.systemFont(ofSize: 42)
}

以这种方式定义的字体可以从任何字体表达式中通过名称引用,无论是带“Font”后缀还是不带,但在带有大括号的子表达式{...}中不可用,除非带有UIFont.前缀

<UILabel font="customFont bold"/>

<UILabel font="custom italic"/>

<UILabel font="{UIFont.customFont} 120%"/>

颜色

可以使用CSS风格的rgb(a)十六进制文字指定颜色。它们可以是3、4、6或8位长,并且以#前缀开头。

#fff // opaque white
#fff7 // 50% transparent white
#ff0000 // opaque red
#ff00007f // 50% transparent red

还支持内置的静态UIKit常量。

white
red
darkGray
etc.

您还可以使用CSS风格的rgb()rgba()函数。为了与CSS约定保持一致,红色、绿色和蓝色值位于0-255范围内,alpha位于0-1范围内。

rgb(255,0,0) // red
rgba(255,0,0,0.5) // 50% transparent red

您可以将这些数据和函数用作更复杂表达式的一部分,例如

<UILabel textColor="isSelected ? #00f : #ccc"/>

<UIView backgroundColor="rgba(255, 255, 255, 1 - transparency)"/>

请注意,无需在大括号中括住这些表达式。除非表达式与命名颜色资产冲突(见下文),否则Layout会理解您的意图。

颜色文字的使用对于开发目的来说很方便,但建议您为应用中常用的颜色定义常量(或针对iOS 11及更高版本,定义XCAssets),因为这些在稍后重构时会更容易。

要提供自定义命名字符串常量,您可以在加载布局时将颜色传递到常量字典中

loadLayout(
    named: "MyLayout.xml",
    constants: [
        "headerColor": UIColor(0.6, 0.5, 0.5, 1),
    ]
)

颜色常量可用于任何表达式(尽管它们可能不在颜色之外有很大的用途)。

您还可以通过扩展UIColor定义自定义颜色,并布局将自动检测它们

extension UIColor {
    @objc static let headerColor = UIColor(0.6, 0.5, 0.5, 1)
}

以这种方式定义的颜色可以从任何颜色表达式中通过名称引用,无论是带“Color”后缀还是不带,但在其他表达式类型中不可用,除非带UIColor.前缀

<UIView backgroundColor="headerColor"/>

<UIView backgroundColor="header"/>

<UIView isHidden="backgroundColor == UIColor.header"/>

最后,在iOS 11及更高版本中,您可以定义命名颜色作为XCAssets,然后在表达式中通过名称引用这些颜色

<UIView backgroundColor="MyColor"/>

<UIView backgroundColor="my color"/>

<UIView backgroundColor="color-number-{level}"/>

对于在框架或独立捆绑包中定义的颜色资源,您可以在颜色名称前加上捆绑包名称(或完整的捆绑标识符)和冒号。例如

<UIView backgroundColor="com.example.MyBundle:MyColor"/>

<UIView backgroundColor="MyBundle:MyColor"/>

您还可以引用存储在常量或变量中的Bundle/NSBundle实例

<UIImageView image="{bundle}:MyColor"/>

注意:无需在颜色资源名称周围使用引号,即使它们包含空格或其他标点符号。如果需要,布局将解释无效的颜色资源名称为表达式。您可以使用大括号{ ... }来区分资源名称、常量或变量名称。

图片

可以通过名称、常量或状态变量来指定静态图片。和颜色一样,无需使用引号包围名称,但在需要的情况下,可以使用{ ... }括号来消歧义。

<UIImageView image="default-avatar"/>

<UIImageView image="{imageConstant}"/>

<UIImageView image="image_{index}.png"/>

和颜色资源一样,在框架或独立包中定义的图片资源可以通过在包名/标识符或常量前加上冒号来进行引用。

<UIImageView image="com.example.MyBundle:MyImage"/>

<UIImageView image="MyBundle:MyImage"/>

<UIImageView image="{bundle}:MyImage"/>

枚举

要为枚举类型的表达式设置值,只需使用值的名称。例如:

<UIImageView contentMode="scaleAspectFit"/>

您可以直接在枚举表达式中使用逻辑,无需转义逻辑或使用引号包围名称。

<UIImageView contentMode="isSmallImage ? center : scaleAspectFit"/>

标准UIKit枚举值作为常量公开,只能在同一类型的表达式中使用。不需要像Swift中那样在枚举值名称前加上点(.),但必须在其他表达式类型中使用枚举值时,在枚举值名称前加上类型。

<!-- will work -->
<UIImageView height="contentMode == UIViewContentMode.scaleAspectFit ? 200 : 300"/>

<!-- won't work -->
<UIImageView height="contentMode == scaleAspectFit ? 200 : 300"/>
<UIImageView height="contentMode == .scaleAspectFit ? 200 : 300"/>

选项集

选项集表达式与枚举表达式的功能相同。如果您想为选项集设置多个值,可以使用逗号来分隔。

<UITextView dataDetectorTypes="phoneNumber, link"/>

不需要像Swift中那样在多个选项集值周围加上方括号。和枚举一样,选项集值名称不能在设置它们的表达式之外使用,除非在名称前加上类型名。

数组

您可以在任何类型的表达式中使用类似Swift风格的花括号标记数组字面量。

<UISegmentedControl items="['Hello', 'World']"/>

您可以使用+运算符连接数组字面量。

<UISegmentedControl items="['Hello'] + ['And', 'Goodbye']"/>

对于数组类型的表达式,方括号是可选的;您只需传递以逗号分隔的值,它们将自动被视为数组。

<UISegmentedControl items="'Hello', 'World'"/>

如果您从数组表达式返回一个非数组值,它将自动被放入数组中。

<!-- 'Hello' becomes ["Hello"] -->
<UISegmentedControl items="'Hello'"/>

,运算符会自动扁平化嵌套数组常量,因此以下代码将生成一个平坦的数组,而不是包含另一个数组的外部数组。

loadLayout(
    named: "MyLayout.xml",
    constants: [
        "firstTwoItems": ["First", "Second"],
    ]
)
<UISegmentedControl items="firstTwoItems, 'Third'"/>

如果需要重用值,您可以在中使用相同的数组字面量语法。

<UIView>
    <macro name="ITEMS" items="'First', 'Second'"/>
    <UISegmentedControl items="ITEMS"/>
</UIView>

如果您需要访问数组中的单个元素,您可以使用表达式中代码的方括号操作符 []

loadLayout(
    named: "MyLayout.xml",
    constants: [
        "colors": [UIColor.green, UIColor.black],
    ],
)
<!-- green label -->
<UILabel textColor="colors[0]"/>

您还可以使用范围来索引数组。所有标准的 Swift 范围操作符都受支持,包括开放式范围

<!-- Only the second and third item -->
<UISegmentedControl items="items[1...2]"/>

<!-- Only the first and second item -->
<UISegmentedControl items="items[..<2]"/>

<!-- All but the first item -->
<UISegmentedControl items="items[1...]"/>

尝试使用超出范围的索引或范围访问数组不会崩溃,但会显示红色警告框错误。目前尚无从表达式内部检查数组边界的直接方法,除非您实现一个自定义的 count() 函数或等效函数(有关详细信息,请参见下面的 函数 部分)。

函数

布局表达式支持许多内置数学函数,如 min()max()pow() 等。但您还可以扩展布局,为模板内部调用添加额外的自定义函数。

自定义函数是符合签名 ([Any]) throws -> Any 的 Swift 闭包。任何符合此类型且传递给您的 LayoutNode 的闭包常量都可以在表达式内部调用。

目前没有办法指定自定义函数所期望的参数数量或类型,因此您必须在自定义函数内部进行类型检查以防止崩溃。以下是一些示例

loadLayout(
    named: "MyLayout.xml",
    constants: [
        "count": { (args: [Any]) throws -> Any in
            guard args.count == 1 else {
                throw LayoutError.message("count() function expects a single argument")  
            }
            switch args[0] {
            case let array as [Any]:
                return array.count
            case let string as String:
                return string.count
            default:
                throw LayoutError.message("count() function expects an Array or String")   
            }
            return array.count
        },
        "uppercased": { (args: [Any]) throws -> Any in
            guard let string = args.first as? String else {
                throw LayoutError.message("uppercased() function expects a String argument")  
            }
            return string.uppercased()
        },
    ],
)
<UILabel text="{uppercased('uppercased text'}"/>

<UILabel text="'foo' contains {count('foo')} characters"/>

可选参数

布局表达式对可选参数的支持目前相当有限。无法指定表达式的返回值是可选的,因此从表达式中返回 nil 通常是一个错误。但也有一些例外

  1. 从 String 表达式中返回 nil 会返回空字符串
  2. 从 UIImage 表达式中返回 nil 会返回宽度和高度为零的空白图像
  3. 对于代理或其他协议属性,返回 nil 是允许的,以覆盖默认绑定行为

这些特定例外的原因是,将一个 nil 图像或文本传递给组件是在 UIKit 中表示给定元素不需要的常见方法,通过允许这些类型的 nil 值,我们可以避免在组件中传递额外的标志来标记这些值未使用。

在表达式内部处理可选值时,有一些稍微更多的灵活性。在表达式中可以引用 nil,并且可以将值与它进行比较。例如

<UIView backgroundColor="col == nil ? #fff : col"/>

在这个示例中,如果 col 常量是 nil,则返回默认颜色白色。这也可以使用 ?? 空值合并运算符更加简洁地编写

<UIView backgroundColor="col ?? #fff"/>

注释

复杂的或晦涩的代码往往需要使用行内注释形式的文档进行说明。您可以将注释插入到XML布局文件中,具体方法如下

<!-- `name` is the user's full name -->
<UILabel text="{name}"/>

不幸的是,虽然XML支持在节点之间使用注释标签,但在节点属性之间没有放置注释的方法,所以如果节点有多个属性,这种方法并不令人满意。

为了绕过这个问题,Layout 允许您在表达式的属性中直接使用 C 风格的 "//" 注释

<UILabel
    text="{name // the user's full name}"
    backgroundColor="colors.primary // the primary color"
/>

如果在开发过程中需要临时注释掉一个表达式,这个特性也非常方便

<UIView temporarilyDisabledProperty="// someValue"/>

注释可以用于任何表达式,但对于字符串类型的表达式有一些注意事项:在字符串表达式中,除了 {...}花括号之外的所有内容都视为字面字符串值的一部分,包括 /字符。因此,这不会按预期工作

<UIImage image="MyImage.png // comment"/>

因为上述表达式中的图像属性被解释为一个字符串,所以 "//注释" 被视为名称的一部分。为了解决这个问题,应将注释放在 {...} 中。以下两种方式都有效

<UIImage image="MyImage.png{// comment}"/>

<UIImage image="{'MyImage.png' // comment}"/>

这种情况的例外是整个表达式都被注释掉。如果您希望临时注释掉一个表达式,无论表达式类型如何,只要在注释开头放置 "//" 就可以

<UIImage image="// MyImage.png"/>

在极不可能的情况中,如果您最初需要字符串表达式的字面值以 "//" 开头,您可以使用 {...} 来转义斜杠

<UILabel text="// this is a comment"/>

<UILabel text="{// this is also a comment}"/>

<UILabel text="{'// this is not a comment'}"/>

<UILabel text="{'//'} this is also not a comment"/>

标准组件

Layout 对大多数内置 UIKit 视图和视图控制器提供了良好的支持。它可以使用 init(frame:) 自动创建几乎任何 UIView 子类,并且可以设置任何与键值编码 (KVC) 兼容的属性,但某些视图期望额外的初始化器参数,或者具有在运行时无法通过名称设置或需要特殊处理的属性。

以下视图和视图控制器都已经过测试,并已知可以正确运行

  • UIButton
  • UICollectionView
  • UICollectionViewCell
  • UICollectionViewController
  • UIControl
  • UIImageView
  • UILabel
  • UINavigationController
  • UIProgressView
  • UIScrollView
  • UISearchBar
  • UISegmentedControl
  • UISlider
  • UIStackView
  • UIStepper
  • UISwitch
  • UITabBarController
  • UITableView
  • UITableViewCell
  • UITableViewController
  • UITableViewHeaderFooterView
  • UITextField
  • UITextView
  • UIView
  • UIViewController
  • UIVisualEffectView
  • UIWebView
  • WKWebView

如果某个视图未在此列表中,它可能在某种程度上工作,但可能需要使用原生 Swift 代码部分配置。如果遇到此类情况,请在 Github 上报告,以便我们可以在未来更好地支持它们。

要程序化配置视图,请先在其 XML 文件中创建一个出口

<SomeView outlet="someView"/>

然后您可以在视图控制器中执行配置

@IBOutlet weak var someView: SomeView? {
    didSet {
        someView?.someProperty = foo
    }
}

在某些情况下,标准的 UIKit 视图和控制器已被扩展,增加了额外的属性或行为,以帮助它们更好地与 Layout 交互。以下列出这些情况

UIControl

UIControl 由于动作绑定方式的原因,需要对它进行特殊处理。每个 UIControl 都有一个 addTarget(_:action:for:) 方法,用于将方法绑定到特定事件。由于布局仅限于设置属性,因此没有直接调用此方法的方式,所以动作通过以下伪属性暴露给布局

  • touchDown
  • touchDownRepeat
  • touchDragInside
  • touchDragOutside
  • touchDragEnter
  • touchDragExit
  • touchUpInside
  • touchUpOutside
  • touchCancel
  • valueChanged
  • primaryActionTriggered
  • editingDidBegin
  • editingChanged
  • editingDidEnd
  • editingDidEndOnExit
  • allTouchEvents
  • allEditingEvents
  • allEvents

这些属性的数据类型为 Selector,可以设置为你的视图控制器中的方法名称。有关更多详细信息,请参阅上面操作部分。

UIButton

UIButton 根据当前 UIControlState 改变的各种外观属性,但指定这些属性的 API 是方法而非属性,因此不能直接暴露给布局。相反,布局为每个状态提供了伪属性

为所有状态设置

  • title
  • attributedTitle
  • titleColor
  • titleShadowColor
  • image
  • backgroundImage

为特定状态设置,其中 [state] 可以是 normalhighlighteddisabledselectedfocused

  • [state]Title
  • [state]AttributedTitle
  • [state]TitleColor
  • [state]TitleShadowColor
  • [state]Image
  • [state]BackgroundImage

UISegmentedControl

UISegmentedControl 包含多个段,每个段都可以显示图像或标题。这通过 init(items:) 构造函数设置,该构造函数接受一个包含 String 或 UIImage 元素的数组。

布局通过 items 表达式来暴露这一点。你可以将其设置为以下标题数组的值

<UISegmentedControl items="'First', 'Second', 'Third'"/>

这适用于字符串,但是目前布局表达式中无法在数组内部指定图像字面量,因此要想为你的段项使用图像,你需要以常量或状态变量的形式在 Swift 中程序化地创建它们,并将它们传递给布局

<UISegmentedControl items="hello, world"/>
loadLayout(
    named: "MyLayout.xml",
    constants: [
        "hello": UIImage(named: "HelloIcon"),
        "world": UIImage(named: "WorldIcon"),
    ]
)

UISegmentedControl 还具有插入、删除或更新段标题和图像的方法,但此 API 不适合与布局一起使用,因此 items 数组作为伪属性暴露,可以随时更新。在下面的示例中,在 Swift 中更改 segmentItems 状态会更新显示的段

<UISegmentedControl items="segmentItems"/>
loadLayout(
    named: "MyLayout.xml",
    state: [
        "segmentItems": ["Hello", UIImage(named: "HelloIcon")],
    ]
)

...

layoutNode?.setState(["segmentItems": ["Goodbye", UIImage(named: "GoodbyeIcon")]], animated: true)

UIButton 一样,UISegmentedControl 也有根据 UIControlState 变化的样式属性,这些属性以相同的方式支持,使用伪属性。

为所有状态设置

  • backgroundImage
  • dividerImage
  • titleColor
  • titleFont

为特定状态设置,其中 [state] 可以是 normalhighlighteddisabledselectedfocused

  • [state]BackgroundImage
  • [state]TitleColor
  • [state]TitleFont

注意:由于命名约定限制,目前不支持为不同状态设置 dividerImage。此外,目前也无法为不同的 UIBarMetrics 值设置不同的图片。

您可以使用以下方式设置所有段落的内边距:

  • contentPositionAdjustment
  • contentPositionAdjustment.horizontal
  • contentPositionAdjustment.vertical

或者,对于特定的段落,其中 [segment] 可以是以下之一:anyleftcenterrightalone

  • [segment]ContentPositionAdjustment
  • [segment]ContentPositionAdjustment.horizontal
  • [segment]ContentPositionAdjustment.vertical

UIStepper

UIButtonUISegmentedControl 类似,UIStepper 也具有基于状态的伪属性

为所有状态设置

  • backgroundImage
  • dividerImage
  • incrementImage
  • decrementImage

为特定状态设置,其中 [state] 可以是 normalhighlighteddisabledselectedfocused

  • [state]BackgroundImage
  • [state]IncrementImage
  • [state]DecrementImage

UIStackView

您可以使用布局的表达式创建任意复杂的视图排列,但有时描述兄弟之间关系的表达式可能相当冗长,并且能够使用类似于 flexbox 的某个东西来描述视图集合的整体排列会更好。

布局支持 UIKit 的 UIStackView 类,您可以在需要 UITableViewUICollectionView 可能过于繁琐的情况下使用类似 flexbox 的集合。下面是一个简单的垂直堆叠示例

<UIStackView
    alignment="center"
    axis="vertical"
    spacing="10">
    
    <UILabel text="First row"/>
    <UILabel text="Second row"/>
</UIStackView>

嵌套在 UIStackView 中的子视图节点将自动添加到 arrangedSubviews 数组中。对于 UIStackView 的子项,会尊重 widthheight 属性,但会忽略 topleftbottomrightcenter.xcenter.y 表示。

由于 UIStackView 是一个非绘制视图,因此只能配置其位置和布局属性。继承自 UIView 的属性,如 backgroundColorborderWidth 不可用。

UITableView

您可以使用 Layout 模板中的 UITableView,就像您会使用任何其他视图一样

<UITableView
    backgroundColor="#fff"
    outlet="tableView"
    style="plain"
/>

tableView 的 delegatedataSource 将自动绑定到文件的所有者,这通常是您的 UIViewController 子类或第一个嵌套的视图控制器,该控制器遵循一个或两个 UITableViewDelegate/DataSource 协议。如果您不需要这种行为,您可以显式设置它们(见上述 代理 部分)。

您将定义 Layout 管理的表格的视图控制器逻辑,这与您不使用 Layout 时几乎相同

class TableViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {

    @IBOutlet var tableView: UITableView? {
        didSet {

            // Register your cells after the tableView has been created
            // the `didSet` handler for the tableView property is a good place
            tableView?.register(MyCellClass.self, forCellReuseIdentifier: "cell")
        }
    }

    var rowData: [MyModel]

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return rowData.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell =  tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! MyCellClass
        cell.textLabel.text = rowData.title
        return cell
    }
}

使用基于布局的 UITableViewCell 也是可能的。在 XML 中定义 UITableViewCell 有两种方式——直接在表 XML 内部,或者在一个独立的文件中。在表 XML 内部定义的单元格模板可能看起来像这样。

<UITableView
    backgroundColor="#fff"
    outlet="tableView"
    style="plain">

    <UITableViewCell
        reuseIdentifier="cell"
        textLabel.text="{title}">

        <UIImageView
            top="50% - height / 2"
            right="100% - 20"
            width="auto"
            height="auto"
            image="{image}"
            tintColor="#999"
        />
    </UITableViewCell>

</UITableView>

然后您的表视图控制器中的逻辑将是这样。

class TableViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
    var rowData: [MyModel]

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return rowData.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        // Use special Layout extension method to dequeue the node rather than the view itself
        let node = tableView.dequeueReusableCellNode(withIdentifier: "cell", for: indexPath)

        // Set the node state to update the cell
        node.setState(rowData[indexPath.row])

        // Cast the node view to a table cell and return it
        return node.view as! UITableViewCell
    }
}

或者,您也可以在其自己的 XML 文件中定义单元格。如果您这样做,队列处理过程将相同,但您需要手动注册它。

class TableViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {

    @IBOutlet var tableView: UITableView? {
        didSet {
            // Use special Layout extension method to register the layout xml file for the cell
            tableView?.registerLayout(named: "MyCell.xml", forCellReuseIdentifier: "cell")
        }
    }

    ...
}

布局支持动态表格单元格高度计算。要启用此功能,只需为您的单元格设置一个高度表达式。动态表格单元格尺寸也要求将表格视图的 rowHeight 设置为 UITableViewAutomaticDimension 并提供一个非零的 estimatedRowHeight 值,但布局默认为您设置这些。

注意:如果您的单元格都拥有相同的宽度,则相对于为每个单元格设置高度,在 UITableView 上显式设置 rowHeight 属性会显著提高效率。

布局还支持使用 XML 布局为 UITableViewHeaderFooterView,并且有对应的方法用于注册和取回 UITableViewHeaderFooterView 布局节点。

注意:要使用自定义的节标题或页脚,您需要在 XML 中将 estimatedSectionHeaderHeightestimatedSectionFooterHeight 设置为一个非零值。

<UITableView estimatedSectionHeaderHeight="20">

    <UITableViewHeaderFooterView
        backgroundView.backgroundColor="#fff"
        height="auto + 10"
        reuseIdentifier="templateHeader"
        textLabel.text="Section Header"
    />
    
    ...

</UITableView>

如果您愿意,可以在 XML 中创建一个 <UITableViewController/> 而不是通过子类化 UIViewController 并实现表数据源和代理。请注意,如果您这样做,不需要显式创建 UITableView,因为 UITableViewController 已经包含了一个。要配置表,您可以直接使用 tableView. 前缀设置控制器的表视图属性,例如。

<UITableViewController
    backgroundColor="#fff"
    tableView.separatorStyle="none"
    tableView.contentInset.top="20"
    style="plain">

    <UITableViewCell
        reuseIdentifier="cell"
        textLabel.text="{title}"
    />
</UITableViewController>

Collections View

布局以与 UITableView 相似的方式支持 UICollectionView。如果您没有指定自定义的 UICollectionViewLayout,布局默认您想要使用 UICollectionViewFlowLayout,并为您自动创建一个。当使用 UICollectionViewFlowLayout 时,您可以使用带有前缀 collectionViewLayout. 的集合视图上的表达式来配置其属性。

<UICollectionView
    backgroundColor="#fff"
    collectionViewLayout.itemSize.height="100"
    collectionViewLayout.itemSize.width="100"
    collectionViewLayout.minimumInteritemSpacing="10"
    collectionViewLayout.scrollDirection="horizontal"
/>

UITableView 一样,集合视图的 delegatedataSource 将会自动绑定到文件的所有者。使用基于布局的 UICollectionViewCell,无论是直接在您的集合视图 XML 中还是在独立的文件中,工作方式也是相同的。在集合视图 XML 内部定义的单元格模板可能看起来像这样。

<UICollectionView
    backgroundColor="#fff"
    collectionViewLayout.itemSize.height="100"
    collectionViewLayout.itemSize.width="100">

    <UICollectionViewCell
        clipsToBounds="true"
        reuseIdentifier="cell">

        <UIImageView
            contentMode="scaleAspectFit"
            height="100%"
            width="100%"
            image="{image}"
            tintColor="#999"
        />
    </UICollectionViewCell>

</UICollectionView>

然后在您的集合视图控制器中的逻辑将是这样的。

class CollectionViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
    var itemData: [MyModel]

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return itemData.count
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

        // Use special Layout extension method to dequeue the node rather than the view itself
        let node = collectionView.dequeueReusableCellNode(withIdentifier: "cell", for: indexPath)

        // Set the node state to update the cell
        node.setState(itemData[indexPath.row])

        // Cast the node view to a table cell and return it
        return node.view as! UICollectionViewCell
    }
}

或者,您也可以在其自己的 XML 文件中定义单元格。如果您这样做,队列处理过程将相同,但您需要手动注册它。

class CollectionViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
    var itemData: [MyModel]

    @IBOutlet var collectionView: UICollectionView? {
        didSet {
            // Use special Layout extension method to register the layout xml file for the cell
            collectionView?.registerLayout(named: "MyCell.xml", forCellReuseIdentifier: "cell")
        }
    }

    ...
}

也支持动态集合单元格尺寸计算。要启用此功能,只需为您的单元格设置宽度和高度表达式。如果您的单元格都拥有相同的尺寸,则相对于在 UICollectionView 上设置显式的 collectionViewLayout.itemSize 更有效率。

布局目前不支持使用 XML 来定义副表的 UICollectionReusableView 实例,但将在未来的版本中添加。

布局支持使用 UICollectionViewController,使用相同的注意事项,如同 UITableViewController

UIVisualEffectView

UIVisualEffectView具有一个类型为UIVisualEffecteffect属性。UIVisualEffect是一个抽象基类,通常不直接使用——相反,通常会将其设置为一个UIBlurEffectUIVibrancyEffect(后者本身包含一个UIBlurEffect)。

可以通过编程方式设置effect属性,或通过将UIVisualEffect实例作为常量或状态变量传递给您的LayoutNode

loadLayout(
    named: "MyLayout.xml",
    constants: [
        "blurEffect": UIBlurEffect(style: .regular),
    ]
)
<UICollectionView
    effect="blurEffect"
>

为了方便起见,Layout还允许您使用表达式直接配置效果。在effect表达式中,使用UIBlurEffect(style)UIVibrancyEffect(style)构造函数配置效果,如下所示

<UICollectionView
    effect="UIVibrancyEffect(light)"
>

style参数的类型为UIBlurEffectStyle,对于UIBlurEffectUIVibrancyEffect都受支持。您可以使用常量或状态变量来设置样式,或者将其设置为以下内置值之一

  • extraLight
  • light
  • dark
  • extraDark
  • regular
  • prominent

注意:您也可以使用此解决方案设置UITableView.separatorEffect属性或其他任何类型为UIVisualEffect的属性,该属性在自定义视图或控制器中公开。

UIWebView

UIWebView的API使用加载内容的方法,这些方法不能直接从XML中直接使用,因此Layout将这些方法作为属性公开。要加载URL,您可以使用request属性,如下所示

<UIWebView request="http://apple.com"/>

<UIWebView request="{urlRequestConstant}"/>

可以使用一个字面量URL字符串或包含URLURLRequest值的常量或状态变量。request参数也可用于本地文件内容。路径没有方案或以/开头被认为是相对于应用程序资源包,而以~/开始的是相对于用户目录

<!-- bundled resource -->
<UIWebView request="pages/index.html"/>

<!-- user document -->
<UIWebView request="~/Documents/homepage.html"/>

要加载字面量HTML字符串,您可以使用htmlString属性

<UIWebView htmlString="&lt;p&gt;Hello World&lt;/p&gt;"/>

<UIWebView htmlString="{htmlConstant}"/>

注意:如果您的Layout XML中指定了字面量htmlString属性,那么您将必须使用&lt;&gt;&quot;对这些标签进行编码。更好的替代方案是使用Layout的嵌入式HTML功能(如富文本字符串部分所述)。

<UIWebView>
    <p>Hello World</p>
</UIWebView>

与标签不同,网络视图可以显示包括CSS样式和JavaScript在内的任意HTML。但是,由于需要转义<&{字符,因此在XML中内联定义CSS或JavaScript块可能有些尴尬。将复杂的脚本或样式表放在单独的本地文件中可能更容易(尽管目前Layout不支持此类文件的实时重新加载)。

UIWebView.loadHTMLString()方法还接受一个用于HTML中相对URL的baseURL参数。Layout将其作为单独的baseURL属性公开。

<UIWebView
    baseURL="http://example.com"
    htmlString="&lt;img href=&quot;/someImage.jpg&quot;&gt;"
/>

如果您需要调整网络视图的内容边距,您可以通过scrollView属性来完成此操作。

<UIWebView
    scrollView.contentInsetAdjustmentBehavior="never"
    scrollView.contentInset.bottom="safeAreaInsets.bottom"
    scrollView.scrollIndicatorInsets="scrollView.contentInset"
    request="..."
/>

WKWebView

布局支持与UIWebView类似的方式使用WKWebView,通过将各种加载方法转换成属性。除了上述的scrollViewrequesthtmlStringbaseURL属性外,对于WKWebView,Layout还添加了fileURLreadAccessURL属性,用于安全访问本地网络内容。

<WKWebView
    readAccessURL="~/Documents"
    fileURL="~/Documents/homepage.html"
/>

Layout还公开了WKWebViewconfiguration属性。这是一个只读属性,但您可以在构建视图时使用常量值设置它,或者使用表达式逐个配置属性。

<WKWebView
    configuration="baseConfiguration"
    request="..."
/>

<WKWebView
    configuration.allowsAirPlayForMediaPlayback="true"
    configuration.allowsInlineMediaPlayback="false"
    request="..."
/>

UITabBarController

通常来说,布局在每屏实现效果最佳,每个屏幕提供一个LayoutLoading视图控制器。尽管如此,仅作为示例,Layout也提供了基本的对定义诸如UITabBarController这样的集合视图控制器的支持。

要定义基于XML的UITabBarController布局,在某些UITabBarController节点内嵌套一个或多个UIViewController节点,如下所示:

<UITabBarController>
    <UIViewController ... />
    <UIViewController ... />
    ... etc
</UITabBarController>

每个UIViewController都有一个tabBarItem属性,用于当视图控制器嵌套在UITabBarController内时配置标签的外观。Layout提供了此对象及其属性,并通过表达式进行配置。

<UITabBarController>
    <UIViewController
        tabBarItem.title="Foo"
        tabBarItem.image="Bar.png"
    />
    ...
</UITabBarController>

tabBarItem具有以下可以设置的子属性:

  • title
  • image
  • selectedImage
  • systemItem
  • badgeValue
  • badgeColor (iOS 10+ only)
  • titlePositionAdjustment

systemItem属性将覆盖标题和图像。它可以设置为以下常量值中的任意一个:

  • more
  • favorites
  • featured
  • topRated
  • recents
  • contacts
  • history
  • bookmarks
  • search
  • downloads
  • mostRecent
  • mostViewed

在不通过子类化和覆盖tabBar属性的情况下,无法替换UITabBarControllerUITabBar,但是您可以通过向<UITabBarController/>添加一个<UITabBar/>节点来在Layout中自定义标签栏。

<UITabBarController>
    <UITabBar
        backgroundImage="Background.png"
        barStyle="default"
        isTranslucent="false"
    />
    ...
</UITabBarController>

以下属性和伪属性表达式对<UITabBar/>可用:

  • barStyle
  • barPosition
  • barTintColor
  • isTranslucent
  • tintColor
  • unselectedItemTintColor (iOS 10+ only)
  • backgroundImage
  • selectionIndicatorImage
  • shadowImage
  • itemPositioning
  • itemWidth
  • itemSpacing

UINavigationController

UINavigationController并不特别适合使用Layout范式,因为它表示一组可以变动的ViewController堆栈,而Layout的XML文件只能描述静态的层级。

尽管如此,您可以使用Layout来指定导航控制器中初始的ViewController堆栈,之后可以通过程序进行更新。

<UINavigationController>
    <UIViewController
        title="Root View"
    />
    <UIViewController
        title="Middle View"
    />
    <UIViewController
        title="Topmost View"
    />
</UINavigationController>

与标签栏类似,导航栏不是直接配置的,而是通过每个UIViewControllernavigationItem属性间接配置。Layout将以如下方式展示此对象及其属性:

<UINavigationController>
    <UIViewController
        navigationItem.title="Form"
        navigationItem.leftBarButtonItem.title="Submit"
        navigationItem.leftBarButtonItem.action="submit:"
    />
    ...
</UINavigationController>

navigationItem具有以下子属性可设置:

  • title
  • prompt
  • titleView
  • hidesBackButton
  • leftBarButtonItem
  • leftBarButtonItems
  • rightBarButtonItem
  • rightBarButtonItems
  • leftItemsSupplementBackButton

许多这些属性只能通过常量或状态变量来设置,因为无法在表达式中为它们创建字面量值。然而,leftBarButtonItemrightBarButtonItem可以通过以下子属性直接进行操作:

  • title
  • image
  • systemItem
  • style
  • action
  • width
  • tintColor

action属性是一个选择器,应与拥有视图控件的某个方法匹配。类似于UIControl,目前无法显式设置目标。

style属性是一个枚举,接受plain(默认值)和done作为其值。`systemItem`属性覆盖标题和图像,可以设置为以下任何常量值:

  • done
  • cancel
  • edit
  • save
  • add
  • flexibleSpace
  • fixedSpace
  • compose
  • reply
  • action
  • organize
  • bookmarks
  • search
  • refresh
  • stop
  • camera
  • trash
  • play
  • pause
  • rewind
  • fastForward
  • undo
  • redo
  • pageCurl

在构建时,也可以通过提供自定义子类来自定义UINavigationController的导航栏和工具栏。此功能在Layout中通过构造器表达式暴露。

<UINavigationController
    navigationBarClass="MyNavigationBar"
    toolbarClass="MyToolbar">
    ...
</UINavigationController>

或者,要自定义导航栏或工具栏的属性,可以直接在UINavigationController内部包含<UINavigationBar/><UIToolbar/>节点,如下所示:

<UINavigationController>
    <UINavigationBar
        backgroundImage="Background.png"
        barStyle="default"
        isTranslucent="false"
    />
    ...
</UINavigationController>

以下属性和伪属性表达式适用于<UINavigationBar/><UIToolbar/>

  • barStyle
  • barPosition
  • barTintColor
  • isTranslucent
  • tintColor
  • backgroundImage
  • shadowImage

以及以下仅适用于<UINavigationBar/>的:

  • titleColor
  • titleFont
  • titleVerticalPositionAdjustment
  • backIndicatorImage
  • backIndicatorTransitionMaskImage

自定义组件

如上所述的Standard Components部分所述,Layout可以在无需任何特殊支持的情况下自动创建和配置大多数内置UIKit视图和视图控制器,但有些需要额外处理以符合Layout范式。

相同的情况也适用于您自己创建的自定义UI组件。如果您遵循视图接口的标准约定,则大多数情况下这些组件应该能够正常工作,但你可能需要采取一些额外的步骤以实现完全兼容性。

命名空间

正如您可能所知,Swift类的作用域限于特定的模块。如果您有一个名为"MyApp"的应用程序,并声明了一个名为FooView的自定义UIView子类,那么视图的完整类名将是MyApp.FooView,而不是仅为FooView,就像在Objective-C中那样。

布局通过自动使用主模块的命名空间处理您可能遇到的一般情况。以下是您在XML中引用自定义视图的两种方式,任意一种都可行。

<MyApp.FooView/>

<FooView/>

出于避免模板化的考虑,您通常应使用后一种形式。然而,如果您将自定义组件打包到单独的模块中,则您需要使用它们的完整合格名称在XML中进行引用。

自定义属性类型

如前所述,布局使用Objective-C运行时自动检测属性名称和类型,用于表达式。如果您使用Swift 4.0或更高版本,则需要显式地将属性用@objc进行注释,以便在布局中使用它们,因为默认行为现在是不将属性暴露给Objective-C运行时。

即使您用@objc标记了您的属性,Objective-C运行时也仅支持可能Swift类型的一个子集,并且即使对于Objective-C类型,某些运行时信息也会丢失。例如,目前无法在运行时自动检测枚举类型的有效原始值和案例名称集合。

也存在一些情况,其中兼容的属性类型可能以不显示为Objective-C属性的方式实现,或者属性设置器可能与KVC(键值编码)不兼容, resulting in a crash when it is accessed using setValue(forKey:)

为了解决这个问题,可以通过使用扩展手动公开额外的属性和自定义设置器/获取器,以便为视图提供支持。布局框架已经使用此功能公开了许多标准UIKit属性的常量,但是如果您使用第三方组件或创建自己的组件,您可能需要编写扩展以通过布局表达式正确支持配置。

要为自定义视图生成一个兼容布局的属性类型定义和设置器/获取器,请按如下方式创建一个扩展:

extension MyView {

    open override class var expressionTypes: [String: RuntimeType] {
        var types = super.expressionTypes
        types["myProperty"] = RuntimeType(...)
        return types
    }

    open override func setValue(_ value: Any, forExpression name: String) throws {
        switch name {
        case "myProperty":
            self.myProperty = values as! ...
        default:
            try super.setValue(value, forExpression: name)
        }
    }
    
    open override func value(_ value: Any, forSymbol name: String) throws -> Any {
        switch name {
        case "myProperty":
            return self.myProperty
        default:
            return try super.value(value, forSymbol: name)
        }
    }
}

这些重写将"myProperty"添加到该视图已知表达式的列表中,并提供该属性的静态设置器和获取器方法。

注意:设置器使用setValue(_:forExpression:),但获取器使用value(_:forSymbol:)。这是因为并非每个可以在表达式内部读取的符号都可以使用表达式进行设置 - 例如,您可能有一些只读属性,如safeAreaInsets,它们是只读的,因此不需要设置器。只读属性不应包含在expressionTypes字典中。

示例中显示的RuntimeType类是Layout通过Swift类型系统的限制,它用作类型包装器。它可以封装信息,例如可能值列表,这在运行时自动确定是不可能的。

RuntimeType可以用来包装任何Swift类型,例如:

RuntimeType(MyStructType.self)

定义自定义运行时类型的首选方法是作为RuntimeType类的静态变量,通过扩展添加。

extension RuntimeType {
    
    @objc static let myStructType = RuntimeType(MyStructType.self)
}

extension MyView {

    open override class var expressionTypes: [String: RuntimeType] {
        var types = super.expressionTypes
        types["myProperty"] = .myStructType
        return types
    }
    
    ...
}

以这种方式公开运行时类型,可以使其可用于参数,对于枚举类型,它使该类型的所有情况都可以通过类型的命名空间在任意的表达式中使用。请注意,myStructType 属性的名称与类型名称匹配,但以小写字母作为前缀 - 这是有要求的,@objc 属性也是必须的。

布局的 RuntimeType 封装器也可以用来指定一组枚举值

extension RuntimeType {

    @objc static let nsTextAlignment = RuntimeType([
        "left": .left,
        "right": .right,
        "center": .center,
        "justified": .justified,
        "natural": .natural,
    ] as [String: NSTextAlignment])
}

Swift 的枚举值不能使用 Objective-C 运行时自动设置,但如果属性的底层类型与 rawValue(如大多数 Objective-C API 所示)相匹配,则通常没有必要提供自定义的 setValue(forExpression:) 实现方法。您需要根据情况逐一测试才能确定这一点。

选项集可以像枚举一样指定

extension RuntimeType {

    @objc static let uiDataDetectorTypes = RuntimeType([
        "phoneNumber": .phoneNumber,
        "link": .link,
        "address": .address,
        "calendarEvent": .calendarEvent,
        "all": .all,
    ] as [String: UIDataDetectorTypes])
}

对于 Objective-C API,通常不需要为选项集值提供自定义的 setValue(forExpression:) 实现方法,但如果属性的类型在 Swift 中定义为 OptionSet 类型而不是 rawValue 类型,则可能需要这样做。

自定义构造函数参数

默认情况下,布局自动使用 init(frame:) 指定初始化器实例化视图,尺寸为零。但有时视图有替代构造函数,它接受一个或多个不能更改后的参数。在这种情况下,必须手动将此构造函数公开给布局。

要公开自定义视图构造函数,请创建以下扩展

extension MyView {

    open override class var parameterTypes: [String: RuntimeType] {
        return [
            "myArgument": RuntimeType(SomeType.self)
        ]
    }
    
    open override class func create(with node: LayoutNode) throws -> MyView {
        if let myArgument = try node.value(forExpression: "myArgument") as? SomeType {
            self.init(myArgument: myArgument)
            return
        }
        self.init(frame: .zero)
    }
}

注意: 我们在这里覆盖的是 parameterTypes 变量,而不是我们之前实现自定义属性时使用的 expressionTypes 变量。区别在于,parameterTypes 用于仅用于构建视图的表达式,并且之后不能更改。参数表达式在更新 state 时不会重新评估。

create(with:) 方法调用 value(forExpression:) 来获取表达式的值。如果表达式未设置,它将返回 nil,因此不需要单独检查这一点。

在上述示例中,如果未设置参数,我们回退到默认构造函数,但如果我们希望使参数强制性的,我们可以抛出错误

open override class func create(with node: LayoutNode) throws -> MyView {
    guard let myArgument = try node.value(forExpression: "myArgument") as? SomeType else {
        throw LayoutError("myArgument is required")
    }
    self.init(myArgument: myArgument)
}

正文文本

布局支持在 XML 文件内使用内联 (X)HTML,这是一个方便的方法来指定属性字符串值(有关详细信息,请参阅 属性字符串 部分)。为了为自定义视图启用此功能,您需要告诉布局 HTML 应用于哪个属性。

这是使用 bodyExpression 类属性完成的。

extension MyView {

    open override class var bodyExpression: String? {
        return "heading"
    }
}

此属性的值必须是已在 expressionTypesparameterTypes 字典中定义的现有属性的名称。属性的必须类型为 StringNSAttributedString

为了方便起见,布局将检测视图是否具有名为 "text"、"attributedText"、"title" 或 "attributedTitle" 的属性,并自动将正文文本映射到该属性。如果您的视图具有匹配这些名称之一的文本属性,则无需重写 bodyExpression

bodyExpression 属性返回 nil 将禁用该视图的嵌入式 HTML 功能。

默认表达式

如果未指定,布局会尝试确定宽度和高度表达式的合理默认值。为此,它会查看各种属性,例如 intrinsicContentSize 和视图是否使用 AutoLayout 约束。然而,这种机制并不总是100% 有效。

对于自定义组件,您可以提供显式的默认表达式以替代使用。这些表达式不仅限于 "width" 和 "height" 表达式,您还可以为主在任何表达式类型提供默认值。

要为您的视图设置默认表达式,请创建一个如下所示的扩展

extension MyView {

    open override class var defaultExpressions: [String: String] {
        return [
            "width": "100%",
            "height": "auto",
            "backgroundColor": "white",
        ]
    }
}

注意: "width" 和 "height" 的默认值几乎总是应设置为 "100%" 或 "auto"。对于具有固定大小的视图,您可能会想要设置特定的数字默认宽度和高度,但通常更好的做法是通过重写 intrinsicContentSize 属性,以便视图在与常规 AutoLayout(而不是布局)一起使用时也能工作

extension MyView {

    open override class var intrinsicContentSize: CGSize {
        return CGSize(
            width: UIViewNoIntrinsicMetric,
            height: 40
        )
    }
}

高级主题

基于布局的组件

如果您正在创建一个使用布局内部组件的库,则为每个组件包装其自己的 UIViewController 子类可能是过度的。

如果您的组件库的用户正在使用布局,则可以将所有组件作为XML文件公开,并允许它们直接使用布局模板或代码进行组合。但是,如果要使库与普通UIKit应用无缝工作,则最好将每个组件作为常规 UIView 子类公开。

为了实现这一点,从 UIView (或 UIControlUIButton 等)子类化,并添加 LayoutLoading 协议。然后您可以像使用视图控制器一样使用 loadLayout(...) 方法

class MyView: UIView, LayoutLoading {

    override init(frame: CGRect) {
        super.init(frame: frame)

        loadLayout(
            named: "MyView.xml",
            state: ...,
            constants: ...,
        )
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()

        // Ensure layout is updated after screen rotation, etc
        self.layoutNode?.view.frame = self.bounds
    }
}

注意:在上面的示例中,在xml中定义的根视图将以MyView的子视图的形式加载,并且自动设置为相同的尺寸。因此,xml中的根视图也最好是MyView的实例,除非你希望你的视图结构是这样的

<MyView>
    <MyView>
        ...
    </MyView>
</MyView>

以这种方式在视图内部尝试加载视图将会引发运行时错误,因为这会导致无限加载循环的风险。

如果布局具有动态尺寸,你可能希望在布局框架更改时更新容器视图的框架。为此,请添加以下代码

class MyView: UIView, LayoutLoading {

    ...
    
    override func layoutSubviews() {
        super.layoutSubviews()

        // Ensure layout is updated after screen rotation, etc
        self.layoutNode?.view.frame = self.bounds
        
        // Update frame to match layout
        self.frame.size = self.intrinsicContentSize
    }
    
    public override var intrinsicContentSize: CGSize {
        return layoutNode?.frame.size ?? .zero
    }
}

LayoutLoading的默认实现会将错误向上冒泡到处理它们的第一个视图或视图控制器。如果响应链中的任何响应者都没有拦截错误,它将显示在Red Box 控制台中。

手动集成

如果你不想使用LayoutLoading协议,你可以通过使用mount(in:)方法手动将LayoutNode挂载到视图或视图控制器中。

class MyViewController: UIViewController {
    var layoutNode: LayoutNode?

    override func viewDidLoad() {
        super.viewDidLoad()

        // Create a layout node from and XML file or data object
        self.layoutNode = try? LayoutNode.with(xmlData: ...)

        // Mount it
        try? self.layoutNode?.mount(in: self)
    }

    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()

        // Ensure layout is updated after screen rotation, etc
        self.layoutNode?.view.frame = self.bounds
    }
}

如果你正在使用一些复杂的架构,如Viper,该架构将视图控制器拆分为子组件,你可能需要将LayoutNode绑定到除了UIViewUIViewController子类之外的对象。在这种情况下,你可以使用bind(to:)方法,该方法将节点的外部接口、操作和代理连接到指定的所有者对象,但不会尝试挂载视图或视图控制器。

mount(in:)bind(to:)方法可能会因为XML标牌有问题、表达式语法或符号错误而抛出错误。

这些错误不会在正确实现的布局中出现,它们通常只会在你代码中出错时发生,因此对于发布版本,应该使用try!try?(假设你在发布前已对应用程序进行了充分测试)来抑制它们。

如果你从外部源加载XML模板,你可能更喜欢捕获并记录这些错误,而不是让它们崩溃或无声失败,因为如果在模板和本地代码独立更新时发生错误,生产中出现错误的可能性更大。

组合

对于大型或复杂的布局,你可能希望将布局拆分为多个文件。当你通过编程创建LayoutNode时,可以将LayoutNode的子树分配给临时变量,这样就可以轻松完成,但XML定义的布局如何呢?

幸运的是,布局有一个很好的解决办法:你XML文件中的任何布局节点都可以包含一个引用外部XML文件的xml属性。这个引用可以指向本地文件,甚至是一个远程URL

<UIView xml="MyView.xml"/>

所引用的XML只是一个普通的布局文件,可以正常加载和使用,但在使用组合特性加载时,它会替换加载它的节点。当加载完成后,原始节点的属性将与外部节点合并。

本地xml文件的加载是同步的,但对于远程URL,加载是异步的,因此原始节点会首先显示,并在外部节点的XML加载完成后更新。

原始节点的任何子节点都将被加载节点的内容替换,因此您可以在内容加载期间插入一个占位符视图。

<UIView backgroundColor="#fff" xml="MyView.xml">
    <UILabel text="Loading..."/>
</UIView>

被引用的XML文件的根节点必须是加载它的节点(或其子类)。例如,您可以替换<UIView/>节点为<UIImageView/>,或者将<UIViewController/>替换为<UITableViewController/>,但您不能将<UILabel/>替换为<UIButton/>,或者将<UIView/>替换为<UIViewController/>

模板

模板是组合的对立面,在OOP中更像类继承。与组合特性一样,模板是一个独立的XML文件,可以导入到您的节点中。但是当一个布局节点导入模板时,节点的小孩会附加到继承的布局中的那些,而不是替换它们。如果您有一组具有共同属性或元素节点,这很有用。

<UIView template="MyTemplate.xml">
    <UILabel>Some unique content</UILabel>
</UIView>

与组合一样,模板本身只是一个普通的布局文件,可以正常加载和使用。

<!-- MyTemplate.xml -->
<UIView backgroundColor="#fff">
    <UILabel>Shared Heading</UILabel>

    <!-- children of the importing node will be inserted here -->
</UIView>

导入的模板的根节点类必须是导入节点的相同类或超类(与组合不同,那里必须相同类或子类)。

如果您的模板具有复杂的内部结构,您可能希望指定子节点将插入的位置,而不是只是将它们附加到现有的顶级子节点。为此,您可以使用<children/>标签。

您可以在模板内部任何位置放置<children/>标签(包括模板节点的子节点中),然后它会由导入节点的子节点替换

<!-- MyTemplate.xml -->
<UIView backgroundColor="#fff">
    <UILabel>Shared Heading</UILabel>
    <UIView>
        <children/> <!-- children of the importing node will be inserted here -->
    </UIView>
    <UILabel>Shared Footer</UILabel>
</UIView>

参数

当使用模板时,您可以设置导入节点的表达式来配置模板的根节点,但这提供了相当有限的个性化控制。理想情况下,您希望能够配置模板内节点的属性,这就是参数出现的地方。

您通过在普通布局节点内添加<param/>节点来定义参数。

<!-- MyTemplate.xml -->
<UIView>
    <param name="text" type="String"/>
    <param name="image" type="UIImage"/>

    <UIImageView image="{image}"/>
    <UILabel text="{text}"/>
</UIView>

每个参数都有一个 nametype 属性。参数定义了一个符号,可以被包含节点或其子节点上定义的任何表达式引用。

可以使用导入节点上的表达式来设置参数

<UIView
    template="MyTemplate.xml"
    text="Lorem ipsum sit dolor "
    image="Rocket.png"
/>

您可以在包含节点上定义一个匹配的表达式来设置参数的默认值。如果导入节点上有相同表达式,则会被覆盖

<!-- MyTemplate.xml -->
<UIView title="Default text">
    <param name="title" type="String"/>
    ...
</UIView>

有时候你会发现自己在一个给定的布局中多次重复同一个表达式。比如,所有的视图可能具有相同的宽度或高度,或者与它们兄弟之间的关系相同。例如

<UIView>
    <UILabel left="20" right="100% - 20" top="20" text="Foo"/>
    <UILabel left="20" right="100% - 20" top="previous.bottom + 20" text="Bar"/>
    <UILabel left="20" right="100% - 20" top="previous.bottom + 20" text="Baz"/>
</UIView>

虽然你可以将数字值作为常量传递到你的布局中,但这对于像 "100%" 或 "previous.bottom" 这样的表达式不适用,其中被引用的符号相对于节点在层次结构中的位置,因此实际值会在每个实例中变化。

布局为此提供了解决方案,以宏的形式。宏是在你的布局模板中定义的可重复使用的表达式。宏可以被包含宏的节点或任何子节点上的表达式引用,但与参数不同,它们不能在外部设置或覆盖,并且它们的值在使用的点上确定,而不是相对于它们被定义的节点。

使用宏,我们可以将上面的示例改为

<UIView>
   <macro name="SPACING" value="20"/>
   <macro name="LEFT" value="SPACING"/>
   <macro name="RIGHT" value="100% - SPACING"/>
   <macro name="TOP" value="previous.bottom + SPACING"/>
   
   <UILabel left="LEFT" right="RIGHT" top="TOP" text="Foo"/>
   <UILabel left="LEFT" right="RIGHT" top="TOP" text="Bar"/>
   <UILabel left="LEFT" right="RIGHT" top="TOP" text="Baz"/>
</UIView>

这消除了重复,使布局更加DRY(Don't Repeat Yourself,避免重复),并更容易重构。

请注意宏使用大写名称 - 这不是必需的,但这是区分宏和普通常量、参数或状态变量的好方法。它还可以避免与现有的视图属性发生命名空间冲突。

忽略文件

每次你在iOS模拟器中运行时加载布局XML文件时,Layout会扫描你的项目目录以查找该文件。这通常很快,但如果你的项目有很多子文件夹,第一次查找XML文件可能需要相当长的时间。

为了加快这个扫描速度,你可以在项目目录中添加一个.layout-ignore文件来告知Layout忽略某些子目录。该文件的格式是一个简单的文件路径列表(每行一个),应该被忽略。你可以使用#表示注释,例如用于分组目的

# Ignore these
Tests
Pods

文件路径相对于放置.layout-ignore文件的文件夹。不支持通配符,如*.xml,不建议使用相对于的路径,如../

搜索从包含你的.xcodeproj的目录开始,但你可以将.layout-ignore文件放置在项目的任何子目录中,并且可以在不同目录中使用多个忽略文件。

布局已经忽略了不可见的文件/文件夹以及以下目录,因此无需包含这些

build
*.build
*.app
*.framework
*.xcodeproj
*.xcassets

.layout-ignore中列出的路径将由LayoutTool也忽略。

示例项目

Layout库中包含了一些示例项目

示例应用

示例应用项目展示了Layout的各种功能。它分为四个标签,整个项目(包括UITabBarController),都是使用Layout XML文件指定的。以下是标签页:

  • 盒子 - 展示了使用状态来管理动画布局的使用
  • 页面 - 展示了使用UIScrollView来创建分页内容
  • 文本 - 展示了Layout的文本功能,包括使用HTML和属性字符串常量
  • 表格 - 展示了Layout对UITableViewUITableViewCell的支持

UIDesigner

UIDesigner项目是一个用于构建布局的实验性所见即所得工具。它是作为一个iPad应用程序编写的,您可以在模拟器或设备上运行它。

UIDesigner目前处于非常初期的开发阶段。它支持Layout XML格式暴露的绝大多数功能,但缺乏导入/导出功能,以及指定常量、参数或出口绑定的能力。

沙盒

Sandbox应用程序是一个用于实验XML布局的简单沙盒。它可以在iPhone或iPad上运行。

与UIDesigner一样,Sandbox应用程序目前没有加载/保存或导入/导出功能,但您可以复制和粘贴XML从编辑屏幕到和。

布局工具

布局项目包含一个名为LayoutTool的命令行应用程序的源代码,该程序提供了一些有用的功能,帮助使用布局进行开发。您无需安装布局工具即可使用布局,但它可能很有帮助。

安装

LayoutTool的最新构建二进制文件包含在项目中的LayoutTool目录内,您可以将其拖放到指定位置进行安装。

为了确保兼容性,始终在更新布局框架的同时更新LayoutTool,因为使用旧版本的LayoutTool处理包含较新功能的XML文件可能会导致数据丢失或损坏。

注意:LayoutTool的二进制文件只有在有影响其行为的更改时才会更新,因此如果您发现版本不完全匹配,不必担心。

要使用CocoaPods自动将布局工具安装到您的项目中,请在Podfile中添加以下内容

pod 'Layout/CLI'

这将安装布局工具二进制文件到您的项目文件夹中的Pods/Layout/LayoutTool目录内。您可以在其他脚本中引用此路径。

格式化

LayoutTool提供的主要功能是对Layout XML文件进行自动格式化。使用LayoutTool format命令,您可以在指定的路径(或路径列表)中查找Layout XML文件并应用标准格式。您可以如下使用此工具

> LayoutTool format /path/to/xml/file(s) [/another/path]

要获取更多信息,请使用LayoutTool help

要使LayoutTool在每次构建项目时自动应用格式化,您可以将应用该工具的运行脚本构建阶段添加到项目中。假设您已使用CocoaPods安装了LayoutTool CLI,该脚本可能看起来像这样

"${PODS_ROOT}/Layout/LayoutTool/LayoutTool" format "${SRCROOT}/path/to/your/layout/xml/"

LayoutTool应用的格式设计专用于Layout文件。最好使用布局工具格式化这些文件,而不是使用通用的XML格式化工具。

相反,LayoutTool仅适用于格式化布局 XML文件。它不是通用的XML格式化工具,当应用到任意XML时,可能不会按预期表现。

LayoutTool会忽略看起来不属于Layout的XML文件,但如果您的项目包含非Layout XML文件,则从LayoutTool format命令中排除这些路径是一个好主意,以提高格式化性能并避免意外误报。

为了安全地确定将应用格式的文件,而不覆盖任何内容,您可以使用 LayoutTool list 来显示LayoutTool在您的项目中可以找到的所有Layout XML文件。

重命名

LayoutTool还提供了一个功能,可以在一个或多个Layout XML模板中重命名类或表达式变量。请按以下方式使用它:

"${PODS_ROOT}/Layout/LayoutTool/LayoutTool" rename "${SRCROOT}/path/to/your/layout/xml/" oldName newName

仅影响表达式中的类名和值。属性(即表达式名称)被忽略,以及HTML元素和字面字符串片段。

注意:执行重命名也会对文件应用标准格式。目前无法禁用此功能。

字符串

LayoutTool的strings命令会打印在您的Layout XML模板中引用的所有Localizable.strings常量。请按以下方式使用它:

"${PODS_ROOT}/Layout/LayoutTool/LayoutTool" strings "${SRCROOT}/path/to/your/layout/xml/"

Xcode扩展

如果您在Xcode中编写Layout XML,您可能希望安装Layout Xcode编辑器扩展,它可以在Xcode IDE中直接提供[LayoutTool]功能的一个子集。

安装

包含在项目中的EditorExtension目录内的最新构建的二进制文件Layout for Xcode,您可以简单地将其拖放到应用程序文件夹中以安装。

安装后,运行Layout for Xcode应用程序并遵循屏幕上的说明。

为了确保兼容性,始终在更新Layout框架的同时更新Layout for Xcode应用程序,因为在包含较新功能的XML文件中使用较旧的Layout for Xcode版本可能会导致数据丢失或损坏。

注意:只有在其行为有更改时,才会更新Layout for Xcode应用程序,因此如果版本不匹配请不要担心。

格式化

当你在Xcode中打开布局XML文件时,选择 编辑 > 布局 > 格式XML 菜单以重新格式化该文件。

常见问题

问:这与像React Native这样的框架有什么不同?

React Native是一个完整的跨平台替代品,用于原生iOS和Android开发,而Layout是为更简单构建普通iOS UIKit应用而存在的方法。特别是,Layout与原生UIKit控制紧密结合,使用自定义控件时需要更少的样板代码,并且可以直接与您现有的原生Swift代码一起使用。

问:这与像Render这样的框架有什么不同?

编程模型非常相似,但Layout的运行时表达式语言意味着你可以在不重新启动模拟器的情况下完成更多UI开发。

问:Layout使用Flexbox吗?

不。Layout要求您明确使用top/left/width/height属性定位每个视图,但它的基于百分比的单位和自动调整大小功能使您能够用最少的代码创建复杂的布局。您还可以在Layout模板中使用iOS的本地flexbox样式的UIStackView

问:为什么Layout使用XML而不是像JSON这样的更现代的格式?

XML更适合表示文档类结构,如视图层次结构。JSON的语法中不区分节点类型、属性和子节点,因此当表示层次结构时会产生很多额外的冗余,因为每个节点都必须包含表示“类型”和“子节点”的键或等效键。JSON也不支持注释,这在复杂布局中很有用。虽然XML并不完美,但它是iOS内置支持的格式中最合适的一种。

问:我真的必须用XML编写布局吗?

您可以在代码中手动创建LayoutNode,但XML是目前推荐的途径,因为它可以使用实时预加载功能。

问:Layout是App Store安全的吗?它已经在生产中使用过吗?

是的,我们已经提交了使用Layout的应用程序到App Store,并且它们被批准没有问题。

问:支持哪些平台?

Layout适用于iOS 9.0及以上版本。目前不支持其他Apple OS(tvOS、watchOS、macOS),也不支持Android或Windows等竞争平台。

问:Layout将来会支持watchOS/tvOS吗?

目前没有计划,但它应该很容易添加对iOS衍生平台的支持。如果您需要这样做,请创建一个pull request,其中包括使Layout能构建在这些平台上的必要更改。

问:Layout将来会支持macOS/AppKit吗?

目前没有计划,但鉴于共享的语言和类似框架,这在未来是有意义的。如果您对此类功能感兴趣,请在GitHub上创建一个issue,讨论您的实施方法。

问:Layout将来会支持Android/Windows吗?

目前没有计划将Layout移植到其他平台。特别是,Android和Windows已经使用易于理解的XML格式来存储它们的视图模板,这减少了对类似Layout替代方案的需求。

问题:为什么Cmd-R不能在我的模拟器中重新加载我的XML文件?

请确保在模拟器中开启了硬件 > 键盘 > 连接硬件键盘选项。

问题:为什么我会得到一个错误,说我自定义的视图类没有被识别?

阅读上述的命名空间部分。

问题:为什么我设置自定义组件属性时会出现错误?

阅读上述的自定义属性类型部分。

问题:我必须使用UIViewController子类来加载我的布局吗?

不。请参阅上述的高级主题部分。

问题:当我在启动我的应用时,Layout让我选择源文件,我选错了,现在我的应用不能正确工作了。我该怎么做?

如果应用能够正常运行,或者显示一个红色框,你可以通过按Cmd-Alt-R重置它。如果它实际上崩溃了,最好的选择是从模拟器中删除应用然后重新安装。