制图员 3.2.5

制图员 3.2.5

nayanda1 维护。



 
依赖
Clavier~> 2.0.3
Builder~> 1.1.0
 

制图员 3.2.5

  • 作者
  • nayanda

制图员

制图员是一个专注于建造模式的Swift DSL框架。如果您仍在使用2.3.x版本,分离的README可在此处找到。如果您仍在使用Swift 5.1,请使用1.1.x版本。分离的README可在此处找到。

Codacy Badge build test SwiftPM Compatible Version License Platform


示例

要运行示例项目,克隆库,首先从示例目录运行 pod install

要求

  • Swift 5.5或更高版本
  • iOS 13.0或更高版本
  • XCode 13或更高版本

安装

Cocoapods

Draftsman 通过 CocoaPods 提供。要安装它,只需将以下行添加到您的 Podfile 中

pod 'Draftsman', '~> 3.2.4'

从 Xcode 使用 Swift Package Manager

  • 使用 Xcode 菜单 文件 > Swift 包 > 添加包依赖
  • 添加 https://github.com/hainayanda/Draftsman.git 作为 Swift 包 URL
  • 版本 中设置规则,选择 最高下一个主要版本 选项,并将 3.2.4 作为其版本
  • 单击下一步并等待

从 Package.swift 使用 Swift Package Manager

Package.swift 中将其添加为目标依赖

dependencies: [
    .package(url: "https://github.com/hainayanda/Draftsman.git", .upToNextMajor(from: "3.2.4"))
]

在你的目标中使用它作为 Draftsman

 .target(
    name: "MyModule",
    dependencies: ["Draftsman"]
)

作者

Nayanda Haberty, [email protected]

许可

Draftsman遵守MIT许可证。更多详情请见LICENSE文件。


基本用法

Draftsman是NSLayoutConstraintsUIView层级构建器。Draftsman使用Swift中新的resultBuilder从Swift,使得声明式方法成为可能。


基本

创建约束非常简单。你只需要调用drf来获取LayoutDraft对象

myView.drf
    .left.equal(to: otherView.drf.right)
    .right.equal(with: .parent).offset(by: 16)
    .top.lessThan(with: .safeArea).offSet(8)
    .bottom.moreThan(with: .top(of: .keyboard))
    .apply()

有两种方法来结束规划约束,可以从任何UIViewUIViewController调用它们

  • func apply() -> [NSLayoutConstraint]
  • func build() -> [NSLayoutConstraint]

两者的区别在于apply将激活约束,而build只创建约束而不激活它们。Apply返回值是可丢弃的,因此是否使用创建的NSLayoutConstraint由您决定。

你可以始终创建一个UIViewControllerUIView并实现Planned协议,在你想应用viewPlan时调用applyPlan()

import Draftsman

class MyViewController: UIViewController, Planned {
    
    var models: [MyModel] = []
    
    @LayoutPlan
    var viewPlan: ViewPlan {
        VStacked(spacing: 32) { 
            if models.isEmpty {
                MyView()
                MyOtherView()
                SomeOtherView()
            } else {
                for model in models {
                    MyModeledView(model)
                }
            }
        }
        .centered()
        .matchSafeAreaH().offset(by: 16)
        .vertical.moreThan(with: .safeArea).offset(by: 16)
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        applyPlan()
    }
}

ViewPlan总是可以组合来使代码更简洁

import Draftsman

class MyViewController: UIViewController, Planned {
    
    var models: [MyModel] = []
    
    @LayoutPlan
    var viewPlan: ViewPlan {
        VStacked(spacing: 32) { 
            stackPlan
        }
        .centered()
        .matchSafeAreaH().offset(by: 16)
        .vertical.moreThan(with: .safeArea).offset(by: 16)
    }

    @LayoutPlan
    var stackPlan: ViewPlan {
        if models.isEmpty {
            emptyStackPlan
        } else {
            modeledStackPlan(for: models)
        }
    }

    @LayoutPlan
    var emptyStackPlan: ViewPlan {
        MyView()
        MyOtherView()
        SomeOtherView()
    }

