MarkupKit 4.5.1

MarkupKit 4.5.1

测试已测试
语言语言 Obj-CObjective C
许可证 Apache-2.0
发布最后发布2018 年 9 月

Greg Brown 维护。



MarkupKit 4.5.1

  • 作者:
  • Greg Brown

Releases CocoaPods

简介

MarkupKit 是一个开源框架,用于简化原生 iOS 和 tvOS 应用程序的开发。它允许开发者使用一种易于阅读的、类似 HTML 的标记语言声明式地构造用户界面,并在大多数情况下可以作为 XIB 文件或故事板的直接替代品。

例如,以下标记创建了一个 UILabel 实例,并设置其 text 属性的值为 "Hello, World!"

<UILabel text="Hello, World!"/>

这段标记与以下 Swift 代码等价

let label = UILabel()
label.text = "Hello, World!"

除了支持所有系统提供的视图类型之外,MarkupKit 还包含一组使使用自动布局更容易的 UIView 子类。它还支持自定义视图类型(即特定应用程序的视图类型)。

提高生产效率

使用标记构建界面可以显著减少开发时间。例如,下面显示的周期表是使用 MarkupKit 的布局视图和 UILabel 实例的组合构建的

在 Interface Builder 中创建此视图是一个艰巨的任务。使用编程来创建它会更困难。然而,在标记中这几乎是微不足道的。此示例的完整源代码可以在 这里 找到。

简化开发

使用标记有助于提高责任划分的清晰度。一个视图的呈现方面的几乎所有方面(包括模型绑定表达式)都可以在视图声明中指定,让控制器负责仅管理视图的行为。

此外,MarkupKit 的实时预览功能允许开发者在设计时验证视图更新,避免了启动 iOS 模拟器的需要。

本指南介绍了 MarkupKit 框架,并概述了其关键特性。第一章描述了 MarkupKit 文档的结构,并解释了如何在标记中创建和配置视图实例。其余章节介绍了框架中包含的类,并讨论了如何使用它们来帮助简化应用开发。还讨论了对多个 UIKit 类的扩展,这些扩展增强了类行为或将其各自类型适应用于标记。

MarkupKit 需要 iOS 10 或 tvOS 10 或更高版本。最新版本可以从此处下载。它也可通过 CocoaPods 获取。

项目工作区中包含了大量代码示例。项目工作区。有关更多信息、示例以及“Hello World”风格的教程介绍,请参阅 wiki

反馈

欢迎并鼓励反馈。请随时联系我,提出任何问题、评论或建议。此外,如果您喜欢使用 MarkupKit,请考虑为其加星

内容

获取 MarkupKit

MarkupKit 的安装和配置方法说明

  • 下载最新的版本存档并解压
  • 在 Xcode 中,在项目导航视图中选择项目根节点
  • 选择应用程序目标
  • 选择“通用”选项卡
  • MarkupKit.framework 拖入“嵌入的搜索二进制”部分
  • 出现对话框时,确保已勾选“根据需要复制项”,然后单击“完成”

请注意,提交到 App Store 之前,框架二进制文件必须“减缩”(Trimmed)。有关更多信息,参见维基百科中的 部署 部分。

文档结构

MarkupKit 通过 XML 定义用户界面的结构。XML 文档的分层性质与 iOS 应用的视图层次结构相对应,这使得理解视图之间的关系变得容易。

元素

MarkupKit 文档中的元素通常表示 UIView 或其子类的实例。当 XML 解析器读取元素时,会动态实例化相应的视图类型并将它们添加到视图层次结构中。

例如,以下标记创建了包含 UIImageViewUILabelLMColumnView 实例。 LMColumnView 是 MarkupKit 提供的 UIView 子类,可以自动将子视图垂直排列

<LMColumnView>
    <UIImageView image="world.png"/>
    <UILabel text="Hello, World!"/>
</LMColumnView>

下面的示例使用程序化方法达到相同的效果

let columnView = LMColumnView()
    
let imageView = UIImageView()
imageView.image = UIImage(named: "world.png")
    
columnView.addSubview(imageView)
    
let label = UILabel()
label.text = "Hello, World!"
    
columnView.addSubview(label)

尽管这两个示例产生相同的结果,但是标记版本更为简洁且易于阅读。

MarkupKit 为 UIView 类添加以下方法以方便从标记构建视图层次结构

- (void)appendMarkupElementView:(UIView *)view;

在文档中声明的每个视图的父视图中调用此方法将视图添加到其父级中。默认实现不执行任何操作;子类必须重写此方法以实现视图特定的行为。例如,LMColumnView 通过调用 addSubview: 在自身上实现 appendMarkupElementView:

注意,如果视图的类型在模块中定义,则必须在视图声明中使用完全限定类名;例如

<MyApp.MyCustomView/>

未编类型的元素

除了视图实例外,元素还可以表示未编类型的(Untyped)数据。例如,UISegmentedControl 的文本内容是通过其 insertSegmentWithTitle:atIndex:animated: 方法指定的。在 MarkupKit 中,这表示如下

<UISegmentedControl>
    <segment title="Small"/>
    <segment title="Medium"/>
    <segment title="Large"/>
    <segment title="Extra-Large"/>
</UISegmentedControl>

每个<segment>元素都会触发对以下方法的调用,该方法也由MarkupKit添加到UIView中。

- (void)processMarkupElement:(NSString *)tag properties:(NSDictionary<NSString *, NSString *> *)properties;

元素名称“segment”作为tag参数传入,并包含表示“title”属性的键值对在properties字典中。

appendMarkupElementView:一样,processMarkupElement:properties:的默认实现不执行任何操作;子类必须重写它以提供特定于视图的行为。例如,UISegmentedControl会重写此方法来调用自身的insertSegmentWithTitle:atIndex:animated:

属性

MarkupKit文档中的属性通常代表视图属性。例如,以下标记声明了一个系统样式的UIButton实例,并将其title属性设置为“Press Me!”。

<UIButton style="systemButton" title="Press Me!"/>

属性值使用键值编码(KVC)设置。字符串、数字和布尔属性的类型转换由KVC自动处理。其他类型,如颜色、字体、图像和枚举,由MarkupKit具体处理,以下将更详细地讨论。

MarkupKit向NSObject添加以下方法,以帮助应用属性值

- (void)applyMarkupPropertyValue:(nullable id)value forKey:(NSString *)key;
- (void)applyMarkupPropertyValue:(nullable id)value forKeyPath:(NSString *)keyPath;

最终,这些方法委托给NSObjectsetValue:forKey:方法。然而,它们允许实现类重写默认行为,并在实际设置属性值之前执行任何必要的转换(例如,将枚举值的字符串表示转换为它的数字等效物)。

MarkupKit实际上在应用属性值时调用第二个方法,这使得在标记中设置嵌套对象的属性成为可能。例如,以下标记创建一个按钮,其标题标签的font属性设置为“Helvetica-Bold 32”。

<UIButton style="systemButton" title="Press Me!" titleLabel.font="Helvetica-Bold 32"/>

一些属性在MarkupKit中有特殊意义,并不表示属性。它们包括“style”、“class”和“id”。它们各自的作用将在稍后更详细地解释。

此外,以“on”开头的属性名称表示控制事件或“动作”。这些属性的值代表在关联事件被触发时调用的处理方法。例如,此标记创建一个按钮,当按钮被按下时会触发关联的动作

<UIButton style="systemButton" title="Press Me!" onPrimaryActionTriggered="buttonPressed"/>

动作已在以下部分中更详细地讨论。

颜色

任何名称等于或以“color”结尾的属性值在设置属性值之前都转换为UIColor实例。MarkupKit中的颜色可以以几种方式指定:

  • 作为以哈希符号开头的十六进制RGB[A]值;例如,"#ff0000"或"#ffffff66"。
  • 作为命名的颜色;例如,“yellow”。
  • 作为模式图像;例如,“background.png”。

例如,以下标记创建一个标签,其文本颜色设置为“#ff0000”,或明亮的红色。

<UILabel text="A Red Label" textColor="#ff0000"/>

此标记创建一个视图,其背景颜色设置为半透明的白色

<LMColumnView backgroundColor="#ffffff66">
    ...
</LMColumnView>

命名颜色

命名颜色值可以引用设置在资产库中的颜色、应用程序颜色表中的条目,或由《UIColor》类定义的颜色常量。颜色表是一个可选的键值对集合,定义在名为《Colors.plist》的文件中。如果存在,此文件必须位于应用程序的主捆绑包中。表的键代表颜色名称,值是相关的RGB[A]值。名称可以用作整个应用程序中的占位符,代替实际的十六进制值。

例如,以下属性列表定义了一个名为“darkRed”的颜色

<plist version="1.0">
<dict>
    <key>darkRed</key>
    <string>#8b0000</string>
</dict>
</plist>

此标记创建一个《UILabel》实例,其文本颜色将设置到属性列表中“darkRed”所代表的值,即“#8b0000”

<UILabel text="A Dark Red Label" textColor="darkRed"/>

命名颜色还可以引用《UIColor》的颜色预置方法,例如《darkGrayColor》。值是访问器方法的名称,去掉“Color”后缀。

例如,以下标记将产生一个系统风格的按钮,其高亮颜色被设置为《greenColor》方法返回的值

<UIButton style="systemButton" tintColor="green"/>

命名颜色按以下顺序进行评估

  • 颜色集
  • 颜色表
  • 颜色常量

例如,如果《Colors.plist》定义了“green”的值,则将使用相应的颜色而不是《greenColor》返回的值。

图案图像

图案图像通过提供用作重复平铺的图像名称来指定。例如,此标记创建一个具有“tile.png”平铺背景图像的表格视图

<LMTableView backgroundColor="tile.png">
    ...
</LMTableView>

图像将在下面进行更详细的讨论。

字体

任何属性名称等于或以“font”结尾的值,在设置属性值之前将转换为《UIFont》实例。《MarkupKit》中的字体可以用以下两种方式之一指定

  • 作为一个显式命名的字体,使用字体的完整名称后跟一个空格和字体大小
  • 作为一个文本样式;例如,“body”

例如,以下标记创建一个其字体被设置为24点Helvetica的《UILabel》

<UILabel text="This is Helvetica 24 text" font="Helvetica 24"/>

