测试已测试 | ✓ |
Lang语言 | SwiftSwift |
许可证 | MIT |
发布最后发布 | 2017年11月 |
SwiftSwift版本 | 4.0.2 |
SPM支持SPM | ✓ |
由 Alexander Cyon 维护。
使用枚举数组及其属性来设置视图样式
let label: UILabel = [.text("Hello World"), .textColor(.red)]
您可以使用swift4
分支在Swift 4代码中使用ViewComposer,例如,在使用CocoaPods时,Podfile将是
pod 'ViewComposer', :git => 'https://github.com/Sajjon/ViewComposer.git', :branch => 'swift4'
pod 'ViewComposer', '~> 0.2'
github "ViewComposer" ~> 0.2
dependencies: [
.Package(url: "https://github.com/Sajjon/ViewComposer.git", majorVersion: 0)
]
参考著名开源框架 Alamofire的说明,以手动集成框架 来手动安装ViewComposer到项目中。
我们使用ViewAttribute
类型的数组来设置视图,这创建了一种名为ViewStyle
的类型,可用于设置视图。 请注意,属性的(枚举)顺序并不重要
let label: UILabel = [.text("Hello World"), .textColor(.red)]
let same: UILabel = [.textColor(.red), .text("Hello World")] // order does not matter
(即使在使用您的应用程序时使用相同的顺序以提高一致性也是一个好主意。)
当您查看一个UIViewController
示例时,这种样式视图的强大尤为明显,这甚至不是一个复杂的ViewController。
class NestedStackViewsViewController: UIViewController {
lazy var fooLabel: UILabel = [.text("Foo"), .textColor(.blue), .color(.red), .textAlignment(.center)]
lazy var barLabel: UILabel = [.text("Bar"), .textColor(.red), .color(.green), .textAlignment(.center)]
lazy var labels: UIStackView = [.views([self.fooLabel, self.barLabel]), .distribution(.fillEqually)]
lazy var button: UIButton = [.text("Baz"), .color(.cyan), .textColor(.red)]
lazy var stackView: UIStackView = [.views([self.labels, self.button]), .axis(.vertical), .distribution(.fillEqually)]
...
}
与常规相比
class VanillaNestedStackViewsViewController: UIViewController {
lazy var fooLabel: UILabel = {
let fooLabel = UILabel()
fooLabel.translatesAutoresizingMaskIntoConstraints = false
fooLabel.text = "Foo"
fooLabel.textColor = .blue
fooLabel.backgroundColor = .red
fooLabel.textAlignment = .center
return fooLabel
}()
lazy var barLabel: UILabel = {
let barLabel = UILabel()
barLabel.translatesAutoresizingMaskIntoConstraints = false
barLabel.text = "Bar"
barLabel.textColor = .red
barLabel.backgroundColor = .green
barLabel.textAlignment = .center
return barLabel
}()
lazy var labels: UIStackView = {
let labels = UIStackView(arrangedSubviews: [self.fooLabel, self.barLabel])
labels.translatesAutoresizingMaskIntoConstraints = false
labels.distribution = .fillEqually
return labels
}()
lazy var button: UIButton = {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.backgroundColor = .cyan
button.setTitle("Baz", for: .normal)
button.setTitleColor(.red, for: .normal)
return button
}()
lazy var stackView: UIStackView = {
let stackView = UIStackView(arrangedSubviews: [self.labels, self.button, self.button])
stackView.distribution = .fillEqually
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
return stackView
}()
...
}
正如我们在上面的ViewComposer示例中所看到的
let button: UIButton = [.color(.red), .text("Red"), .textColor(.blue)]
无需子类
当然,您可以将您的var
更改为lazy
(推荐),并设置观图中尚未由ViewComposer支持的属性,如下所示
lazy var button: UIButton = {
let button: UIButton = [.color(.red), .text("Red"), .textColor(.blue)]
// setup attributes not yet supported by ViewComposer
button.layer.isDoubleSided = false // `isDoubleSided` is not yet supported
return button
}()
属性枚举数组[ViewAttribute]
创建了一个ViewStyle
(可以创建视图的包装器)。
一个[ViewAttribute]
数组可以与另一个数组或单个属性合并。此数组还可以与一个ViewStyle
合并。一个ViewStyle
也可以与单个ViewAttribute
合并。属性数组还可以与单个属性合并。任何类型都可以在合并的左右两侧。
合并的结果始终是一个ViewStyle
,因为这是最精细的类型。
由于您正在合并的可能包含相同的属性,因此有两个不同的合并函数,merge:master
和 merge:slave
,即,您需要决定保留哪个值。
使用 merge:slave
和 merge:master
合并具有重复值的 [ViewAttribute]
数组
let foo: [ViewAttribute] = [.text("foo")] // can use `ViewStyle` as `bar`
let bar: ViewStyle = [.text("bar"), .color(.red)] // prefer `ViewStyle`
// The merged results are of type `ViewStyle`
let fooMerged = foo.merge(slave: bar) // [.text("foo"), .color(.red)]
let barMerged = foo.merge(master: bar) // [.text("bar"), .color(.red)]
如上所述,您还可以合并单个属性
let foo: ViewAttribute = .text("foo")
let style: ViewStyle = [.text("bar"), .color(.red)]
// The merged results are of type `ViewStyle`
let mergeSingleAttribute = style.merge(master: foo) // [.text("foo"), .color(.red)]
let array: [ViewAttriubte] = [.text("foo")]
let mergeArray = style.merge(master: foo) // [.text("foo"), .color(.red)]
您还可以合并可选的 ViewStyle
,这很方便您初始化
final class MyViewDefaultingToRed: UIView {
init(_ style: ViewStyle? = nil) {
let style = style.merge(slave: .default)
self.style = style
super.init(frame: .zero)
setup(with: style) // setup the view using the style..
}
}
private extension ViewStyle {
static let `default`: ViewStyle = [.color(.red)]
}
<-
和 <<-
我们不是写 foo.merge(slave: bar)
,而是可以写 foo <- bar
。同样,我们不是写 foo.merge(master: bar)
,而是可以写 foo <<- bar
。
let foo: ViewStyle = [.text("foo")]
let bar: ViewStyle = [.text("bar"), .color(.red)]
// The merged results are of type `ViewStyle`
let fooMerged = foo <- bar // [.text("foo"), .color(.red)]
let barMerged = foo <<- bar // [.text("bar"), .color(.red)]
当然,<-
和 <<-
运算符可以在 ViewStyle
、ViewAttribute
和 [ViewAttribute]
之间交替使用。
这些运算符也可以和可选的 ViewStyle
一起使用。因此,如果需要,我们可以在 MyViewDefaultingToRed
的初始化器中使用 merge:slave
运算符来重新编写合并。
final class MyViewDefaultingToRed: UIView {
init(_ style: ViewStyle? = nil) {
let style = style <- .default
...
ViewComposer
使用右结合性进行链式合并当然,您也可以链式合并。但当我们链式合并三个操作数时,例如:
let foo: ViewStyle = ...
let bar: ViewStyle = ...
let baz: ViewStyle = ...
let result1 = foo <<- bar <<- baz
let result2 = foo <- bar <- baz
let result3 = foo <<- bar <- baz
let result4 = foo <- bar <<- baz
合并的顺序是什么?我们应该首先将 foo
与 bar
合并,然后将这个中间结果与 baz
合并?还是我们应该首先将 bar
与 baz
合并,然后将这个中间结果与 foo
合并?
这被称为 运算符结合性。在上面的例子中
不考虑左结合性或右结合性,单独的结果会相同。这意味着使用 左结合性 和 右结合性 时,result1
的值相同。同样适用于 result2
和 result3
。
let foo: ViewStyle = [.text("foo")]
let bar: ViewStyle = [.text("bar")]
// Associativity irrelevant
let result1 = foo <<- bar <<- .text("baz") // result: `[.text(.baz)]`
let result2 = foo <- bar <- .text("baz") // result: `[.text(.foo)]`
let result3 = foo <<- bar <- .text("baz") // result: `[.text(.bar)]`
但看看这个例子,结合性非常重要!
let foo: ViewStyle = [.text("foo")]
let bar: ViewStyle = [.text("bar")]
// Associativy IS relevant
// when using `right` associative:
let result4r = foo <- bar <<- .text("baz") // result: `[.text(.foo)]` USED IN ViewComposer
// When using `left` associative:
let result4l = foo <- bar <<- .text("baz") // result: `[.text(.baz)]`
ViewComposer
对 <-
以及 <<-
运算符都使用 右结合性。这意味着在链式合并运算符的值时,您应该从右到左阅读。
您还可以声明一些标准样式,例如 font
、textColor
、textAlignment
以及您想用于所有 UILabel
的上/下划线字符串。由于 ViewStyle
是可合并的,这使得在标签之间共享样式并将自定义值合并到共享样式以创建从合并样式创建的标签变得方便。
let style: ViewStyle = [.textColor(.red), .textAlignment(.center)]
let fooLabel: UILabel = labelStyle.merge(master: .text("Foo")))
再看看上述相同的例子,但这次利用 merge(master:
运算符 <<-
let labelStyle: ViewStyle = [.textColor(.red), .textAlignment(.center)]
let fooLabel: Label = labelStyle <<- .text("Foo")
在这里,<<-
运算符实际上直接创建了 UILabel
,而不是先创建 ViewStyle
。
让我们看看 ViewController 示例,利用预定义样式的强大功能和 <<-
运算符
private let labelStyle: ViewStyle = [.textColor(.red), .textAlignment(.center), .font(.boldSystemFont(ofSize: 30))]
class LabelsViewController: UIViewController {
private lazy var fooLabel: UILabel = labelStyle <<- .text("Foo")
private lazy var barLabel: UILabel = labelStyle <<- [.text("Bar"), .textColor(.blue), .color(.red)]
private lazy var bazLabel: UILabel = labelStyle <<- [.text("Baz"), .textAlignment(.left), .color(.green), .font(.boldSystemFont(ofSize: 45))]
lazy var stackView: UIStackView = [.views([self.fooLabel, self.barLabel, self.bazLabel]), .axis(.vertical), .distribution(.fillEqually)]
}
与常规相比:
class LabelsViewControllerVanilla: UIViewController {
private lazy var fooLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.text = "Foo"
label.textColor = .red
label.textAlignment = .center
label.font = .boldSystemFont(ofSize: 30)
return label
}()
private lazy var barLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.text = "Bar"
label.backgroundColor = .red
label.textColor = .blue
label.textAlignment = .center
label.font = .boldSystemFont(ofSize: 30)
return label
}()
private lazy var bazLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.text = "Baz"
label.backgroundColor = .green
label.textColor = .red
label.textAlignment = .left
label.font = .boldSystemFont(ofSize: 45)
return label
}()
lazy var stackView: UIStackView = {
let stackView = UIStackView(arrangedSubviews: [self.fooLabel, self.barLabel, self.bazLabel])
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.distribution = .fillEqually
stackView.axis = .vertical
return stackView
}()
}
private let height: CGFloat = 50
private let style: ViewStyle = [.font(.big), .height(height)]
private let fieldStyle = style <<- .borderWidth(2)
final class LoginViewController: UIViewController {
lazy var emailField: UITextField = fieldStyle <<- [.placeholder("Email"), .delegate(self)]
lazy var passwordField: UITextField = fieldStyle <<- [.placeholder("Password"), .delegate(self)]
// can line break merge
lazy var loginButton: UIButton = style <<-
.states([Normal("Login", .blue), Highlighted("Logging in...", .red)]) <-
.target(self.target(#selector(loginButtonPressed))) <-
[.color(.green), .cornerRadius(height/2)]
lazy var stackView: UIStackView = .axis(.vertical) <-
.views([self.emailField, self.passwordField, self.loginButton]) <-
[.spacing(20), .layoutMargins(all: 20), .marginsRelative(true)]
...
}
extension LoginViewController: UITextFieldDelegate {
public func textFieldDidEndEditing(_ textField: UITextField) {
textField.validate()
}
}
private extension LoginViewController {
@objc func loginButtonPressed() {
print("should login")
}
}
注意我们将 self
作为 关联值
传递给名为 .delegate
的属性,将 LoginViewController
类本身设置为 UITextViewDelegate
。
与原始的相比较
private let height: CGFloat = 50
final class VanillaLoginViewController: UIViewController {
lazy var emailField: UITextField = {
let field = UITextField()
field.translatesAutoresizingMaskIntoConstraints = false
field.placeholder = "Email"
field.layer.borderWidth = 2
field.font = .big
field.delegate = self
field.addConstraint(field.heightAnchor.constraint(equalToConstant: height))
return field
}()
lazy var passwordField: UITextField = {
let field = UITextField()
field.translatesAutoresizingMaskIntoConstraints = false
field.placeholder = "Password"
field.layer.borderWidth = 2
field.font = .big
field.delegate = self
field.addConstraint(field.heightAnchor.constraint(equalToConstant: height))
return field
}()
lazy var loginButton: UIButton = {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.layer.cornerRadius = height/2
button.addConstraint(button.heightAnchor.constraint(equalToConstant: height))
button.setTitle("Login", for: .normal)
button.setTitle("Logging in..", for: .highlighted)
button.setTitleColor(.blue, for: .normal)
button.setTitleColor(.red, for: .highlighted)
button.backgroundColor = .green
button.addTarget(self, action: #selector(loginButtonPressed), for: .primaryActionTriggered)
return button
}()
lazy var stackView: UIStackView = {
let stackView = UIStackView(arrangedSubviews: [self.emailField, self.passwordField, self.loginButton])
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.spacing = 20
let margins: CGFloat = 20
stackView.layoutMargins = UIEdgeInsets(top: margins, left: margins, bottom: margins, right: margins)
stackView.isLayoutMarginsRelativeArrangement = true
return stackView
}()
...
}
extension VanillaLoginViewController: UITextFieldDelegate {
public func textFieldDidEndEditing(_ textField: UITextField) {
textField.validate()
}
}
private extension VanillaLoginViewController {
@objc func loginButtonPressed() {
print("should login")
}
}
我们还可以使用 .delegates
属性为 UITextViewDelegate
以及更多代理类型。
在这里查看支持的属性列表。
感谢 Olivier Halligon,即 "AliSoftware" 撰写的代码片段——卓越的开源项目 SwiftGen 的创造者
extension ViewAttribute {
static func l10n(_ key: L10n) -> ViewAttribute {
return .text(key.string)
}
}
您可以用这种方式使用您的 L10n
枚举情况
let label: UILabel = [.l10n(.helloWorld), .textColor(.red)]
清晰、简洁且类型安全!
目前,您可以使用重复值的数组创建属性数组
// NEVER DO THIS!
let foobar: [ViewAttribute] = [.text("bar"), .text("foo")]
// NOR THIS
let foofoo: [ViewAttribute] = [.text("foo"), .text("foo")]
//NOR using style
let foobarStyle: Style = [.text("bar"), .text("foo")] // confusing!
// NOR this
let foofooStyle: Style = [.text("foo"), .text("foo")] // confusing!
存在属性包含重复值的数组。但是,使用它来实例化视图,例如 UILabel
时,实际上会忽略重复值。
let foobar: [ViewAttribute] = [.text("foo"), .text("bar")] //contains both, since array may contain duplicates
let label: UILabel = make(foobar) // func `make` calls `let style = attributes.merge(slave: [])` removing duplicates.
print(label.text!) // prints "foo", since duplicate value `.text("bar")` has been removed by call to `make`
因此,强烈建议不要使用重复值的数组进行实例化。但是,在将类型与重复值合并的场景中已得到处理,因为您已经选择了想要保留的属性,使用 merge:master
或 merge:slave
中的一个来保留它。
如果你想使用更加糖化的语法,可以使用符合类型Composable
的子类作为替代方案。你可以在以下链接中找到一些示例(Label、Button、StackView等):示例。
在ViewComposer的当前版本中,为了使用数组字面量,必须使用符号后缀操作符^
来创建继承自UIKit
类的Composable
类型子类。
final class Label: UILabel, Composable { ... }
...
let label: Label = [.text("foo")]^ // requires use of `ˆ`
其中有一个名为custom
的属性,它接受一个BaseAttributed
类型。如果你想要创建一个包含自定义属性的视图,这很有用。
在示例应用中,你可以找到两个使用custom
属性的情况,一个是简单的,另一个是高级的。
比如说,你创建了一个自定义属性FooAttribute
。
enum FooAttribute {
case foo(String)
}
然后创建一个共享协议,用于所有想要使用FooAttribute
进行样式化的类型,我们把这个协议叫作FooProtocol
。
protocol FooProtocol {
var foo: String? { get set }
}
在这个例子中,我们在FooAttribute
内部只声明了一个属性(case
),但当然可以声明多个。这些属性的列表应该包含在一个符合BaseAttributed
类型的类型中,这要求执行func install(on styleable: Any)
函数。在这个函数中,我们用属性来设计这种类型。现在可以清楚地看出,不跳过步骤 2,并使用一个桥接可以使用FooAttribute
的所有类型的协议会更加方便。
struct FooStyle: BaseAttributed {
let attributes: [FooAttribute]
init(_ attributes: [FooAttribute]) {
self.attributes = attributes
}
func install(on styleable: Any) {
guard var foobar = styleable as? FooProtocol else { return }
attributes.forEach {
switch $0 {
case .foo(let foo):
foobar.foo = foo
}
}
}
}
FooAttribute
的用法现在我们可以创建一个符合FooProtocol
的简单视图,我们可以使用自定义的FooAttribute
来设计。这可能是一个奇怪的例子,因为为什么不直接从UILabel
派生?但这样会让代码过于简短和简单,因为UILabel
已经符合Styleable
。这就是为什么要有这个例子,因为UIView
不符合Styleable
。
final class FooLabel: UIView, FooProtocol {
typealias Style = ViewStyle
var foo: String? { didSet { label.text = foo } } //
let label: UILabel
init(_ style: ViewStyle? = nil) {
let style = style <- .textAlignment(.center)]//default attribute
label = style <- [.textColor(.red)] //default textColor
super.init(frame: .zero)
compose(with: style) // setting up this view and calls `setupSubviews` below
}
}
extension FooLabel: Composable {
func setupSubviews(with style: ViewStyle) {
addSubview(label) // and add constraints...
}
}
现在我们可以创建和设计FooLabel
,我们使用“标准”的ViewAttribute
,同时通过使用custom
传递带有FooAttribute
的自定义FooStyle
(容器FooAttribute
)。
let fooLabel: FooLabel = [.custom(FooStyle([.foo("Foobar")])), .textColor(.red), .color(.cyan)]
在这里我们创建FooLabel
,并用我们的自定义FooStyle
进行样式化(容器FooAttribute
),同时也用textColor
和color
进行样式化。这样,你可以将自定义属性与“标准”属性结合起来。
完整的代码可以在示例应用中找到
请注意,自定义属性的合并不是自动发生的。参见下一节。
查看TriangleView.swift以获取自定义属性的高级用法示例。
Composable
类派生,并且其父类符合Makeable
不能使用数组字面量实例化的错误。(需要使用符号后缀操作符^
)UIDatePicker
UIScrollView
(由于UITableView
和UICollectionView
继承自它,因此很棘手)UITabBar
UIToolbar