    @LayoutPlan
    func modeledStackPlan(for models: [MyModel]) -> ViewPlan {
        for model in models {
            MyModeledView(model)
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        applyPlan()
    }
}

使用UITableView或UICollectionView?用Draftsman和Combine轻松搞定

import Draftsman
import Combine

class MyViewController: UIViewController, Planned {
    
    @Published var models: [MyModel] = []
    
    @LayoutPlan
    var viewPlan: ViewPlan {
        Tabled(forCell: MyTableCell.self, observing: $models) { cell, model in
            cell.apply(model)
        }
        .fillParent()
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        applyPlan()
    }
}

视图层次

您可以在创建约束的同时创建视图层次,使用draftContentdrf.insert方法和子视图草稿的insert方法来插入(例如,draftStackedContentdrf.insertStacked,如果有通过UIStackView排列的子视图,则使用insertStacked)。别忘了调用apply()build(),两个都将重新排列视图层次,但只有apply()会激活创建的约束。

view.draftContent {
    UIView().drf
        .center.equal(to: .parent)
        .horizontal.equal(to: .safeArea)
        .vertical.moreThan(with: .safeArea)
        .insert {
            myView.drf
                .edges(.equal, to: .parent)
        }
}.apply()

视图的层次结构与您代码中声明的闭包很相似。上面的代码将按顺序执行以下操作

  1. 创建一个新的UIView
  2. 新的UIView将创建约束
  3. 新的UIView将插入myView
  4. myView将创建约束
  5. 所有约束都将被创建并激活

因此,如果层次是按伪层次结构样式编写的,它应类似于以下内容

view
|____new UIView
|    |____myView

传递给闭包的可兼容类型

  • UIView的任何子类
  • UIViewController的任何子类

如果传递UIViewController,它将自动将UIViewController视图作为子节点添加,并将UIViewController作为其当前UIViewController的子节点。您可以插入所需数量的组件,它将恰好符合您编写的所有视图。

使用Builder

在大多数情况下,您可以通过调用属性名称并通过再次调用drf来回到Draftsman来使用内置在Draftsman中的Builder库来构建您的视图

myView.drf
    .backgroundColor(.black)
    .drf.bottom.moreThan(to: .safeArea)
    .center.equal(to: .parent)

如果属性未映射,您可以在调用属性名称之前调用构建器

myView.drf.builder
    .backgroundColor(.black)
    .drf.bottom.moreThan(to: .safeArea)
    .center.equal(to: .parent)

使用Combine

在大多数情况下,您可以通过调用属性名称来使用Combine Publisher自动分配给属性,并通过调用storeAll(in:)stored()来回到Draftsman

@Published var myText: String?
@Published var myColor: UIColor?

var cancellables: Set<AnyCancellable> = .init()

...
...

UILabel().drf
    .text(assignedBy: $myText)
    .textColor(by: $myColor)
    .storeAll(in: &cancellables)
    .center.equal(to: .parent)
    .bottom.moreThan(to: .safeArea)

《storeAll(in:)》与《stored()`》的区别在于,《stored()`》会将可取消订阅项保留到视图本身中。如果您只需一次性使用这种方法来订阅此属性,则可以更好使用,除非您了解自己在做什么。

如果属性未映射,则可以在调用属性名称之前调用订阅者。

UILabel().drf.subscriber
    .text(assignedBy: $myText)
    .textColor(by: $myColor)
    .storeAll(in: &cancellables)
    .center.equal(to: .parent)
    .bottom.moreThan(to: .safeArea)

基本定位

定位视图非常简单。您只需声明哪个锚点应与其他锚点相关联即可

myView.drf
    .top.equal(to: other.drf.top)
    .right.moreThan(to: other.drf.right).offset(by: 8)
    .bottom.lessThan(to: other.drf.bottom).offset(by: 8).priority(.required)
    .left.equal(to: other.leftAnchor)
    .centerX.moreThan(to: other.centerXAnchor).inset(by: 8)
    .centerY.lessThan(to: other.centerYAnchor).inset(by: 8).identifier("centerY")

Draftsman 提供的基本定位锚点有

  • top(顶部)
  • left(左)
  • bottom(底部)
  • right(右)
  • centerX(水平中心)
  • centerY(垂直中心)
  • leading(前导)
  • trailing(尾部)

所有这些对于UIViewUILayoutGuide都是可用的。您可以使用以下三种方法之一来创建约束:

  • equal(to:)
  • moreThan(to:)
  • lessThan(to:)

这些方法可以接受来自UIKit的基本NSLayoutAnchor或者使用来自DraftsmanAnchor,只要它们位于同一轴线上。要添加常数,请使用其中一个offset(by:)inset(by:)方法。其中,`offset`是锚点外部的间距,而`inset`是锚点内部的间距。

alt text

对于中心锚点,offset和inset的描述如下图所示:

alt text

您还可以为创建的约束添加优先级或/和标识符。

基本尺寸设置

尺寸设置视图非常简单。您只需声明哪个锚点应与其他锚点或常数相关联即可}

myView.drf
    .height.equal(to: other.drf.width)
    .width.moreThan(to: other.drf.height).added(by: 8)
    .height.lessThan(to: anyOther.heightAnchor).substracted(by: 8).priority(.required)
    .width.equal(to: anyOther.widthAnchor).multiplied(by: 0.75).identifier("width")

Draftsman 提供的基本尺寸锚点有

  • height(高度)
  • width(宽度)

所有这些对于UIViewUILayoutGuide都是可用的。您可以使用以下三种方法之一来创建约束:

  • equal(to:)
  • moreThan(to:)
  • lessThan(to:)

这些方法可以接受来自《UIKit》的基本《NSLayoutDimension》或者使用来自《Draftsman》的尺寸《Anchor》。要添加常数,请使用其中一个`added(by:)`、`substracted(by:)`或`multiplied(by:]`方法。您还可以为创建的约束添加优先级或/和标识符。

使用常数也可以实现尺寸设置

myView.drf
    .height.equal(to: 32)
    .width.moreThan(to: 64)
    .width.lessThan(to: 128).priority(.required).identifier("width")

它与上述方法类似,但接受《CGFloat》类型的值。

组合两个或多个锚点

使用多个锚点创建约束非常简单,您总能结合两个或多个锚点,一次创建多个约束

myView.drf
    .top.left.equal(to: other.drf.top.left)
    .bottom.left.right.moreThan(to: anyOther.drf.top.left.right)

它将与单个锚点类似,但您只能传递具有相同轴组合的绘图员锚点

  • 所有相同的锚点组合都可以相互关联
  • top.lefttop.rightbottom.leftbottom.rightcenterX.centerY 都可以相互关联
  • top.leadingtop.trailingbottom.leadingbottom.trailingcenterX.centerY 都可以相互关联
  • top.left.bottomtop.right.bottom 都可以相互关联
  • top.left.rightbottom.left.right 都可以相互关联
  • top.leading.bottomtop.trailing.bottom 都可以相互关联
  • top.leading.trailingbottom.leading.trailing 都可以相互关联

锚点组合有一些快捷方式

  • verticaltop.bottom 相同
  • horizontalleft.right 相同
  • localizedHorizontalleading.trailing 相同
  • centercenterX.centerY 相同
  • edgestop.left.bottom.right 相同
  • localizedEdgestop.leading.bottom.trailing 相同
  • sizewidth.height 相同

示例

myView.drf
    .vertical.equal(to: other.drf.vertical)
    .bottom.horizontal.moreThan(to: anyOther.drf.top.horizontal)

使用 sizewidth.height 时,如果需要,也可以通过使用 CGSize 实现

myView.drf
    .size.equal(to: CGSize(sides: 30))

对于偏移量和内边距,CGFloat 与所有尺寸兼容。但如果您需要为每个边缘显式分配,您总是可以传递其他内容

  • VerticalOffsets 用于垂直锚点的偏移量
  • VerticalInsets 用于垂直锚点的内边距
  • HorizontalOffsets 用于水平锚点的偏移量
  • HorizontalInsets 用于水平锚点的内边距
  • AxisOffsets 用于交叉位置锚点的偏移量,它仅仅是 CGPoint 的别名
  • AxisInsets 用于交叉位置锚点的内边距,它仅仅是 CGPoint 的别名
  • EdgeOffsets 用于 3 和 4 位置锚点的偏移量,它仅仅是 UIEdgeInsets 的别名
  • EdgeInsets 用于 3 和 4 位置锚点的内边距,它仅仅是 UIEdgeInsets 的别名