可以通过使用“System”作为字体名称来指定当前系统字体。“System-Bold”和“System-Italic”也受到支持。

使用名称为样式常量的名称来表示文本样式,名称为样式常量的名称减去前导“UIFontTextStyle”前缀,以首字母小写开始。例如

<UILabel text="This is headline text" font="headline"/>

图片

任何属性名等于或以"image"结尾的值,在设置属性值之前都会转换为 UIImage 实例。例如,以下标记创建了一个 UIImageView 实例,并将其 image 属性设置为名为 "background.png" 的图片

<UIImageView image="background.png"/>

通常使用 UIImage 类的 imageNamed:inBundle:compatibleWithTraitCollection: 方法来加载图片。属性值作为此方法的第一个参数传递。

如果文档的所有者(通常为视图控制器)实现了名为 bundleForImages 的方法,第二个参数将包含此方法返回的值。MarkupKit 将 bundleForImages 方法的默认实现添加到 UIResponder 中,返回加载视图的包。子类可以覆盖此方法以提供自定义的图片加载行为。如果所有者没有实现 bundleForImages,将使用主包。

最后,如果所有者遵守了 UITraitEnvironment 协议,第三个参数将包含 traitCollection 方法返回的值。否则,它将是 nil

枚举类型

枚举类型不是 KVC 自动处理的。但是 MarkupKit 为 UIKit 常用的枚举提供了翻译。例如,以下标记创建了一个 UITextField 实例,仅在用户编辑时显示清除按钮,并呈现适合输入电子邮件地址的软件键盘

<UITextField placeholder="Email Address" clearButtonMode="whileEditing" keyboardType="emailAddress"/>

MarkupKit 中的枚举值是其 UIKit 对应物的缩写。属性值只是枚举值的全称减去前缀类型名,并将首字符转换为小写。例如,上述示例中的 "whileEditing" 对应于 UITextFieldViewMode 枚举的 UITextFieldViewModeWhileEditing 值。同样,"emailAddress" 对应于 UIKeyboardType 枚举的 UIKeyboardTypeEmailAddress 值。

请注意,属性值根据属性名称而不是其值或相关属性类型转换为枚举类型。例如,以下标记将标签的 text 属性值设置为文字字符串 "whileEditing"

<UILabel text="whileEditing"/>

边距

The UIView 类允许调用者指定在布局其内容时周围为其所有子视图保留的空间量。这个值被称为视图的 "布局边距",由一个 UIEdgeInsets 结构实例表示(或在 iOS 11 及以后版本中的 NSDirectionalEdgeInsets)。

由于边距内边距不是原生支持 KVC 的,MarkupKit 提供了一种简写方法来指定布局边距值。"layoutMargins" 属性接受一个单一的数值,该数值将应用于结构的所有组件。例如,以下标记创建了一个列视图,其顶部、左侧、底部和右侧布局边距设置为 20

<LMColumnView layoutMargins="20">
    ...
</LMColumnView>

还可以使用这种简写方式指定其他几种视图类型的边距属性。例如

<UIButton title="Press Me!" contentEdgeInsets="12"/>

此外,MarkupKit 为这些类型添加了属性,允许单独指定边距组件。例如

<LMColumnView layoutMarginLeft="20" layoutMarginRight="20">
    ...
</LMColumnView>
    
<UIButton title="Press Me!" contentEdgeInsetTop="12" contentEdgeInsetBottom="12"/>

这些扩展将在稍后更详细地讨论。

工厂方法

一些视图无法通过在视图类型上调用 new 方法来实例化。例如,通过调用 buttonWithType: 类方法创建 UIButton 的实例,而通过 initWithFrame:style: 创建 UITableView 实例。

为了处理这些情况,MarkupKit 支持一个名为 "style" 的特殊属性。该属性的值是 "工厂方法" 的名称,这是一个不带参数的类方法,用于产生给定类型的实例。MarkupKit 向如 UIButtonUITableView 这样的类添加了大量的工厂方法,以启用这些类型在标记中构建。

例如,以下标记通过调用 MarkupKit 添加到 UIButton 类中的 systemButton 方法来创建系统样式的 UIButton 的实例

<UIButton style="systemButton" title="Press Me!"/>

在稍后更详细地讨论 MarkupKit 添加到 UIKit 类型中的完整扩展集。

属性模板

通常,在构建用户界面时,将同一组属性值重复应用于给定类型的实例。例如,设计者可能希望所有按钮具有类似的外观。虽然可以在每个按钮实例之间简单地复制属性定义,但这既重复又不能易于以后修改设计 - 必须找到并单独修改每个实例,这可能是耗时且容易出错的。

MarkupKit 允许开发人员将常见的属性定义集合抽象为类似于 CSS 的 "模板",然后可以通过名称将其应用到单个视图实例。这使得分配常见的属性值以及稍后修改它们变得容易得多。

属性模板使用 JavaScript 对象表示法(JSON) 进行指定。每个模板都由一个字典对象表示,该对象定义在 JSON 文档的最高级别。字典的键表示模板的名称,其内容表示在应用模板时将设置的属性值。

通过properties处理指令(PI)将模板添加到MarkupKit文档中。例如,下面的PI定义了一个名为" greeting"的模板,它包含" font"和"textAlignment"属性的设置。

<?properties {
    "greeting": {
        "font": "Helvetica 24", 
        "textAlignment": "center"
    }
}?>

使用保留的"class"属性将模板应用于视图实例。此属性的值指向当前文档中定义的模板名称。模板定义的所有属性值都应用于视图。支持嵌套属性,如"titleLabel.font"。

例如,鉴于 precedents 模板定义,下面的标记将生成一个读作"Hello, World!"的标签,标签的字体为24点的Helvetica,文本水平居中。

<UILabel class="greeting" text="Hello, World!"/>

可以使用模板名称的逗号分隔列表将多个模板应用于视图;例如

<UILabel class="bold, red" text="Bold Red Label"/>

注意,尽管XML中的属性值始终表示为字符串,但模板定义中的属性值可以是任何有效的JSON类型,例如数字或布尔值。然而,这并非绝对必要,因为KVC会自动将字符串转换为适当的类型。

可以在单个标记文档中指定多个properties PI。它们的条目合并为一个单独的模板集合,供文档使用。如果多个PI定义相同的模板,其内容将合并为一个单独的模板字典,其中最近定义的值优先。

outlet

可以使用保留的"id"属性为视图实例分配名称。这创建了一个使视图可访问的"outlet"。使用KVC,MarkupKit会将命名的视图实例"注入"到文档的所有者,允许应用程序与之交互。

例如,下面的标记创建了一个包含UITextField的表格视图单元。文本字段被分配了一个ID为"textField"。

<LMTableView>
    <LMTableViewCell>
        <UITextField id="textField" placeholder="Type something"/>
    </LMTableViewCell>
</LMTableView>

拥有类可能在Objective-C中声明文本字段的outlet,如下所示

@property (nonatomic) IBOutlet UITextField *textField;

或者在Swift中,如下所示

@IBOutlet var textField: UITextField!

在两种情况下,当加载文档时,outlet将填充文本字段实例,并且应用程序可以与之交互,就像它在storyboard中定义或在程序中创建一样。

动作

大多数非平凡的应用程序都需要以某种方式响应用户交互。UIKit控件(UIControl类的子类)会触发事件,通知应用程序此类交互已经发生。例如,当按钮实例被按住时,UIButton类会触发UIControlEventPrimaryActionTriggered事件。

虽然应用程序可以使用outlet以程序方式注册事件,但MarkupKit提供了一个更方便的替代方案。任何名称以"on"开头(但不指代属性)的属性都被认为是控件事件。属性值表示在事件触发时将被触发的动作名称。属性名称只是"on"前缀加上事件名称,减去"UIControlEvent"前缀。

例如,以下标记声明了一个UIButton实例,当按钮被触按时会调用文档所有者的方法

<UIButton style="systemButton" title="Press Me!" onPrimaryActionTriggered="buttonPressed:"/>

例如

@IBAction func buttonPressed(_ sender: UIButton) {
    // User tapped button
}

请注意,sender 参数是可选的;如果从处理程序名称中省略了尾部冒号,则事件将触发对无参数处理程序方法的调用

@IBAction func buttonPressed() {
    // User tapped button
}

本地化

如果属性的值以 "@" 开头,MarkupKit 会尝试查找该值的本地化版本,然后再设置属性。例如,如果一个应用程序在 Localizable.strings 中定义了如下的本地化问候语

"hello" = "Hello, World!";

以下标记将产生一个 UILabel 实例,其 text 属性被设置为 "Hello, World!"

<UILabel text="@hello"/>

如果文档的所有者实现了名为 bundleForStrings 的方法,则将使用此方法返回的包加载本地化字符串值。与 bundleForImages 类似,MarkupKit 向 UIResponder 添加了一个默认的 bundleForStrings 实现,返回加载视图的包。子类可以重写此方法以提供自定义字符串加载行为。如果所有者没有实现 bundleForStrings,则将使用主包。

此外,如果所有者实现了名为 tableForStrings 的方法,则将使用此方法返回的表名来本地化字符串值。如果所有者没有实现此方法或返回 nil,则将使用默认字符串表(Localizable.strings)。由 UIResponder 提供的默认实现返回 nil

可以通过在文本字符串前添加一个反引号 ("^") 来转义前导的 "@" 字符。例如,此标记会产生一个包含字面文本 "@hello" 的标签

<UILabel text="^@hello"/>

数据绑定

以 "$" 字符开头的属性值表示数据绑定。"$" 字符后面的文本表示一个绑定到相应视图属性的表达式。任何时候绑定表达式的值发生变化,视图中的目标属性都会自动更新。MarkupKit 使用 键值观察 监视属性更改,并使用 Foundation 框架的 NSExpression 类评估表达式。

例如,一个视图控制器可能定义了一个名为 name 的可绑定属性,如下所示

class ViewController: UIViewController {
    @objc dynamic var name: String?

    ...
}

以下标记在 UILabel 实例的 text 属性和控制器中的 name 属性之间建立了一个绑定。任何对 name 的更改都将自动反映在标签上

<UILabel text="$name"/>

