SwiftWidgets 是一个UIKit小部件框架,专注于开发速度、可重用性和可组合性。
关于
SwiftUI 是未来(并且相当出色),但对于仍然需要支持iOS 12(以及 11 和有时甚至 10)的人来说,我们可能需要几年的时间才能在生产环境中使用它。 SwiftWidgets 试图通过使用 可重用 和 可组合 的视图容器(小部件)来简化UIKit开发。小部件主要用于表格视图(WidgetTableViewController
),但您可以轻松地将其嵌入到堆叠视图、滚动视图、其他小部件或普通的 UIView
中。
安装
CocoaPods 是Cocoa项目的依赖管理器。您可以使用以下命令安装它
$ gem install cocoapods
要使用 CocoaPods 将 SwiftWidgets 集成到您的 Xcode 项目中,请在您的 Podfile
中指定它
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '10.0'
use_frameworks!
target '<Your Target Name>' do
pod 'SwiftWidgets'
end
然后,运行以下命令
$ pod install
快速开始
import SwiftWidgets
class QuickstartExample: WidgetTableViewController {
override func viewDidLoad() {
super.viewDidLoad()
addWidget(ImageWidget.self) { // 1
$0.image.image = 🏙
$0.height = 150
}
addReusableWidget(LabelWidget.self) { // 2
$0.text.text = "Label widget with a pretty long text that will auto grow."
}
addReusableWidget(LabelWidget.self) { // 3
$0.text.text = "Different font and text color."
$0.text.font = Settings.Font.with(size: 45)
$0.text.color = Settings.Color.darkGray
$0.text.alignment = .center
$0.tap = { widgetInstance in
print("Text tapped - widget", widgetInstance)
}
}
addWidget(ButtonWidget.self) { // 4
$0.text.text = "Button"
$0.text.font = Settings.Font.title
$0.text.color = .white
$0.padding.vertical = Settings.Offset.basic
$0.cornerRadius = 1.0
$0.buttonColor = Settings.Color.primary
$0.height = 80
$0.click = {
print("button click")
}
}
}
}
简要说明
- 向表格视图添加一个新的图像小部件。使用闭包将图像源设置为一个
UIImage
并将小部件高度设置为150
。 - 添加一个可重用标签小部件(小部件视图通过
tableView.dequeueReusableCell
重新使用,并使用新的模型实例重新初始化)。 - 添加一个标签小部件,设置文本内容、字体、颜色和对齐方式,并设置点击回调处理程序。
- 添加一个按钮小部件,具有自定义文本、圆角、垂直内边距和点击回调。按钮可以是禁用的,因此最好是使用
click
回调代替常规的tap
,以防止在禁用状态下接收事件。
在以下各节中,我们将讨论部件解剖和生命周期、设置、制作自己的部件等内容。
部件解剖
部件由视图(继承自Widget
)和模型(继承自WidgetModel
)组成。要制作自己的部件,只需实现一对视图 - 模型并对typealias Model
进行设置,使其用于模型类名。WidgetInstantiable
协议将处理其余部分。
class CustomWidget: Widget, WidgetInstantiable {
typealias Model = CustomWidgetModel
@IBOutlet weak var mainLabel: UILabel!
@IBOutlet weak var imageView: UIImageView!
@IBOutlet weak var spacingConstraint: NSLayoutConstraint!
public let imageComp = ImageComponent()
override func load() {
super.load()
spacingConstraint.constant = CGFloat(model.titleImageSpacing)
imageComp.setup(target: imageView, model: model.image, widgetModel: model)
LabelComponent().setup(target: mainLabel, model: model.text, widgetModel: model)
}
}
class CustomWidgetModel: WidgetModel {
var image = ImageComponentModel()
var text = LabelComponentModel()
var titleImageSpacing: Float = 20
// override defaults
override func afterInit() {
text.alignment = .center
height = 300
}
}
部件布局
SwiftWidgets使用标准的margin - padding - content
布局模型。
间距
使用$0.margin.left
、$0.margin.right
、$0.margin.top
和$0.margin.bottom
设置间距内衬,以及便利属性$0.margin.horizontal
(用于left & right)、$0.margin.vertical
(用于top & bottom)和$0.margin.all
(用于所有4个)。主视图的背景颜色使用$0.color.background
设置。
填充
使用类似的$0.padding.left
等设置填充内衬,填充视图的背景使用$0.color.padding
设置。
内容
主要内容视图通过addContentView()
方法添加或在一个xib
文件中自动选择为根视图的第一个子视图。其背景颜色使用$0.color.content
设置。
分隔符
小部件可以有一个可选的分隔符(底部的一条水平线),它可以通过使用$0.separator.XXX
进行自定义。
open class SeparatorModel {
public var enabled: Bool // is separator shown or not?
public var height: Float // height of the separator
public var color: UIColor // separator color
public var rightPadding: Float // right padding
public var leftPadding: Float // left padding
}
附件视图
您还可以使用预定义的类型(如.disclosureIndicator
、.activityIndicator
、.checkmark
)以及通过提供图像($0.accessory.image
)或视图($0.accesory.view
)来设置附件视图($0.accessory.type
)。其他可自定义的属性包括$0.accessory.size
、$0.accessory.rightPadding
和$0.accessory.color
。
尺寸
默认情况下,小部件通过其内部约束定义了固有的大小(例如LabelWidget
,其大小随内容文本增长)。但是,您也可以通过设置$0.height = 150
显式定义其高度。这通常发生在您想要表格中所有小部件具有固定大小时。
默认设置
在init
方法中,小部件模型被填充为默认值。默认值是通过调用设置的
Settings.initDefaults(Config())
其中Config
实现了SettingsConfig
协议。
您还可以直接获取默认值,例如在此处我们使用默认标题字体、主颜色和2倍的常规填充设置标签。
addReusableWidget(LabelWidget.self) {
$0.text.text = "Use default settings"
$0.text.font = Settings.Font.title // we could also use an explicit font size: Settings.Font.with(size: 25)
$0.text.color = Settings.Color.primary
$0.padding.all = Settings.Offset.basic2 // .basic = N, basic2 = 2*N, basic4 = 4*N etc.
}
有关更多默认值,请查找SettingsConfig
协议,以及SettingsOffset
(Settings.Offset.XXX
)、SettingsColor
(Settings.Color.XXX
)和SettingsFont
(Settings.Font.XXX
)。
只要您仅使用Settings
属性设置小部件,您就可以使用不同的配置轻松为小部件着色。
Widget生命周期
构建
Widget视图可以是通过代码(通过重写build()
方法)构建的,也可以是从与widget视图同名的一个xib
实例化的。对于来自Widget解剖学的CustomWidget
,该widget定义在CustomWidget.xib
和CustomWidget.swift
中。如果未使用xib
,则widget视图必须实现build()
方法。
public lazy var mainContent: UITextField = {
let view = UITextField()
return view
}()
public override func build() {
addContentView(mainContent)
}
build()
在生命周期中只调用一次,因此请使用它来创建所有必要的视图,使用约束排列它们,并使用addContentView
设置根视图。addContentView
负责设置管理基本布局(填充、边距等)所需的视图和约束。
加载
在构建widget视图后,一个模型被添加,并通过在load()
中使用模型数据设置widget。如果您计划在table view(addReusableWidget
)中重用widget,您需要在视图生命周期的多个阶段处理被多次调用的load()
,每次使用不同的模型。这可能导致一些清理工作,特别是如果您的widget保持状态。如果您不计划重用widget,则在视图生命周期中只发生一次一次使用单个模型的load()
。
附加到单元格
在WidgetTableViewController
中的widget嵌入到table view cell(WidgetTableViewCell
)中。您可以在attachedToCell()
中更改cell。
override func attachedToCell() {
parentCell?.isUserInteractionEnabled = true
}
重用
在重用视图时,将添加一个新的模型,并再次调用load()
和attachedToCell()
。之后,视图就可以再次使用了。
小部件容器
存在几个内置容器用于显示和布局小部件(如下列出)。如果您需要更具体的容器,您可以通过实现 WidgetContainer
协议轻松地编写自己的容器。该协议定义了如 addWidget
等常见方法,因此在不同容器中显示小部件时需要做的努力很少(主要是设置容器)。
WidgetTableViewController
WidgetTableViewController
是默认容器,用于显示小部件。它会在表格视图中自动显示小部件,并负责滚动、重复利用小部件等。
VerticalWidgetViewContainer
VerticalWidgetViewContainer
水平地渲染小部件。它不提供滚动和重复利用小部件的功能。一个典型的用法是将容器固定在窗口底部,然后在其他屏幕内容上显示一个小部件或两个小部件。
Vertical Container Widget
VContainerWidget
在小部件内部垂直渲染小部件。它基本上是将 VerticalWidgetViewContainer
封装在小部件内的容器。
Horizontal Scroll Container Widget
HorizontalScrollContainerWidget
在横向滚动条内部渲染小部件。小部件的宽度是固定的。
水平容器控件
HContainerWidget
可以水平渲染控件,并支持多种对齐选项。
嵌入视图
您可以将控件轻松地嵌入到一个 UIView
中,如下所示:
let maker = WidgetCreator()
let button = maker.getWidget(ButtonWidget.self) {
$0.text.text = "Button"
}
button.embedIn(parentView)
自定义控件
多数控件主要用于显示图片和文本,并且以水平和垂直堆叠的形式布局。《SwiftWidgets》提供了一些组件,用于以标准化方式定义和显示文本和图片,以及一些布局视图的实用类。
让我们看看一个显示头像、姓名和角色名称的控件示例
class ActorWidget: Widget, WidgetInstantiable {
typealias Model = ActorWidgetModel
let content = HViewContainer()
let vertical = VViewContainer()
let nameLabel = UILabel()
let characterLabel = UILabel()
let imageView = UIImageView()
public let imageComp = ImageComponent()
override func build() {
vertical.views = [nameLabel, characterLabel] // 3
vertical.verticalAlignment = .center
content.views = [imageView, vertical] // 4
content.verticalAlignment = .center
addContentView(content) // 5
}
override func load() {
super.load()
content.spacing = Settings.Offset.basic
vertical.spacing = Settings.Offset.basic
content.layoutContent() // 6
vertical.layoutContent()
imageView.snp.makeConstraints {
$0.size.equalTo(60) // 7
}
LabelComponent().setup(target: nameLabel, model: model.name, widgetModel: model) // 8
LabelComponent().setup(target: characterLabel, model: model.character, widgetModel: model)
imageComp.setup(target: imageView, model: model.image, widgetModel: model)
}
}
class ActorWidgetModel: WidgetModel {
public var name = LabelComponentModel() // 1
public var character = LabelComponentModel()
public var image = ImageComponentModel() // 2
override func afterInit() {
image.setup = { image in
image.layer.cornerRadius = 30
image.clipsToBounds = true
}
}
}
LabelComponentModel
可以自定义标签的文本、字体、颜色、对齐方式和行数ImageComponentModel
可以自定义图像的图像/imageUrl、内容模式、占位图像、错误图像、活动指示器和图像视图的tintColor- 设置一个包含演员名称和角色标签的垂直容器,标签垂直居中
- 设置一个包含演员个人简介图像视图和上一步中垂直容器的水平容器
- 将水平容器设置为控件的 内容视图
- 在垂直和水平容器内布局子视图
- 在前面的步骤中覆盖了子视图的约束,因此我们需要设置图像视图的尺寸
- 根据模型数据设置标签和图像视图组件
然后只需添加控件并进行配置。您可以通过指定不同的字体、颜色等轻松地制作变体控件。
protocol WidgetMaker: WidgetContainer {
}
extension WidgetMaker {
func addActor(_ actor: MovieCastViewModel) {
addWidget(ActorWidget.self) {
$0.image.imageUrl = URL(string: imageUrl)
$0.image.contentMode = .scaleAspectFill
$0.name.text = actor.name
$0.name.font = Settings.Font.with(size: 18, weight: .semibold)
$0.character.text = actor.character
$0.character.font = Settings.Font.with(size: 14)
$0.character.color = Settings.Color.darkGray
$0.padding.vertical = Settings.Offset.basic2
$0.padding.horizontal = Settings.Offset.basic2
$0.accessory.type = .disclosureIndicator
$0.separator.enabled = true
}
}
}
开发
SwiftWidgets 目前处于测试预览版,核心接口可能发生变化。欢迎提交 Pull Request!
许可证
SwiftWidgets采用MIT许可证发布。详情见LICENSE文件。