隐含关系

您可以传递UIViewUILayoutGuide而不是明确传递Anchor,它将使用相同的锚点来创建约束。

myView.drf
    .vertical.equal(to: otherView)
    .bottom.horizontal.moreThan(to: view.safeAreaLayoutGuide)

在上面的示例中,它会创建myView垂直锚点和otherView垂直锚点之间的等距约束,然后它将创建另一个以myView的底部和底部。

匿名锚点

有时您不想甚至不能显式使用锚点。在这些情况下,您始终可以使用AnonymousLayout

myView.drf
    .top.left.equal(with: .parent)
    .bottom.moreThan(with: .safeArea).offset(by: 16)
    .size.lessThan(with: .previous)

可用AnonymousLayout包括

  • mySelf,这将自动获取当前视图
  • parent,这将自动获取当前父视图
  • safeArea,这将自动获取当前父视图的safeAreaLayoutGuide
  • keyboard,这将自动获取键盘布局指南(由Clavier提供)
  • keyboardSafeArea,这将自动获取带有safeArea的键盘布局指南(由Clavier提供)
  • previous,这将自动获取上一个视图
  • previousSafeArea,这将自动获取上一个safeAreaLayoutGuide

这与一个常规锚点相同,但它将自动获取匿名视图的相同锚点。如果您想显式获取匿名视图的不同锚点,则可以进行如下操作

myView.drf
    .top.equal(with: .top(of:.parent))
    .bottom.moreThan(with: .bottom(of: .safeArea)).offset(by: 16)
    .width.lessThan(with: .height(of: .previous))

可用的显式锚点包括

  • left(of:)
  • leading(of:)
  • right(of:)
  • trailing(of:)
  • centerX(of:)
  • top(of:)
  • bottom(of:)
  • centerY(of:)
  • topLeft(of:)
  • topLeading(of:)
  • topRight(of:)
  • topTrailing(of:)
  • bottomLeft(of:)
  • bottomLeading(of:)
  • bottomRight(of:)
  • bottomTrailing(of:)
  • center(of:)
  • centerLeft(of:)
  • centerLeading(of:)
  • centerRight(of:)
  • centerTrailing(of:)
  • centerTop(of:)
  • centerBottom(of:)

布局约束快捷键