绑定也可以通过调用 MarkupKit 添加到 UIResponder 类中的 bind:toView:withKeyPath: 方法来程序化地建立。

与 "@" 类似,可以使用反引号来转义前导的 "$" 字符。例如,此标记将设置标签的文本为字面文本 "$name",而不是创建一个绑定

<UILabel text="^$name"/>

绑定表达式

绑定表达式不仅限于简单的属性。例如,一个自定义的表格视图单元格可能使用以下类的实例作为模型

class Row: NSObject, Decodable {
    @objc var heading: String?
    @objc var detail: String?
}

class CustomCell: LMTableViewCell {
    @objc dynamic var row: Row!
    
    ...
}

此标记绑定了标题和详情视图的 text 属性到行的各自的 headingdetail 属性。每次 row 的值改变时,标签都将被更新

<root accessoryType="disclosureIndicator">
    <LMColumnView>
        <UILabel text="$row.heading"/>
        <UILabel text="$row.detail"/>
    </LMColumnView>
</root>

表达式格式化器

绑定表达式不仅限于字符串值。例如,它们可以引用数字或日期属性,甚至可以包含数学运算。无法将此类表达式的结果直接绑定到基于字符串的目标属性。但是,可以使用格式化器将绑定值转换为适当的文本表示。

格式化器通过在绑定声明中追加一个格式化指定符来应用。格式化指定符包含要应用格式化器的名称以及传递给格式化器的任何相关参数。格式指定符与实际表达式之间由双冒号 ("::") 分隔。

例如,以下标记使用日期格式化器将个人的出生日期(由 NSDate 实例表示)转换为短日期格式的字符串

<UILabel text="$person.dateOfBirth::date;dateStyle=short;timeStyle=none"/>

此标记使用最大三位小数将双精度值转换为字符串

<UILabel text="$averageAge::number;numberStyle=decimal;maximumFractionDigits=3"/>

格式化器通过以下方法获取,该方法 MarkupKit 添加到 UIResponder

- (nullable NSFormatter *)formatterWithName:(NSString *)name arguments:(NSDictionary<NSString *, id> *)arguments;

在绑定、格式化表达式的值改变时,会在此方法上调用文档的所有者。默认实现提供了以下格式化器类型的支持

  • "number" - NSNumberFormatter
  • "date" - NSDateFormatter
  • "personNameComponents" - NSPersonNameComponentsFormatter
  • "byteCount" - NSByteCountFormatter
  • "measurement" - NSMeasurementFormatter

参数代表将设置在格式化器上以配置其行为的属性。枚举值的应用方式与之前描述的属性类似。拥有类可以重写此方法以支持自定义格式化器。

释放绑定

在所有者解除分配之前,必须通过调用 MarkupKit 添加到 UIResponder 类中的 unbindAll 方法来释放绑定。例如

deinit {
    unbindAll()
}

此方法删除之前添加的所有观察者。

<?case iOS?>
    <!-- iOS-specific content -->
<?case tvOS?>
    <!-- tvOS-specific content -->
<?end?>

MarkupKit为UIView类添加了一个processMarkupInstruction:data:方法,以便利视图层的处理。例如,LMTableView重写此方法以支持部分标题和页脚声明以及部分分隔符。

<LMTableView style="groupedTableView">
    <?sectionHeaderView?>
    <UITableViewHeaderFooterView textLabel.text="Section 1"/>        
    ...
    
    <?sectionBreak?>
    
    <?sectionHeaderView?>
    <UITableViewHeaderFooterView textLabel.text="Section 2"/>
    ...
</LMTableView>

以下将详细介绍这些处理指令和其他内容。

  • LMViewBuilder - 处理标记文档,将其内容反序列化为iOS应用可用的视图层次结构
  • LMRowViewLMColumnView - 布局视图,分别按水平或垂直线排列子视图
  • LMSpacer - 创建用于其他视图之间的灵活空间的视图
  • LMAnchorView - 可选地将子视图锚定到一条或多条边缘的视图
  • LMRootView - 提供视图层次结构无边界根的布局视图
  • LMScrollView - 是UIScrollView的子类,可以自动适应其内容的尺寸
  • LMPageView - 是UIScrollView的子类,便于声明分页内容
  • LMTableViewLMTableViewCellLMTableViewHeaderFooterView - 分别是UITableViewUITableViewCellUITableViewHeaderFooterView的子类,可以方便地声明表格视图内容
  • LMTableViewController - 以简化LMTableView管理为目标的UITableViewController子类
  • LMCollectionViewLMCollectionViewCell - 分别是UICollectionViewUICollectionViewCell的子类,可以方便地声明集合视图内容
  • LMSegmentedControl - 便于声明分段控制内容UISegmentedControl子类
  • LMPickerView - 便于声明选择器视图内容UIPickerView子类

还讨论了对几个UIKit类进行扩展,增强了这些类的行为或适应各自类型以在标记中使用

LMViewBuilder

LMViewBuilder是负责加载MarkupKit文档的类。它提供了一个类方法,它接受文档名称、所有者和可选的根视图,从一个标记中反序列化视图层次结构

+ (UIView *)viewWithName:(NSString *)name owner:(nullable id)owner root:(nullable UIView *)root;

文档名称和所有者

name参数表示要加载的视图的名称。它是包含视图声明的XML文件的文件名,不带.xml扩展名。

owner参数表示视图的所有者。这通常是UIViewController的一个实例,但这不是严格要求的。例如,自定义表格和集合视图单元类型通常指定它们自己作为所有者。

如果所有者实现了名为bundleForView的方法,视图文档将从这个方法返回的包中加载。MarkupKit向UIResponder添加了一个默认的bundleForView实现,它返回加载类的包。子类可以重写此方法以提供自定义视图加载行为。如果所有者没有实现bundleForView,则将使用主包。

注意,颜色表始终从主包加载。

文档根

root参数表示在加载文档时将用作根视图实例的值。此值通常是nil,这意味着根视图将由文档本身指定。但是,当非nil时,它表示根视图是由调用者提供的。在这种情况下,可以保留<root>标签作为文档的根元素来引用此视图。

例如,如果将 LMScrollView 的一个实例作为 viewWithName:owner:root: 的参数传递,则该标记

<root>
    <UIImageView image="world.png"/>
</root>

等同于以下内容

<LMScrollView>
    <UIImageView image="world.png"/>
</LMScrollView>    

通常情况下,当实现自定义表格或集合视图单元格时,会使用 root 参数。在这种情况下,单元格实例在加载视图时将自身同时作为所有者和根

override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
    super.init(style: style, reuseIdentifier: reuseIdentifier)

    LMViewBuilder.view(withName: "CustomTableViewCell", owner: self, root: self)
}

颜色和字体值

LMViewBuilder 还定义了以下两个类方法,分别用于解码颜色和字体值

+ (UIColor *)colorValue:(NSString *)value;
+ (UIFont *)fontValue:(NSString *)value;

这些方法也可以由应用程序代码调用。有关更多信息,请参阅LMViewBuilder.h

LMLayoutView

自动布局是iOS的一项功能,它允许开发者创建能够自动适应设备大小、方向或内容更改的应用程序。使用自动布局构建的应用程序通常具有很少或没有硬编码的视图定位逻辑,而是根据它们的“内禀”内容大小动态排列用户界面元素。

在iOS中,自动布局主要是通过布局约束来实现的,尽管功能强大,但不太方便使用。为了简化过程,MarkupKit提供了一组视图类,这些类的唯一职责是管理它们各自子视图的大小和位置

  • LMRowView - 在水平线上排列子视图
  • LMColumnView - 在垂直线上排列子视图
  • LMAnchorView - 可选地将子视图锚定到一个或多个边缘
  • LMRootView - 为视图层次结构提供一个与边距无关的根视图

这些类在内部使用布局约束,允许开发者轻松利用自动布局,同时无需直接管理约束。它们可以嵌套以创建复杂布局,这些布局会自动调整到方向或屏幕尺寸变化。

请注意,当执行布局时,将 hidden 属性设置为 true 的子视图将被忽略。布局视图会监听其子视图此属性的变化,并在必要时自动重新布局。

布局边距

布局视图的子视图通常相对于其布局边距定位,这些边距默认初始化为 0。以下标记创建了一个行视图,其子视图将与其自身的边缘有12像素的填充

<LMRowView layoutMargins="12">
    ...
</LMRowView>

MarkupKit为UIView添加了以下属性,以便可以单独设置边距值。

@property (nonatomic) CGFloat layoutMarginTop;
@property (nonatomic) CGFloat layoutMarginLeft;
@property (nonatomic) CGFloat layoutMarginBottom;
@property (nonatomic) CGFloat layoutMarginRight;
@property (nonatomic) CGFloat layoutMarginLeading;
@property (nonatomic) CGFloat layoutMarginTrailing;

例如,这段标记创建了一个层视图,其顶部和底部边距设置为8像素,左右边距设置为16像素。

<LMRowView layoutMarginTop="8" layoutMarginBottom="8" layoutMarginLeading="16" layoutMarginTrailing="16">
    ...
</LMRowView>

在iOS 11中,layoutMarginLeadinglayoutMarginTrailing属性直接映射到视图的定向边距。在iOS 10中,它们根据系统文本方向动态应用。

触摸交互

默认情况下,布局视图不消耗触摸事件。在布局视图内发生但在子视图中不相交的触摸将被忽略,允许事件通过视图。将非nil的背景颜色分配给布局视图将使视图开始消耗事件。

更多详细信息,请参阅LMLayoutView.h

LMRowView和LMColumnView

LMRowView和LMColumnView类分别用于在水平或垂直线上对子视图进行布局。这两个类都扩展了抽象类LMBoxView,而LMBoxView自身扩展了LMLayoutView并添加了以下属性

@property (nonatomic) LMHorizontalAlignment horizontalAlignment;
@property (nonatomic) LMVerticalAlignment verticalAlignment;
    
@property (nonatomic) CGFloat spacing;
    
@property (nonatomic) BOOL alignToBaseline;

前两个属性分别指定了盒视图子视图的水平和对齐方式。水平对齐选项如下:

  • LMHorizontalAlignmentFill
  • LMHorizontalAlignmentLeading
  • LMHorizontalAlignmentTrailing
  • LMHorizontalAlignmentCenter

