ViewComposer 0.2.31

ViewComposer 0.2.31

测试已测试
Lang语言 SwiftSwift
许可证 MIT
发布最后发布2017年11月
SwiftSwift版本4.0.2
SPM支持SPM

Alexander Cyon 维护。



Platform
Carthage Compatible
Version
License

ViewComposer

使用枚举数组及其属性来设置视图样式

let label: UILabel = [.text("Hello World"), .textColor(.red)]

目录

安装

Swift 4

您可以使用swift4分支在Swift 4代码中使用ViewComposer,例如,在使用CocoaPods时,Podfile将是

pod 'ViewComposer', :git => 'https://github.com/Sajjon/ViewComposer.git', :branch => 'swift4'

Swift 3

CocoaPods

pod 'ViewComposer', '~> 0.2'

Carthage

github "ViewComposer" ~> 0.2

Swift包管理器

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
    }()

    ...
}

非侵入式 - 标准UIKit视图

正如我们在上面的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:mastermerge:slave,即,您需要决定保留哪个值。

示例

使用 merge:slavemerge: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)]

当然,<-<<- 运算符可以在 ViewStyleViewAttribute[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

合并的顺序是什么?我们应该首先将 foobar 合并,然后将这个中间结果与 baz 合并?还是我们应该首先将 barbaz 合并,然后将这个中间结果与 foo 合并?

这被称为 运算符结合性。在上面的例子中

不考虑左结合性或右结合性,单独的结果会相同。这意味着使用 左结合性右结合性 时,result1 的值相同。同样适用于 result2result3

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<- 以及 <<- 运算符都使用 右结合性。这意味着在链式合并运算符的值时,您应该从右到左阅读。

预定义样式

您还可以声明一些标准样式,例如 fonttextColortextAlignment 以及您想用于所有 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 以及更多代理类型。

支持属性

在这里查看支持的属性列表

SwiftGen支持

viewcomposer + SwiftGen =❤️

感谢 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:mastermerge: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

步骤 1:创建属性枚举

enum FooAttribute {
    case foo(String)
}

步骤 2(可选):使用自定义属性的类型的协议

然后创建一个共享协议,用于所有想要使用FooAttribute进行样式化的类型,我们把这个协议叫作FooProtocol

protocol FooProtocol {
    var foo: String? { get set }
}

步骤 3(最终):创建包含自定义属性列表的样式

在这个例子中,我们在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),同时也用textColorcolor进行样式化。这样,你可以将自定义属性与“标准”属性结合起来。

完整的代码可以在示例应用中找到

请注意,自定义属性的合并不是自动发生的。参见下一节。

合并自定义属性

查看TriangleView.swift以获取自定义属性的高级用法示例。

路线图

架构/实现

  • 将实现更改为使用Codable/Encodable(Swift 4)?
  • 修复了符合条件的类从Composable类派生,并且其父类符合Makeable不能使用数组字面量实例化的错误。(需要使用符号后缀操作符^

支持UIKit视图