构建布局约束的快捷方式有多种,可以通过 drf 访问。

  • fillParent() 等同于 edges.equal(with: .parent)
  • fillSafeArea() 等同于 edges.equal(with: .safeArea)
  • matchParentH() 等同于 horizontal.equal(with: .parent)
  • matchParentV() 等同于 vertical.equal(with: .parent)
  • matchSafeAreaH() 等同于 horizontal.equal(with: .safeArea)
  • matchSafeAreaV() 等同于 vertical.equal(with: .safeArea)
  • matchParentSize() 等同于 size.equal(with: .parent)
  • centered() 等同于 center.equal(with: .parent)
  • centeredH() 等同于 centerX.equal(with: .parent)
  • centeredV() 等同于 centerY.equal(with: .parent)
  • cornered(atestone 等同于 top.left.equal(with: .parent),或任何其他角落
  • widthMatchHeight() 等同于 width.equal(with: .height(of: .mySelf))
  • heightMatchWidth() 等同于 height.equal(with: .width(of: .mySelf))
  • sized(_:) 等同于 size.equal(with: givenSize)

UITableView 和 UICollectionView

使用 Draftsman 与 UITableViewUICollectionView 一起工作非常简单。只需调用 renderCells 和任何一系列可哈希对象。

UITableView().drf.renderCells(using: myArrayOfHashables) { item in
    MyTableCell.render { cell in
        cell.apply(with: item)
    }
}

对不同的项目使用多个单元?只需渲染它。

UITableView().drf.renderCells(using: myArrayOfEnum) { item in
    switch item { 
    case .typeOne:
        MyTableCellOne.render { cell in
            cell.apply(with: item)
        }
    case .typeTwo:
        MyTableCellTwo.render { cell in
            cell.apply(with: item)
        }
    }
}

这在 UICollectionView 上也同样适用。

UICollectionView().drf.renderCells(using: myArrayOfHashables) { item in
    MyCollectionCell.render { cell in
        cell.apply(with: item)
    }
}

此功能由 UITableViewDiffableDataSourceUICollectionViewDiffableDataSource 提供支持。

SectionCompatible

如果您使用分区 UITableViewUICollectionView,请调用 renderSections 并传入 SectionCompatible 序列。

UITableView().drf.renderSections(using: myArrayOfSectionCompatibles) { item in
    MyTableCell.render { cell in
        cell.apply(with: item)
    }
}

SectionCompatible 是一个声明如下的协议

public protocol SectionCompatible {
    associatedtype Identifier: Hashable
    associatedtype Item: Hashable
    var identifier: Identifier { get }
    var items: [Item] { get }
}

如果您不想实现 SectionCompatible,可以使用 Sectioned 结构体代替。

Sectioned(myIdentifier, items: myArrayOfItems)

使用 Combine 渲染单元格

大多数情况下,您的单元格会随着时间的推移而改变,您不希望再次渲染整个表格。在这种情况下,您可以使用合并 Publisher 而不是序列。

@Published var myItems: [Item] = []

...
...

UITableView().drf.renderCells(observing: $myItems) { item in
    MyTableCell.render { cell in
        cell.apply(with: item)
    }
}

每当发布者发布更改时,表格将根据已发布的项更新。这也适用于 renderSections

@Published var mySections: [Sectioned<MySection, Item>] = []

...
...

UITableView().drf.renderSections(observing: $mySections) { item in
    MyTableCell.render { cell in
        cell.apply(with: item)
    }
}

所有这些功能都在 UITableViewUICollectionView 上都可用。

自定义视图

空格视图(SpacerView)

您可以使用SpacerView作为UIStackView内容的间隔。

UIScrollView().drf.insertStacked { 
    MyView()
    SpacerView(12)
    OtherView()
}

或者,如果您希望间隔的大小是动态的,可以在初始化时留空。

UIScrollView().drf.insertStacked { 
    MyView()
    SpacerView()
    OtherView()
}

可滚动栈视图(ScrollableStackView)

有两种自定义的 UIView 被命名为 ScrollableStackView,它是一个在 UIScrollView 内的 UIStackView。如果您需要一个内容大于容器的 UIStackView 并可滚动的,可以使用它。它有两个公开的初始化方法可供使用:

  • init(frame: CGRect)
  • init(frame: CGRect = .zero, axis: NSLayoutConstraintAxisSize, margins: UIEdgeInsets? = nil, alignment: UIStackViewAlignment = .center, spacing: CGFloat = .zero)

除此之外,它可以像常规 UIStackView 和常规 UIScrollView 一样使用,但不能更改其分布,因为这需要确保视图的行为正确。


布局辅助工具

如果在缩短 viewPlan 和使其更简洁时有所需求,可以使用一些辅助工具。此辅助工具将接受 LayoutPlan 闭包,因此您不需要通过 drf 访问它,而是可以直接在它的初始化时访问。

水平堆叠和垂直堆叠

水平堆叠垂直堆叠 是创建水平和垂直 UIStackView 的快捷方式,无需显式创建。它有 3 个公共初始化器可供使用。

  • init(_ stack: UIStackView = UIStackView(), @LayoutPlan _ layouter: () -> ViewPlan) {
  • init(margins: UIEdgeInsets? = nil, distribution: UIStackView.Distribution = .fill, alignment: UIStackView.Alignment = .fill, spacing: CGFloat = .zero, @LayoutPlan _ layouter: () -> ViewPlan)
  • init(margin: CGFloat, distribution: UIStackView.Distribution = .fill, alignment: UIStackView.Alignment = .fill, spacing: CGFloat = .zero, @LayoutPlan _ layouter: () -> ViewPlan)

示例

VStacked(distribution: .fillEqually) { 
    SomeView()
    MyView()
    OtherView()
}
.fillParent()

这将与以下操作等效

UIStackView(axis: .vertical, distribution: .fillEqually).drf
    .fillParent()
    .insertStacked { 
        SomeView()
        MyView()
        OtherView()
    }

水平可滚动堆叠和垂直可滚动堆叠

水平可滚动堆叠垂直可滚动堆叠 是创建水平和垂直 ScrollableStackView 的快捷方式,无需显式创建。它有 3 个公共初始化器可供使用。

  • init(_ stack: ScrollableStackView = ScrollableStackView(), @LayoutPlan _ layouter: () -> ViewPlan) {
  • init(margins: UIEdgeInsets? = nil, alignment: UIStackView.Alignment = .fill, spacing: CGFloat = .zero, @LayoutPlan _ layouter: () -> ViewPlan)
  • init(margin: CGFloat, alignment: UIStackView.Alignment = .fill, spacing: CGFloat = .zero, @LayoutPlan _ layouter: () -> ViewPlan)

示例

HScrollableStacked(alignment: .fill) { 
    SomeView()
    MyView()
    OtherView()
}
.fillParent()

这将与以下操作等效

ScrollableStackView(axis: .horizontal, alignment: .fill).drf
    .fillParent()
    .insertStacked { 
        SomeView()
        MyView()
        OtherView()
    }

Tabled(forCell: MyCell.self, observing: $items) { cell, item in
    cell.apply(item)
}

这将与以下操作等效

UITableView().drf.renderCells(observing: $items) { item in
    MyCell.render { cell in
        cell.apply(with: item)
    }
}

Collectioned(forCell: MyCell.self, observing: $items) { cell, item in
    cell.apply(item)
}

Margined(by: 12) { 
    MyView()
}
.fillParent()

这将与以下操作等效

UIView().drf.builder
    .backgroundColor(.clear)
    .drf.fillParent()
    .insert { 
        MyView().fillParent().offsetted(by: 12)
    }

public protocol Planned: AnyObject {
    var planIdentifier: ObjectIdentifier { get }
    var appliedConstraints: [NSLayoutConstraint] { get }
    var viewPlanApplied: Bool { get }
    @LayoutPlan
    var viewPlan: ViewPlan { get }
    @discardableResult
    func applyPlan() -> [NSLayoutConstraint]
}

import Draftsman

class MyViewController: UIViewController, Planned {
    
    @LayoutPlan
    var viewPlan: ViewPlan {
        VStacked(spacing: 32) { 
            MyView()
            MyOtherView()
            SomeOtherView()
        }
        .centered()
        .matchSafeAreaH().offset(by: 16)
        .vertical.moreThan(with: .safeArea).offset(by: 16)
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        applyPlan()
    }
}

PlannedCell 是为特定单元格定制的 Planned 类型,声明方式如下

public protocol PlannedCell: Planned {
    @LayoutPlan
    var contentViewPlan: ViewPlan { get }
}

您需要实现的是 contentViewPlan getter,因为所有内容都将由扩展实现。它会跳过 contentView 直接进入内容的实现

class TableCell: UITableView, PlannedCell {
    
    @LayoutPlan
    var contentViewPlan: ViewPlan {
        HStacked(margin: 12, spacing: 8) { 
            UIImageView(image: UIImage(named: "icon_test")).drf
                .sized(CGSize(sides: 56))
            VStacked(margin: 12, spacing: 4) {
                UILabel(text: "title text")
                UILabel(text: "subtitle text")
            }
        }
    }
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        applyPlan()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        applyPlan()
    }
}

  • UITablePlannedCell,它是 UITableViewCell & PlannedCell
  • UICollectionPlannedCell,它是 UICollectionViewCell & PlannedCell

Planned Stack

PlannedStack 是为特定单元格定制的 Planned 类型,声明方式如下

public protocol PlannedStack: Planned {
    @LayoutPlan
    var stackViewPlan: ViewPlan { get }
}

您需要实现的只有 stackViewPlan getter,因为所有内容都将由扩展实现。它会自动将计划视为堆栈的 arrangeSubviews

class MyStack: UIStackView, PlannedStack {
    
    @LayoutPlan
    var stackViewPlan: ViewPlan {
        UIImageView(image: UIImage(named: "icon_test"))
            .sized(CGSize(sides: 56))
        UILabel(text: "title text")
        UILabel(text: "subtitle text")
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        applyPlan()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        applyPlan()
    }
}

您可以使用 UIPlannedStack,因为是 UIStackView & PlannedStack 的别称


贡献

您知道怎么做,只需克隆并提交一个 pull request