垂直对齐选项如下:

  • LMVerticalAlignmentFill
  • LMVerticalAlignmentTop
  • LMVerticalAlignmentBottom
  • LMVerticalAlignmentCenter

默认情况下,两个值都设置为"fill",这在盒视图的两个轴上锚定了子视图,并确保任何子视图的位置没有歧义。其他值将使子视图锚定到单个边缘或沿给定轴居中。

例如,这段标记创建了一个包含三个标签的行视图,这些标签在水平上与行的首侧对齐,在垂直上与行的顶端对齐。

<LMRowView horizontalAlignment="leading" verticalAlignment="top">
    <UILabel text="One"/>
    <UILabel text="Two"/>
    <UILabel text="Three"/>
</LMRowView>

空白视图也可以用于在行或列内对齐子视图。这将在以下几个方面进行更详细的讨论。

spacing属性表示在连续子视图之间的预留空间量。对行视图而言,这指的是子视图之间的水平空间;对列视图而言,是指视图之间的垂直空间。例如,下面的标记创建了一个行视图,其中的标签将以16像素的间隔分开。

<LMRowView spacing="16">
    <UILabel text="One"/>
    <UILabel text="Two"/>
    <UILabel text="Three"/>
</LMRowView>

在iOS 10中,默认的间隔值是8。在iOS 11及以后的版本中,系统使用默认值。

此外,MarkupKit还为UIView添加了以下属性,可用于在子视图的基础上创建间隔

@property (nonatomic) CGFloat topSpacing;
@property (nonatomic) CGFloat bottomSpacing;
@property (nonatomic) CGFloat leadingSpacing;
@property (nonatomic) CGFloat trailingSpacing;

顶部和底部值适用于垂直布局(即列视图),而首端和末端值适用于水平布局(即行视图)。

alignToBaseline属性可以用来管理子视图相对于彼此的垂直对齐方式。这将在下面进行更详细的讨论。基线对齐默认是禁用的。

LMRowView

LMRowView类将子视图排列成一行。子视图按声明的顺序从左到右排列。例如,以下标记创建了一个包含三个标签的行视图

<LMRowView>
    <UILabel text="One"/>
    <UILabel text="Two"/>
    <UILabel text="Three"/>
</LMRowView>

如果行视图的垂直对齐设置为“填充”(默认),则每个子视图的顶部和底部边缘都将固定在行的顶部和底部边缘(不包括布局边距),以确保所有标签的高度相同。否则,子视图将根据指定的值垂直对齐。

基线对齐

此标记创建了一个包含三个不同字号的标签的行视图。因为alignToBaseline设置为true,所以三个标签的基线将对齐。

<LMRowView alignToBaseline="true">
    <UILabel text="abcd" font="Helvetica 12"/>
    <UILabel text="efg" font="Helvetica 24"/>
    <UILabel text="hijk" font="Helvetica 48"/>
</LMRowView>

此外,可以通过baseline属性控制子视图将要对齐的基线。默认值为“first”,即子视图将对齐到第一条基线。但是,也可以将子视图对齐到最后一条基线;例如

<LMRowView alignToBaseline="true" baseline="last">
    ...
</LMRowView>

更多信息请参见LMRowView.h

LMColumnView

LMColumnView类将子视图排列成垂直线。子视图按声明的顺序从上到下排列。例如,以下标记创建了一个包含三个标签的列视图

<LMColumnView>
    <UILabel text="One"/>
    <UILabel text="Two"/>
    <UILabel text="Three"/>
</LMColumnView>

如果列视图的水平对齐设置为“填充”(默认),则每个子视图的左侧和右侧边缘都将固定在列的左侧和右侧边缘(不包括布局边距),以确保所有标签的宽度相同。否则,子视图将根据指定的值水平对齐。

基线对齐

此标记创建一个包含三个标签的列视图,这些标签具有不同的字体大小。因为将 alignToBaseline 设置为 true,所以标签将根据它们的第一个和最后一个基线在垂直方向上排列,而不是根据它们的边界矩形。

<LMColumnView alignToBaseline="true">
    <UILabel text="abcd" font="Helvetica 16"/>
    <UILabel text="efg" font="Helvetica 32"/>
    <UILabel text="hijk" font="Helvetica 24"/>
</LMColumnView>

网格对齐

LMColumnView 定义以下附加属性,该属性指定嵌套子视图应垂直对齐在网格中,类似于 HTML 表格。

@property (nonatomic) BOOL alignToGrid;

当此属性设置为 true 时,连续行的子视图将调整大小以匹配列中最宽子视图的宽度。例如,以下标记将生成一个包含三个行并排列为两列的网格。

<LMColumnView alignToGrid="true">
    <LMRowView>
        <UILabel text="First row"/>
        <UILabel text="This is row number one."/>
    </LMRowView>

    <LMRowView>
        <UILabel text="Second row"/>
        <UILabel text="This is row number two."/>
    </LMRowView>

    <LMRowView>
        <UILabel text="Third row"/>
        <UILabel text="This is row number three."/>
    </LMRowView>
</LMColumnView>

不是 LMRowView 实例的列视图子视图将被排除在对齐之外。这使得它们可以用作节分隔符或标题等。

有关更多信息,请参阅 LMColumnView.h

固定尺寸

尽管视图通常根据其内在内容大小进行排列,但有时需要为特定视图维度分配一个固定值。MarkupKit 向 UIView 添加以下属性以支持显式大小定义。

@property (nonatomic) CGFloat width;
@property (nonatomic) CGFloat height;

例如,以下标记声明了一个图像视图,其 高度 属性设置为 240 像素。

<UIImageView image="world.png" contentMode="scaleAspectFit" height="240"/>

如果图像的高度小于或大于 240 像素,则将进行调整以适应此高度。由于内容模式设置为 "scaleAspectFit",因此宽度将相应调整,以便图像保持正确的纵横比。

或者,可以使用以下属性来允许视图的尺寸变化,同时保持固定纵横比。

@property (nonatomic) CGFloat aspectRatio;

注意,由于它们是通过布局约束内部实现的,因此对这些属性的更改可以是动画。例如

func toggleDetail() {
    view.layoutIfNeeded()

    detailView.height = detailSwitch.isOn ? 175 : 0

    UIView.animate(withDuration: 0.33, animations: {
        self.view.layoutIfNeeded()
    })
}

有限尺寸

MarkupKit 还向 UIView 添加了以下属性,用于定义给定维度的限制值。

@property (nonatomic) CGFloat minimumWidth;
@property (nonatomic) CGFloat maximumWidth;
@property (nonatomic) CGFloat minimumHeight;
@property (nonatomic) CGFloat maximumHeight;

指定最小宽度和高度值确保相应的维度大于或等于给定的值。类似地,指定最大宽度和高度确保相应的维度小于或等于给定的值。

例如,以下标记声明了一个具有120和240宽度值的UILabel实例

<UILabel text="Lorem ipsum dolor sit amet..." numberOfLines="0"
    minimumWidth="120" maximumWidth="240"/>

这确保了标签的宽度至少为120像素,最大为240像素。

查看权重

通常,行视图或列视图会比其子视图的内省大小需要更多空间。MarkupKit向UIView增加了以下属性,用于确定如何分配额外空间

@property (nonatomic) CGFloat weight;

此值指定视图希望在其父视图中获得的额外空间量(一旦确定所有未加权视图的大小),并且相对于在父视图中指定的所有其他权重。对于行视图,权重应用于额外的水平空间;对于列视图则应用于额外的垂直空间。

例如,下面的两个标签将被均匀分配大小,并给定列视图高度的50%

<LMColumnView>
    <UILabel weight="0.5" text="Hello"/>
    <UILabel weight="0.5" text="World"/>
</LMColumnView>

由于权重是相对的,此标记将产生相同的结果

<LMColumnView>
    <UILabel weight="1" text="Hello"/>
    <UILabel weight="1" text="World"/>
</LMColumnView>

在此示例中,第一个标签将获得可利用空间的六分之一,第二个标签将获得三分之一,第三个标签将获得二分之一

<LMColumnView>
    <UILabel weight="1" text="One"/>
    <UILabel weight="2" text="Two"/>
    <UILabel weight="3" text="Three"/>
</LMColumnView>

LMRowView中处理权重的方式类似,但在水平方向。

注意,显式定义的宽度和高度值优先于权重。如果一个视图既有权重又有固定维度值,则忽略权重值。

LMSpacer

权重的常见用途是在视图周围添加灵活的空间。例如,以下标记将标签垂直居中在列中

<LMColumnView>
    <UIView weight="1"/>
    <UILabel text="Hello, World!"/>
    <UIView weight="1"/>
</LMColumnView>

类似地,以下标记将标签水平居中在行中

<LMRowView>
    <UIView weight="1"/>
    <UILabel text="Hello, World!"/>
    <UIView weight="1"/>
</LMRowView>

由于填充视图非常常见,MarkupKit提供了一个名为LMSpacer的专用UIView子类,用于方便地在其他视图之间创建灵活空间。LMSpacer有一个默认权重为1,因此前面的例子可以重写如下,删除“权重”属性,提高可读性

<LMRowView>
    <LMSpacer/>
    <UILabel text="Hello, World!"/>
    <LMSpacer/>
</LMRowView>

与布局视图一样,填充视图默认不消耗触摸事件,因此它们不会干扰其下面的任何用户界面元素。给填充视图指定非nil的背景颜色会使视图开始消耗事件。

有关更多信息,请参阅LMSpacer.h

LMAnchorView

LMAnchorView类可选地将子视图锚定到其一个或多个边缘。虽然可以使用行、列、图层和填充视图的组合实现相似的布局,但在某些情况下,锚定视图可能提供一种更简单的方法。

锚点被指定为逗号分隔的边列表,用于在父容器中将视图锚定。锚点选项包括以下内容:

  • LMAnchorNone
  • LMAnchorTop
  • LMAnchorBottom
  • LMAnchorLeft
  • LMAnchorRight
  • LMAnchorLeading
  • LMAnchorTrailing
  • LMAnchorAll

例如,下面的标记创建了包含四个标签的锚定视图,这些标签锚定到视图的顶部、左侧、右侧和底部边缘。标签都将向内缩进16像素。

<LMAnchorView layoutMargins="16">
    <UILabel text="Top" anchor="top"/>
    <UILabel text="Left" anchor="left"/>
    <UILabel text="Right" anchor="right"/>
    <UILabel text="Bottom" anchor="bottom"/>
</LMAnchorView>

子视图也可以锚定到父视图的左侧和右侧边缘,以支持从右至左的语言环境;例如

<LMAnchorView layoutMargins="16">
    <UILabel text="Leading" anchor="leading"/>
    <UILabel text="Trailing" anchor="trailing"/>
</LMAnchorView>

此外,子视图可以锚定到给定维度的多个边缘。例如,下面的标记创建了一个包含两个标签的锚定视图,每个标签都将占据锚定视图的全部宽度。

<LMAnchorView layoutMargins="16">
    <UILabel text="Top" anchor="top, left, right"/>
    <UILabel text="Bottom" anchor="bottom, left, right"/>
</LMAnchorView>

如果没有为给定维度指定锚点,子视图将在锚定视图中在该维度内居中。

有关更多信息,请参阅 LMAnchorView.h

LMRootView

在iOS 10中,在某些情况下,UIKit可以为视图的边距分配系统定义的、不可覆盖的值。在这种情况下,可以使用LMRootView类。这个类将子视图固定到其边缘而不是其边距,并提供了以下属性,可以用来在视图的顶部和底部预留额外的空间

@property (nonatomic) CGFloat topPadding;
@property (nonatomic) CGFloat bottomPadding;

例如,一个视图控制器可以重写viewWillLayoutSubviews来设置根视图的顶部和底部间距分别等于其顶部和底部布局引导的长度,从而确保任何子视图都位于引导之间。

override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()

    let rootView = view as! LMRootView

    rootView.topPadding = topLayoutGuide.length
    rootView.bottomPadding = bottomLayoutGuide.length
}

在iOS 11中,顶部和底部布局引导已弃用。针对iOS 11及以后版本的应用程序可以使用UIViewControllerviewRespectsSystemMinimumLayoutMargins属性来替代LMRootView以禁用系统定义的边距。

LMRootView是唯一可以直接在故事板中使用的MarkupKit视图类型(即从NSCoder实例初始化)。但是,任何视图类型都可以嵌套在根视图中。

LMScrollView

LMScrollView类扩展了标准的UIScrollView类,简化了在标记中定义可滚动内容。它呈现单个内容视图,可选地允许用户在任一或两个方向上滚动。

滚动视图的内容通过contentView属性指定。以下LMScrollView属性确定如何呈现内容。

@property (nonatomic) BOOL fitToWidth;
@property (nonatomic) BOOL fitToHeight;

当两个值都设置为false(默认值)时,滚动视图将根据需要自动显示滚动条,允许用户在两个方向上滑动以查看完整的内容。例如

<LMScrollView>
    <UIImageView image="large_image.png"/>
</LMScrollView>

fitToWidth 设置为 true 时,滚动视图将确保其内容的宽度与其自身的宽度相匹配,导致内容在垂直方向上自动换行并滚动。当需要时,将显示垂直滚动条,但永远不会显示水平滚动条,因为内容的宽度永远不会超过滚动视图的宽度。

<LMScrollView fitToWidth="true">
    <UILabel text="Lorem ipsum dolor sit amet, consectetur adipiscing..."
        numberOfLines="0"/>
</LMScrollView>

同样,当 fitToHeight 设置为 true 时,滚动视图将确保其内容的高度与其自身的宽度相匹配,导致内容在水平方向上自动换行并滚动。当需要时,将显示水平滚动条,但永远不会显示垂直滚动条。

请注意,与 UIStackView 类似,分配新的内容视图不会将其作为滚动视图的子视图移除。要完全移除内容视图,请在视图中调用 removeFromSuperview

有关更多信息,请参阅 LMScrollView.h

LMPageView

LMPageView 类扩展了标准的 UIScrollView 类,以实现分页滚动视图内容的声明。例如,以下标记声明了一个包含三个页面的分页视图。页面的出现顺序与声明的顺序相同。

<LMPageView>
    <UILabel text="Page 1" textAlignment="center"/>
    <UILabel text="Page 2" textAlignment="center"/>
    <UILabel text="Page 3" textAlignment="center"/>
</LMPageView>

MarkupKit 为 UIScrollView 添加了一个 currentPage 属性,可以轻松地将滚动视图的页面索引与页面控件显示的索引同步;例如

func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
    pageControl.currentPage = scrollView.currentPage
}

在后面的部分将更详细地讨论 MarkupKit 对 UIScrollView 的扩展。

LMScrollView 类似,removePage: 方法不会将给定的视图作为页面视图的子视图移除。要完全移除页面视图,请在视图中调用 removeFromSuperview

LMPageView 仅在 iOS 中可用。有关更多信息,请参阅 LMPageView.h

LMTableView, LMTableViewCell, and LMTableViewHeaderFooterView

LMTableViewUITableView 的子类,充当其自身的数据源和代理,从静态定义的表格视图部分集合中提供单元格。默认情况下,LMTableView 启用自适应内容,允许将其用作通用布局工具。

LMTableViewCellLMTableViewHeaderFooterView 分别是 UITableViewCellUITableViewHeaderFooterView 的子类,分别提供自定义表格视图内容的载体。它们会自动为其内容应用约束,以启用自适应行为。

MarkupKit 还为标准的 UITableViewCell 类提供了扩展,允许其在标记中使用。这将在后面的部分中更详细地讨论。

声明

LMTableView提供两种工厂方法,用于根据标记构建表格视图实例。

+ (LMTableView *)plainTableView;
+ (LMTableView *)groupedTableView;

例如,以下标记创建了一个包含三行的普通样式的表格视图。

<LMTableView style="plainTableView">
    <UITableViewCell textLabel.text="Row 1"/>
    <UITableViewCell textLabel.text="Row 2"/>
    <UITableViewCell textLabel.text="Row 3"/>
</LMTableView>

部分管理

sectionBreak处理指令在表格视图中创建新部分。它对应于调用LMTableView类的insertSection:方法。此标记创建了一个包含两个部分(当表格视图初始化时隐式创建的第一个部分)的分组表格视图。

<LMTableView style="groupedTableView">
    <UITableViewCell textLabel.text="Row 1a"/>
    <UITableViewCell textLabel.text="Row 1b"/>
    <UITableViewCell textLabel.text="Row 1c"/>

    <?sectionBreak?>

    <UITableViewCell textLabel.text="Row 2a"/>
    <UITableViewCell textLabel.text="Row 2b"/>
    <UITableViewCell textLabel.text="Row 2c"/>
</LMTableView>

sectionHeader元素将标题分配给当前部分。它对应于调用LMTableViewsetTitle:forHeaderInSection:方法。例如,以下标记将部分标题添加到默认部分

<LMTableView style="groupedTableView">
    <sectionHeader title="Section 1"/>

    <UITableViewCell textLabel.text="Row 1"/>
    <UITableViewCell textLabel.text="Row 1"/>
    <UITableViewCell textLabel.text="Row 1"/>
</LMTableView>

或者,可以使用sectionHeaderView处理指令为当前部分分配自定义标题视图。它对应于调用LMTableViewsetView:forHeaderInSection:方法。PI之后紧随的视图元素用作该部分的标题视图。例如

<LMTableView style="groupedTableView">
    <?sectionHeaderView?>
    <UITableViewHeaderFooterView textLabel.text="Section 1"/>
    ...
</LMTableView>

类似地,可以使用sectionFooter元素或sectionFooterView处理指令分别将标题或自定义脚注视图分配给当前部分。

最后,使用sectionName处理指令将名称与部分相关联。它对应于调用LMTableViewsetName:forSection:方法。例如

<LMTableView style="groupedTableView">
    <?sectionName firstSection?>
    <UITableViewCell textLabel.text="Row 1a"/>
    <UITableViewCell textLabel.text="Row 1b"/>
    <UITableViewCell textLabel.text="Row 1c"/>

    <?sectionBreak?>

    <?sectionName secondSection?>
    <UITableViewCell textLabel.text="Row 2a"/>
    <UITableViewCell textLabel.text="Row 2b"/>
    <UITableViewCell textLabel.text="Row 2c"/>

    <?sectionBreak?>

    ...
</LMTableView>

这允许通过名称而不是顺序值来识别部分,提高可读性,并使控制器代码更易于应对视图变化。

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    if (tableView.name(forSection: indexPath.section) == "firstSection") {
        // User tapped row in first section
    } else if (tableView.name(forSection: indexPath.section) == "secondSection") {
        // User tapped row in second section
    } else {
        // User tapped row in other section
    }
}

部分选择模式

sectionSelectionMode处理指令用于设置部分的选择模式。它对应于调用LMTableViewsetSelectionMode:forSection:方法。

此PI的有效值包括"默认"、"单选标记"和"多选标记"。"默认"选项产生默认选择行为(应用程序负责管理选择状态)。"单选标记"选项确保在给定时间只有一条行被选中,类似于一组单选按钮。"多选标记"选项导致每次点击行时切换行的选中状态,类似于一组复选框。

例如,以下标记创建了一个表格视图,用户可以选择几种颜色中的一种。

<LMTableView style="groupedTableView">
    <?sectionSelectionMode singleCheckmark?>
    <UITableViewCell textLabel.text="Red" value="#ff0000"/>
    <UITableViewCell textLabel.text="Green" value="#00ff00"/>
    <UITableViewCell textLabel.text="Blue" value="#0000ff"/>
</LMTableView>

由MarkupKit扩展定义的value属性用于
与单元格关联一个可选的任意值。MarkupKit还向
UITableViewCell类添加了一个布尔类型的
checked属性,当设置后,会在
相应的行中出现勾选标记。

通常,不应直接设置
checked属性。相反,其状态
通常通过以下LMTableView方法管理:

- (nullable id)valueForSection:(NSInteger)section;
- (void)setValue:(nullable id)value forSection:(NSInteger)section;

- (NSArray *)valuesForSection:(NSInteger)section;
- (void)setValues:(NSArray *)values forSection:(NSInteger)section;

前两个方法用于获取和设置单个选中的值,通常与
选择模式设置为“单个勾选”的分区一起使用。第二
组方法用于获取和设置选中值的列表,通常与使用
“多选勾选”选择模式的分区一起使用。

附件视图

可以使用backgroundView处理指令为表格视图
分配背景视图。它对应于调用
UITableView类的
setBackgroundView:方法。例如,以下
代码创建了一个具有活动指示器视图的背景的分组
表格视图。

<LMTableView style="groupedTableView">
    <?backgroundView?>
    <UIActivityIndicatorView id="activityIndicatorView"/>

    ...
</LMTableView>

tableHeaderViewtableFooterView处理指令
分别用于设置表格视图的头部和尾部视图,并对应于
UITableView类的
setTableHeaderView:
setTableFooterView:方法。例如,以下
代码声明了一个包含搜索栏作为头部视图的表格视图。

<LMTableView>
    <?tableHeaderView?>
    <UISearchBar id="searchBar"/>

    ...
</LMTableView>

自定义数据源/委托实现

为了支持静态内容声明,LMTableView作为其
自己的数据源和委托。然而,在许多情况下,一个
应用程序可能需要在同一个表格视图中显示静态和
动态内容,或者响应类似于
tableView:didSelectRowAtIndexPath:
委托事件。在这种情况下,表格视图控制器可以
将自身注册为表格视图的数据源或委托,并将调
用转发给表格视图实现。此外,LMTableView实现
了以下
UITableViewDataSource
UITableViewDelegate协议的方法:

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath

- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath

- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
- (NSArray *)tableView:(UITableView *)tableView editActionsForRowAtIndexPath:(NSIndexPath *)indexPath
- (UISwipeActionsConfiguration *)tableView:(UITableView *)tableView leadingSwipeActionsConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath
- (UISwipeActionsConfiguration *)tableView:(UITableView *)tableView trailingSwipeActionsConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath

- (BOOL)tableView:(UITableView *)tableView canFocusRowAtIndexPath:(NSIndexPath *)indexPath

- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section

虽然控制器可以手动执行这种委托,但在
大多数情况下这并非必需。稍后讨论的
LMTableViewController类提供了这些方法的
默认实现,这些方法简单地委派给表格视图。
因此,管理LMTableView实例的视图控制器能够
通常只是扩展LMTableViewController并重写适当的
方法,在必要时委派给基类。

自定义单元格内容

<LMTableView style="plainTableView">
    <LMTableViewCell>
        <UIDatePicker datePickerMode="date"/>
    </LMTableViewCell>
</LMTableView>

override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
    super.init(style: style, reuseIdentifier: reuseIdentifier)

    LMViewBuilder.view(withName: "CustomTableViewCell", owner: self, root: self)
}

<root layoutMargins="12">
    <LMColumnView>
        ...
    </LMColumnView>
</root>

  • ignoreLayoutMargins - 指示单元格将其内容固定在其边缘而不是其边距上
  • backgroundView - 设置单元格的背景视图
  • selectedBackgroundView - 设置单元格的选择背景视图
  • multipleSelectionBackgroundView - 设置单元格的多选背景视图

自定义头/尾部内容

<LMTableView style="groupedTableView">
    ...
    
    <?sectionFooterView?>
    <LMTableViewHeaderFooterView>
        <LMRowView>
            <UILabel weight="1" text="On/Off"/>
            <UISwitch/>
        </LMRowView>
    </LMTableViewHeaderFooterView>
</LMTableView>

LMTableViewController

LMTableViewControllerUITableViewController 的子类,简化了 LMTableView 实例的管理。默认情况下,它将数据源和代理操作委托给表格视图本身。子类可以覆盖默认实现,以提供自定义表格视图内容或响应用户对表格视图的行选择和编辑请求等事件。

例如,下面的标记定义了一个包含两个区段的表格视图。第一个包含由标记定义的静态内容。第二个展示控制器提供的动态内容。

<LMTableView>
    <?sectionName static?>
    ...
    
    <?sectionBreak?>
    <?sectionName dynamic?>
    ...
</LMTableView>

控制器类扩展了 LMViewController,并覆盖了 tableView:numberOfRowsInSection:tableView:cellForRowAtIndexPath: 来为动态区段提供内容。它也覆盖了 tableView:didSelectRowAtIndexPath: 来响应用户对自定义行的选择。

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    let n: Int
    if (tableView.name(forSection: section) == "dynamic") {
        n = numberOfCustomRows()
    } else {
        n = super.tableView(tableView, numberOfRowsInSection: section)
    }
    
    return n
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell: UITableViewCell
    if (tableView.name(forSection: indexPath.section) == "dynamic") {
        cell = customCellForRow(indexPath.row)
    } else {
        cell = super.tableView(tableView, cellForRowAt: indexPath)
    }
    
    return cell
}

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    if (tableView.name(forSection: indexPath.section) == "dynamic") {
        handleCustomRowSelection(indexPath.row)
    } else {
        super.tableView(tableView, didSelectRowAt: indexPath)
    }
}

请注意,除非控制器在 Storyboard 中定义,否则必须显式将子类分配为表格视图的数据源和代理在 loadView 中。

override func loadView() {
    view = LMViewBuilder.view(withName: "MyTableViewController", owner: self, root: nil)

    tableView.dataSource = self
    tableView.delegate = self
}

LMCollectionView 和 LMCollectionViewCell

LMCollectionViewLMCollectionViewCell 类简化了在标记中声明集合视图中内容的过程。以下将更详细地讨论这两个类。

声明

与允许开发者声明性定义表格视图结构的 LMTableView 不同,LMCollectionView 仅提供了比其基类 UICollectionView 稍微多的功能。它主要作为在标记中声明集合视图实例的一种手段。

使用 UICollectionViewinitWithFrame:collectionViewLayout: 方法以编程方式创建 UICollectionView 实例。 LMCollectionView 提供以下工厂方法,允许在标记中构建集合视图:

+ (LMCollectionView *)flowLayoutCollectionView;

此方法创建使用集合视图流布局的 LMCollectionView 实例。

<LMCollectionView id="collectionView" style="flowLayoutCollectionView"/>

MarkupKit 向 UICollectionViewFlowLayout 类添加了多个属性,允许其声明性配置。例如,以下标记将流布局的项目宽度设置为 80,将项目高度设置为 120,并将区段内边距设置为 12。

<LMCollectionView style="flowLayoutCollectionView"
    collectionViewLayout.itemWidth="80" collectionViewLayout.itemHeight="120"
    collectionViewLayout.sectionInset="12"
    backgroundColor="#ffffff"/>

这些属性将在稍后的章节中更详细地讨论。

辅助视图

使用 backgroundView 处理指令可以为集合视图(UICollectionView)分配背景视图。它对应于调用 UICollectionView 类的 setBackgroundView: 方法。例如,以下 markup 创建一个带有活动指示器视图背景的集合视图:

<LMCollectionView style="flowLayoutCollectionView"
    collectionViewLayout.itemWidth="80" collectionViewLayout.itemHeight="120"
    collectionViewLayout.sectionInset="12">
    <?backgroundView?>
    <UIActivityIndicatorView id="activityIndicatorView"/>

    ...
</LMCollectionView>

自定义单元格内容

LMTableViewCell 一样,LMCollectionViewCell 支持自定义单元格内容的声明。它扩展了 UICollectionViewCell 并自动对其内容应用约束,以启用自适应行为。

通过重写 initWithFrame: 并指定单元格视图作为文档所有者,调用者可以创建自定义集合视图单元格,其内容以markup形式表达。

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

    LMViewBuilder.view(withName: "CustomCollectionViewCell", owner: self, root: self)
}

因为初始化器将单元格实例本身作为 root 参数的值传递给 viewWithName:owner:root,因此 CustomCollectionViewCell.xml 中声明的 markup 必须包含一个 <root> 标签来引用此参数。

<root layoutMargins="12">
    <LMColumnView>
        ...
    </LMColumnView>
</root>

LMCollectionViewCell 还支持以下处理指令,可以用于指定各种背景视图的值:

  • ignoreLayoutMargins - 指示单元格将其内容固定在其边缘而不是其边距上
  • backgroundView - 设置单元格的背景视图
  • selectedBackgroundView - 设置单元格的选择背景视图

有关更多信息,请参阅 LMCollectionView.hLMCollectionViewCell.h

LMSegmentedControl

LMSegmentedControlUISegmentedControl 的子类,支持声明分段控件内容。

使用 segment 元素可以向分段控件添加一个选项段。可以使用 "title" 属性指定选项段的标题。

<LMSegmentedControl>
    <segment title="Yes"/>
    <segment title="No"/>
</LMSegmentedControl>

同样,可以使用 "image" 属性为选项段指定图像。

<LMSegmentedControl>
    <segment image="yes.png"/>
    <segment image="no.png"/>
</LMSegmentedControl>

LMSegmentedControl 提供以下方法通过值而不是通过选项段索引来管理选择状态:

- (void)insertSegmentWithTitle:(nullable NSString *)title value:(nullable id)value atIndex:(NSUInteger)segment animated:(BOOL)animated;
- (void)insertSegmentWithImage:(nullable UIImage *)image value:(nullable id)value atIndex:(NSUInteger)segment animated:(BOOL)animated;

- (nullable id)valueForSegmentAtIndex:(NSUInteger)segment;
- (void)setValue:(nullable id)value forSegmentAtIndex:(NSUInteger)segment;

可以按如下方式在 markup 中声明选项值:

<UISegmentedControl>
    <segment title="Red" value="#ff0000"/>
    <segment title="Green" value="#00ff00"/>
    <segment title="Blue" value="#0000ff"/>
</UISegmentedControl>

value 属性返回与选定选项段相关的值。

@property (nonatomic, nullable) id value;

设置此属性会自动选择与相关值关联的选项段。例如,将上述声明的分段控件的 value 属性设置为 "#00ff00" 会选择第二个选项段。

LMPickerView

LMPickerView 是位于 UIPickerView 中的子类,其作为自己的数据源和代理,从静态定义的行和组件标题集合中提供内容。例如,以下 markup 声明了一个包含四个行的picker视图,代表大小选项:

<LMPickerView>
    <row title="Small"/>
    <row title="Medium"/>
    <row title="Large"/>
    <row title="Extra-Large"/>
</LMPickerView>

row元素对应于调用LMPickerViewinsertRow:inComponent:withTitle:value:方法。row标签的“title”属性值用作行的标题。

还可以将一个可选值与每行关联。

<LMPickerView>
    <row title="Small" value="S"/>
    <row title="Medium" value="M"/>
    <row title="Large" value="L"/>
    <row title="Extra-Large" value="XL"/>
</LMPickerView>

这允许应用程序通过键而不是通过行索引来管理选择状态。下文将更详细地讨论选择管理。

组件管理

componentSeparator处理指令在选择视图中插入一个新的组件。它对应于调用LMPickerViewinsertComponent:方法。以下标记声明了一个包含两个组件的选择视图,第一个组件包含一组大小选项,第二个组件包含颜色选项(当一个选择视图初始化时,第一个组件将隐式创建)

<LMPickerView>
    <row title="Small" value="S"/>
    <row title="Medium" value="M"/>
    <row title="Large" value="L"/>
    <row title="Extra-Large" value="XL"/>

    <?componentSeparator?>

    <row title="Red" value="#ff0000"/>
    <row title="Yellow" value="#ffff00"/>
    <row title="Green" value="#00ff00"/>
    <row title="Blue" value="#0000ff"/>
    <row title="Purple" value="#ff00ff"/>
</LMPickerView>

componentName处理指令为组件分配一个名称。它对应于调用LMPickerViewsetName:forComponent:方法。这允许通过名称而不是索引来识别组件,因此可以轻松添加或重新排列而不会破坏控制器代码。例如

<LMPickerView>
    <?componentName sizes?>
    <row title="Small" value="S"/>
    <row title="Medium" value="M"/>
    <row title="Large" value="L"/>
    <row title="Extra-Large" value="XL"/>

    <?componentSeparator?>

    <?componentName colors?>
    <row title="Red" value="#ff0000"/>
    <row title="Yellow" value="#ffff00"/>
    <row title="Green" value="#00ff00"/>
    <row title="Blue" value="#0000ff"/>
    <row title="Purple" value="#ff00ff"/>
</LMPickerView>

选择管理

以下LMPickerView方法可以用于通过组件值而不是通过行索引来管理选择状态

- (nullable id)valueForComponent:(NSInteger)component;
- (void)setValue:(nullable id)value forComponent:(NSInteger)component animated:(BOOL)animated;

第一个方法返回给定组件中选定的行所关联的值,第二个方法在给定组件中选择与给定值相对应的行(选择可以是可选的,并且可以动画化)。

自定义数据源/代理实现

为了支持静态内容声明,LMPickerView充当自己的数据源和代理。但是,可以在LMPickerView实例上设置应用程序特定的数据源或代理,以提供自定义组件内容或处理组件选择事件。实现类应根据需要将代理委托给提供的滚动选择视图实例。LMPickerView实现了以下数据源和代理方法

- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView;
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component;

- (NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component

例如,以下标记声明了一个包含静态和动态组件的选择视图

<LMPickerView id="pickerView">
    <?componentName sizes?>
    ...

    <?componentName colors?>
    ...

    <?componentName dynamic?>
</LMPickerView>

控制器类实现了numberOfComponentsInPickerView:pickerView:numberOfRowsInComponent:pickerView:titleForRow:forComponent:,以提供动态组件的内容

func numberOfComponents(in pickerView: UIPickerView) -> Int {
    return pickerView.numberOfComponents(in: pickerView)
}
    
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
    let n: Int
    if (pickerView.name(forComponent: component) == "dynamic") {
        n = numberOfCustomRows()
    } else {
        n = pickerView.pickerView(pickerView, numberOfRowsInComponent: component)
    }

    return n
}

func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
    let title: String?
    if (pickerView.name(forComponent: component) == dynamicComponentName) {
        title = String(row + 1)
    } else {
        title = pickerView.pickerView(pickerView, titleForRow: row, forComponent: component)
    }

    return title
}

LMPickerView仅在iOS可用。有关更多信息,请参阅LMPickerView.hhttps://github.com/gk-brown/MarkupKit/blob/master/MarkupKit-iOS/MarkupKit/LMPickerView.h

UIKit 扩展

MarkupKit 为多个 UIKit 类扩展了功能,以增强其行为或使其适应在标记中使用。例如,如前所述,一些类定义了自定义初始化器,并且必须通过工厂方法实例化。此外,一些标准 UIKit 类的特性并未作为可设置的属性通过 KVC 暴露。MarkupKit 添加了所需的工厂方法和属性定义,以便允许这些类在标记中使用。以下为这些扩展的文档。

UIResponder

MarkupKit 向 UIResponder 添加了以下方法,允许文档所有者自定义从哪些包中加载视图文档、图像和本地化字符串值

- (NSBundle *)bundleForView;
- (NSBundle *)bundleForImages;
- (NSBundle *)bundleForStrings;
- (nullable NSString *)tableForStrings;

此外,MarkupKit 添加了这些方法来支持视图与文档所有者之间的声明性数据绑定

- (void)bind:(NSString *)expression toView:(UIView *)view withKeyPath:(NSString *)keyPath;
- (void)unbindAll;

第一个方法是创建所有者与相关视图实例之间的绑定。第二个方法释放所有绑定,必须在所有者释放之前调用。

最后,MarkupKit 向 UIResponder 添加了此方法,使文档所有者能够为绑定表达式提供自定义格式化程序

- (nullable NSFormatter *)formatterWithName:(NSString *)name arguments:(NSDictionary<NSString *, id> *)arguments;

UIView

MarkupKit 向 UIView 添加了以下属性,用于为给定维度定义固定或范围值

@property (nonatomic) CGFloat width;
@property (nonatomic) CGFloat minimumWidth;
@property (nonatomic) CGFloat maximumWidth;
    
@property (nonatomic) CGFloat height;
@property (nonatomic) CGFloat minimumHeight;
@property (nonatomic) CGFloat maximumHeight;

还向 UIView 添加了 weight 属性,该属性由行和列视图使用以确定如何在容器内分配额外空间

@property (nonatomic) CGFloat weight;

anchor 属性用于指定视图的一组锚点值。它与 LMAnchorView 布局视图类一起使用

@property (nonatomic) LMAnchor anchor;

添加了以下属性,以允许单独设置视图布局边距的组件

@property (nonatomic) CGFloat layoutMarginTop;
@property (nonatomic) CGFloat layoutMarginLeft;
@property (nonatomic) CGFloat layoutMarginBottom;
@property (nonatomic) CGFloat layoutMarginRight;
@property (nonatomic) CGFloat layoutMarginLeading;
@property (nonatomic) CGFloat layoutMarginTrailing;

这些属性添加是为了支持随子视图间隔

@property (nonatomic) CGFloat topSpacing;
@property (nonatomic) CGFloat bottomSpacing;
@property (nonatomic) CGFloat leadingSpacing;
@property (nonatomic) CGFloat trailingSpacing;

如前所述,添加了 processMarkupInstruction:dataappendMarkupElementView: 方法以支持标记处理

- (void)processMarkupInstruction:(NSString *)target data:(NSString *)data;
- (void)processMarkupElement:(NSString *)tag properties:(NSDictionary *)properties;
- (void)appendMarkupElementView:(UIView *)view;

最后,MarkupKit 向 UIView 添加了以下方法以支持 Xcode 中的实时预览

- (void)preview:(NSString *)viewName owner:(nullable id)owner;

实时预览将在稍后详细讨论。

UIButton

使用UIButton的方法程序创建UIButton实例。MarkupKit向UIButton增加了以下工厂方法,以便在标记中声明按钮。

+ (UIButton *)systemButton;
+ (UIButton *)detailDisclosureButton;
+ (UIButton *)infoLightButton;
+ (UIButton *)infoDarkButton;
+ (UIButton *)contactAddButton;
+ (UIButton *)plainButton;

按钮内容通过setTitle:forState:setImage:forState:等方法进行程序配置。MarkupKit向UIButton增加以下属性,以便在标记中定义此内容。

@property (nonatomic, nullable) NSString *title;
@property (nonatomic, nullable) UIColor *titleColor;
@property (nonatomic, nullable) UIColor *titleShadowColor;
@property (nonatomic, nullable) NSAttributedString *attributedTitle;
@property (nonatomic, nullable) UIImage *image;
@property (nonatomic, nullable) UIImage *backgroundImage;

这些属性设置对应值的“正常”状态。例如,以下标记创建了一个标题为“按下我!”的系统样式按钮。

<UIButton style="systemButton" title="Press Me!"/>

此外,MarkupKit增加了以下属性,以允许设置按钮内容的边缘填充。

@property (nonatomic) CGFloat contentEdgeInsetTop;
@property (nonatomic) CGFloat contentEdgeInsetLeft;
@property (nonatomic) CGFloat contentEdgeInsetBottom;
@property (nonatomic) CGFloat contentEdgeInsetRight;

@property (nonatomic) CGFloat titleEdgeInsetTop;
@property (nonatomic) CGFloat titleEdgeInsetLeft;
@property (nonatomic) CGFloat titleEdgeInsetBottom;
@property (nonatomic) CGFloat titleEdgeInsetRight;

@property (nonatomic) CGFloat imageEdgeInsetTop;
@property (nonatomic) CGFloat imageEdgeInsetLeft;
@property (nonatomic) CGFloat imageEdgeInsetBottom;
@property (nonatomic) CGFloat imageEdgeInsetRight;

例如

<UIButton style="systemButton" title="Press Me!" 
    contentEdgeInsetLeft="8" 
    contentEdgeInsetRight="8"/>

最后,MarkupKit覆盖了UIButton的appendMarkupElementView:方法,以允许在标记中定义自定义按钮内容。例如,以下标记创建了一个包含垂直排列的图像视图和标签的按钮。

<UIButton>
    <LMColumnView>
        <UIImageView image="world.png"/>
        <UILabel text="Hello, World!"/>
    </LMColumnView>
</UIButton>

UISegmentedControl

MarkupKit向UISegmentedControl类增加了以下方法和属性。这些方法主要添加到UISegmentedControl,以便在使用标记中的LMSegmentedControl实例时无需进行类型转换。但是,修改器方法由LMSegmentedControl实现。

- (nullable id)valueForSegmentAtIndex:(NSUInteger)segment;
- (void)setValue:(nullable id)value forSegmentAtIndex:(NSUInteger)segment;

@property (nonatomic, nullable) id value;

UITextField

MarkupKit向UITextField增加了以下处理指令以支持以下属性,允许在标记中配置文本字段关联的视图。

  • ?leftView?
  • ?rightView?

例如,以下标记声明了一个适用于输入电子邮件地址的UITextField实例。该文本字段包含一个电子邮件图标作为右视图,以提示用户输入字段的内容。

<UITextField id="emailAddressTextField" placeholder="Email Address"
    keyboardType="emailAddress"
    rightViewMode="always">
    <?rightView?>
    <UIImageView image="email.png"/>
</UITextField>

UILabel

MarkupKit向UILabel增加了以下属性,以允许独立配置标签的阴影偏移宽度和高度。

@property (nonatomic) CGFloat shadowOffsetWidth;
@property (nonatomic) CGFloat shadowOffsetHeight;

例如,以下标记创建了一个具有3个阴影偏移宽度和高度的标签

<UILabel text="Hello, World!" 
    shadowColor="red" 
    shadowOffsetWidth="3" 
    shadowOffsetHeight="3"/>

UIImageView

从tvOS 11开始,MarkupKit提供了声明图像视图中覆盖内容的支持

<UIImageView id="imageView" contentMode="scaleAspectFit" tintColor="black">
    <?case tvOS?>
    <?overlayContent?>
    <UILabel id="label" textColor="red" textAlignment="center" font="System-Bold 24"/>
    <?end?>
</UIImageView>

内容会自动调整为匹配图像视图的边界

UIPickerView

MarkupKit向UIPickerView类添加了以下实例方法。这些方法主要添加到UIPickerView中,以便在标记中使用LMPickerView实例时不需要强制类型转换。然而,仅仅是LMPickerView实现了这些变量方法。

- (NSString *)nameForComponent:(NSInteger)component;
- (NSInteger)componentWithName:(NSString *)name;
    
- (nullable id)valueForRow:(NSInteger)row forComponent:(NSInteger)component;
- (void)setValue:(nullable id)value forRow:(NSInteger)row forComponent:(NSInteger)component;
    
- (nullable id)valueForComponent:(NSInteger)component;
- (void)setValue:(nullable id)value forComponent:(NSInteger)component animated:(BOOL)animated;

UIProgressView

使用initWithProgressViewStyle:方法程序性地创建UIProgressView的实例。MarkupKit为UIProgressView添加以下工厂方法,以便在标记中声明进度视图

+ (UIProgressView *)defaultProgressView;
+ (UIProgressView *)barProgressView;

例如,以下标记声明了一个默认样式的UIProgressView的实例。

<UIProgressView style="defaultProgressView"/>

UIScrollView

MarkupKit向UIScrollView类添加了以下属性,以便可以单独设置滚动视图的内容内边距

@property (nonatomic) CGFloat contentInsetTop;
@property (nonatomic) CGFloat contentInsetLeft;
@property (nonatomic) CGFloat contentInsetBottom;
@property (nonatomic) CGFloat contentInsetRight;

此外,MarkupKit还添加了这个属性和方法,以帮助简化对分页滚动视图(包括LMPageView)的操作

@property (nonatomic) NSInteger currentPage;
    
- (void)setCurrentPage:(NSInteger)currentPage animated:(BOOL)animated;

最后,可以使用refreshControl处理指令将刷新控制器声明性地与滚动视图实例关联;例如

<LMScrollView fitToWidth="true">
    <?refreshControl?>
    <UIRefreshControl onValueChanged="refresh:"/>
    ...
</LMScrollView>

UITableView

MarkupKit 向 UITableView 类中添加了以下实例方法。这些方法主要添加到 UITableView,主要目的是在使用 LMTableView 实例与 UITableViewController 结合时不需要进行类型转换。

- (NSString *)nameForSection:(NSInteger)section;
- (NSInteger)sectionWithName:(NSString *)name;
    
- (nullable id)valueForSection:(NSInteger)section;
- (void)setValue:(nullable id)value forSection:(NSInteger)section;

- (NSArray *)valuesForSection:(NSInteger)section;
- (void)setValues:(NSArray *)values forSection:(NSInteger)section;

UITableViewCell

通过调用 UITableViewCellinitWithStyle:reuseIdentifier: 方法以编程方式创建 UITableViewCell 的实例。MarkupKit 向 UITableViewCell 添加了以下工厂方法,以便可以在标记中声明表格视图单元格。

+ (UITableViewCell *)defaultTableViewCell;
+ (UITableViewCell *)value1TableViewCell;
+ (UITableViewCell *)value2TableViewCell;
+ (UITableViewCell *)subtitleTableViewCell;

例如,以下标记声明了一个包含三个副标题样式 UITableViewCell 实例的 LMTableView 实例。

<LMTableView style="plainTableView">
    <UITableViewCell style="subtitleTableViewCell" textLabel.text="Row 1" detailTextLabel.text="This is the first row."/>
    <UITableViewCell style="subtitleTableViewCell" textLabel.text="Row 2" detailTextLabel.text="This is the second row."/>
    <UITableViewCell style="subtitleTableViewCell" textLabel.text="Row 3" detailTextLabel.text="This is the third row."/>
</LMTableView>

MarkupKit 还向 UITableViewCell 添加了以下属性。

@property (nonatomic, nullable) id value;
@property (nonatomic) BOOL checked;

value 属性用于将可选值与单元格关联。它与 UIViewtag 属性类似,但不限于整数值。当单元格被选中时,将 checked 属性设置为 true,未被选中时设置为 false。这两个属性主要用于与 LMTableView 章节选择模式一起使用。

附加视图

MarkupKit 向 UITableViewCell 添加了 appendMarkupElementView: 的实现,将给定的视图设置为单元格的附加视图,允许在标记中声明附加视图。例如,以下标记创建了一个具有 UISwitch 作为附加视图的单元格。

<UITableViewCell textLabel.text="This is a switch">
    <UISwitch id="switch"/>
</UITableViewCell>

UICollectionViewFlowLayout

MarkupKit 向 UICollectionViewFlowLayout 添加了以下属性,允许在标记中配置它。

@property (nonatomic) CGFloat itemWidth;
@property (nonatomic) CGFloat itemHeight;

@property (nonatomic) CGFloat estimatedItemWidth;
@property (nonatomic) CGFloat estimatedItemHeight;
    
@property (nonatomic) CGFloat sectionInsetTop;
@property (nonatomic) CGFloat sectionInsetLeft;
@property (nonatomic) CGFloat sectionInsetBottom;
@property (nonatomic) CGFloat sectionInsetRight;

@property (nonatomic) CGFloat headerReferenceWidth;
@property (nonatomic) CGFloat headerReferenceHeight;
    
@property (nonatomic) CGFloat footerReferenceWidth;
@property (nonatomic) CGFloat footerReferenceHeight;

例如

<LMCollectionView style="flowLayoutCollectionView"
    collectionViewLayout.itemWidth="80" collectionViewLayout.itemHeight="120"
    collectionViewLayout.sectionInset="12"
    backgroundColor="#ffffff"/>

UIVisualEffectView

通过使用带 UIVisualEffect 实例作为参数的 initWithEffect: 方法创建 UIVisualEffectView 的实例。MarkupKit 向 UIVisualEffectView 添加了以下工厂方法,以简化在标记中构建 UIVisualEffectView

+ (UIVisualEffectView *)extraLightBlurEffectView;
+ (UIVisualEffectView *)lightBlurEffectView;
+ (UIVisualEffectView *)darkBlurEffectView;
+ (UIVisualEffectView *)extraDarkBlurEffectView;
+ (UIVisualEffectView *)regularBlurEffectView;
+ (UIVisualEffectView *)prominentBlurEffectView;

注意,extraDarkBlurEffectView 只在 tvOS 中可用。

CALayer

UIViewlayer 属性返回一个 CALayer 实例,可以用来配置视图属性。但是,CALayershadowOffset 属性是一个 CGSize,这不被原生 KVC 支持。MarkupKit 为 CALayer 添加了以下方法,以允许独立配置层的阴影宽度和高度。

@property (nonatomic) CGFloat shadowOffsetWidth;
@property (nonatomic) CGFloat shadowOffsetHeight;

例如,以下标记创建了一个系统按钮,阴影不透明度为 0.5,半径为 10,偏移高度为 3

<UIButton style="systemButton" title="Press Me!" 
    layer.shadowOpacity="0.5" 
    layer.shadowRadius="10" 
    layer.shadowOffsetHeight="3"/>

实时预览

带有 IB_DESIGNABLE@IBDesignable 标记的视图类可以调用 MarkupKit 添加到 UIView 类中的 preview: owner: 方法,以在设计时预览 markup 变更,避免启动 iOS 模拟器。如果加载文档时发生错误,将显示包含错误信息的标签,允许快速识别错别字和其他错误。

例如,以下视图控制器显示了一个简单的问候语

class ViewController: UIViewController {
    @IBOutlet var label: UILabel!

    override func loadView() {
        view = LMViewBuilder.view(withName: "ViewController", owner: self, root: LMColumnView())
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        label.text = "Hello, World!"
    }
}

以下是视图的 markup

<root backgroundColor="white">
    <UILabel id="label" textAlignment="center" textColor="red"/>
</root>

以下视图类可用于在 Xcode 内直接预览 markup 文档。视图对 prepareForInterfaceBuilder 的实现提供占位符内容;使用临时控制器实例以确保正确处理任何出口、动作或绑定

@IBDesignable
class ViewControllerPreview: LMColumnView {
    override func prepareForInterfaceBuilder() {
        let owner = ViewController(nibName: nil, bundle: nil)

        preview("ViewController", owner: owner)

        owner.label.text = "Hello, World!"
    }
}

请注意,视图类和 XIB 文件仅在设计时使用 - 运行时视图控制器仍然负责加载视图文档。

实时预览可以显著减少开发时间,因为它消除了通常需要通过模拟器测试更新的往返。使用实时预览,可以快速验证连续更新,只有在达到所需的布局后才会启动模拟器。

更多信息

有关更多信息示例,请参阅 wiki