...或者 UIKit 的光明新未来(也许)
注意
Subviews 使用的是属性包装器,而不是宏,因此它支持 Swift 5.5 以来的所有 Swift 版本
Subviews 是一个构建于 UIKit 之上的轻量级软件包,它提供了一种编写自定义视图子类的新方式,用更少的样板代码和更高的清晰度。
简而言之,它将所有这些代码转换成
final class HelloWorldView: UIView {
let label = UILabel()
init() {
super.init(frame: .zero)
configure()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure() {
label.text = "Hello, UIKit 2023"
label.translatesAutoresizingMaskIntoConstraints = false
addSubview(label)
NSLayoutConstraint.activate([
label.topAnchor.constraint(equalTo: topAnchor),
label.bottomAnchor.constraint(equalTo: bottomAnchor),
label.leadingAnchor.constraint(equalTo: leadingAnchor),
label.trailingAnchor.constraint(equalTo: trailingAnchor),
])
}
}
这样
import Subviews
final class HelloWorldView: Superview {
@Subview(.pin, {
$0.text = "Hello, UIKit 2023"
})
var label = UILabel()
}
注意
帮助拯救乌克兰。通过 United24 捐款,这是乌克兰总统的官方筹款平台
具有快速布局选项的最简单视图
final class EmptyStateView: Superview {
@Subview([.alignCenterX, .alignCenterY(offset: -10)], {
$0.text = "This list is empty"
})
var label = UILabel()
}
更高级的示例,使用堆栈视图和使用 self
的动态视图配置
final class EmojiLogoView: Superview {
let republicName: String
let republicEmoji: String
@Subview(.pin, {
$0.axis = .vertical
$0.alignment = .center
})
var stackView = UIStackView()
@ArrangedSubview(of: \.stackView, { (label, self) in
label.font = .systemFont(ofSize: 60)
label.text = self.republicEmoji
})
var emojiSeal = UILabel()
@ArrangedSubview(of: \.stackView, { (label, self) in
label.font = .systemFont(
ofSize: 20,
weight: .heavy,
width: .condensed
)
label.text = self.republicName
})
var nameLabel = UILabel()
}
let republicOfBoba = EmojiLogoView(republicName: "REPUBLIC OF BOBA", republicEmoji: "🧋")
更高级的示例,包含更多快速布局选项和使用 self
的动态视图创建
final class BestFlagView: Superview {
let republicName: String
let republicEmoji: String
@Subview([.pinTop, .pinHorizontally, .relativeHeight(0.8)], {
$0.backgroundColor = .white
})
var whiteBackground = UIView()
@Subview([.pinBottom, .pinHorizontally, .relativeHeight(0.2)], {
$0.backgroundColor = .systemRed
})
var redStripe = UIView()
@Subview(of: \.whiteBackground, [.alignCenterX, .pinBottom(inset: 4)])
var logo = { (self) in
EmojiLogoView(
republicName: self.republicName,
republicEmoji: self.republicEmoji
)
}
}
let bestFlag = BestFlagView(republicName: "CALIFORNIA BURRITO", republicEmoji: "🌯")
也适用于视图控制器
final class EmptyStateVC: ParentViewController {
// use @Child to add child view controllers:
@Child([.safeAreaPin], {
$0.view.backgroundColor = .systemGray
})
var backgroundVC = UIViewController()
// @Subview is also supported:
@Subview(.pin)
var emptyStateView = EmptyStateView()
}
https://github.com/dreymonde/Subviews.git
首先,您应该始终使用 Superview
(或您自己的继承自 Superview
的类)作为自定义 UIView 的基类(对于自定义视图控制器,使用 ParentViewController
)。这将确保所有 @Subview
和 @Child
属性都被正确添加。
注意
如果您不想更改您的基类,请参阅 在不继承Superview
的情况下使用@Subview
final class CustomView: Superview {
// ...
}
final class CustomViewController: ParentViewController {
// ...
}
@Subview
s 可以添加到视图以及视图控制器
final class CustomView: Superview {
@Subview(.pin)
var button = UIButton(type: .system)
}
如果您没有明确指定父视图,子视图将直接添加到 self
(对于视图控制器,则是 self.view
)。或者您可以使用 @Subview(of:)
来使用不同的父视图,从而创建视图层级结构
final class CustomView: Superview {
@Subview(.pin) // added to `self`
var background = UIView()
// use the "\." keypath syntax!
@Subview(of: \.background, .marginsPin) // added to `background`
var button = UIButton(type: .system)
}
Subviews 提供了许多方便易用的布局修饰符。它们都是 100% UIKit 并且基于自动布局
final class GreenFlagView: Superview {
@Subview([
.marginsPinHorizontally,
.pinVertically(insets: .all(4)),
.height(40),
.aspectRatio(3.0/2.0)
])
var rectangularFlag = RectangleView(color: .green)
}
所有可用快速布局选项的列表 (ViewLayoutOption
结构)
// Center:
// `offset` parameter is optional
.alignCenter(offset:)
.alignCenterX(offset:)
.alignCenterY(offset:)
// Size:
.size(_ size:)
.height(_ height:)
.width(_ width:)
.aspectRatio(_ widthToHeight:)
.aspectRatioSquare
.relativeSize(_ relativeSize:)
.relativeHeight(_ relativeHeight:)
.relativeWidth(_ relativeWidth:)
// Edges Pin:
// `insets` / `inset` parameter is optional
.pin(insets:)
.pin(inset:)
.pinHorizontally(insets:)
.pinVertically(insets:)
.pinBottom(inset:)
.pinTop(inset:)
.pinLeading(inset:)
.pinTrailing(inset:)
// Margins Pin:
// `insets` / `inset` parameter is optional
.marginsPin(insets:)
.marginsPin(inset:)
.marginsPinHorizontally(insets:)
.marginsPinVertically(insets:)
.marginsPinBottom(inset:)
.marginsPinTop(inset:)
.marginsPinLeading(inset:)
.marginsPinTrailing(inset:)
// Safe Area Pin:
// `insets` / `inset` parameter is optional
.safeAreaPin(insets:)
.safeAreaPin(inset:)
.safeAreaPinHorizontally(insets:)
.safeAreaPinVertically(insets:)
.safeAreaPinBottom(inset:)
.safeAreaPinTop(inset:)
.safeAreaPinLeading(inset:)
.safeAreaPinTrailing(inset:)
// Readable Content Guide Pin:
// `insets` / `inset` parameter is optional
.readableContentPin(insets:)
.readableContentPin(inset:)
.readableContentPinHorizontally(insets:)
.readableContentPinVertically(insets:)
.readableContentPinBottom(inset:)
.readableContentPinTop(inset:)
.readableContentPinLeading(inset:)
.readableContentPinTrailing(inset:)
您可以将一个或多个布局选项与 @Subview
或 @Child
一起使用
@Subview(.alignCenter)
@Subview([.pinTop, .pinBottom, .marginPinLeading])
@Child(.safeAreaPin)
@Child([.pinVertically, .alignCenterX, .relativeWidth(0.8)])
如果您想对子视图本身执行任何配置,或微调自动布局代码,您可以简单地在 @Subview
或 @Child
中使用基本配置块
final class SuccessLabel: Superview {
@Subview(.pin, {
$0.text = "Success!"
$0.font = .systemFont(ofSize: 24, weight: .bold)
$0.textColor = .systemGreen
})
var label = UILabel()
}
关于 Subviews 最令人惊奇的事情之一是,它允许您在配置块内使用 self
!纯 Swift 泛型,没有魔法。
它适用于两种情况。 首先,它允许您构建复杂的自动布局约束,而仅仅使用快速布局选项是不够的。例如
final class BobaLabel: Superview {
@Subview([.pinLeading], { (label) in
label.text = "🧋"
label.font = .systemFont(ofSize: 48, weight: .heavy)
})
var bobaEmoji = UILabel()
// parentheses around (label, self) are required by Swift
@Subview([.pinTrailing, .pinVertically], { (label, self) in
label.text = "Boba\nRepublic"
label.numberOfLines = 0
label.font = .systemFont(ofSize: 48, weight: .heavy)
NSLayoutConstraint.activate([
label.leadingAnchor.constraint(equalTo: self.bobaEmoji.trailingAnchor),
label.firstBaselineAnchor.constraint(equalTo: self.bobaEmoji.firstBaselineAnchor)
])
})
var textLabel = UILabel()
}
其次,它允许您轻松地将自定义类的属性“注入”到子视图中,而无需任何额外的函数
final class ErrorLabel: Superview {
let error: Error
// parentheses around (label, self) are required by Swift
@Subview(.pin, { (label, self) in
label.text = self.error.localizedDescription
label.textAlignment = .center
label.numberOfLines = 0
})
private var errorLabel = UILabel()
}
如您所见,errorLabel
可以简单地直接从 self
获取 error
属性。 同样,没有魔法!
Subview 的另一个非魔法特性是使用带有 self
的动态块创建子视图
final class SymbolLabel: Superview {
let systemSymbolName: String
// parentheses around (self) are required by Swift
@Subview(.pin)
var systemImageView = { (self) in
let image = UIImage(systemName: self.systemSymbolName)
return UIImageView(image: image)
}
}
let volleyball = SymbolLabel(systemSymbolName: "volleyball.fill")
当然,没有堆栈视图就没有现代的 UIKit 代码。 Subviews 使用 @ArrangedSubview(of:)
属性包装器提供对堆栈视图的支持
final class UkraineFlag: Superview {
// UIStackView should be defined first
@Subview(.pin, {
$0.axis = .vertical
$0.distribution = .fillEqually
})
var stack = UIStackView()
// Make sure you use @ArrangedSubview, not @Subview!
@ArrangedSubview(of: \.stack, {
$0.backgroundColor = .systemBlue
})
var topStripe = UIView()
@ArrangedSubview(of: \.stack, {
$0.backgroundColor = .systemYellow
})
var bottomStripe = UIView()
}
警告
确保您将@ArrangedSubview
与堆栈视图一起使用,而不是@Subview
!
@ArrangedSubview
支持快速布局选项、配置块和动态创建块,与 @Subview
相同
final class FlaggedLogo: Superview {
let title: String
@Subview(.pin, {
$0.axis = .horizontal
$0.alignment = .center
$0.spacing = 12
})
var mainStack = UIStackView()
@ArrangedSubview(of: \.mainStack, [.height(40), .aspectRatioSquare])
var flag = UkraineFlag()
@ArrangedSubview(of: \.mainStack, { (label, self) in
label.text = self.title
label.font = .systemFont(ofSize: 36, weight: .heavy, width: .condensed)
})
var text = UILabel()
}
let bravery = FlaggedLogo(title: "BRAVERY")
警告
此功能是实验性的,可能会在未来的版本中更改或删除(会提前通知)。如果您想帮助 Subviews,请尝试一下,我很期待看到您的反馈!
如果您不想使用 @ArrangedSubview
,您可以选择实验性的 _HorizontalStack
/ _VerticalStack
函数,该函数采用 self
指定的结果构建器
final class PeruFlag: Superview {
let leftStripe = RectangleView(color: .red)
let centerStripe = RectangleView(color: .white)
let rightStripe = RectangleView(color: .red)
// parentheses around (self) are required by Swift
@Subview(.pin, {
$0.distribution = .fillEqually
})
var flagStack = _HorizontalStack { (self) in
self.leftStripe
self.centerStripe
self.rightStripe
}
}
不要求对将包含在堆栈中的视图使用 @Subview
,但是如果您想使用任何 @Subview
提供的功能,例如动态创建块,您可以随意使用
final class TwoColorVerticalFlag: Superview {
let topColor: UIColor
let bottomColor: UIColor
@Subview
var topStripe = { RectangleView(color: $0.topColor) }
// ^ you can use `$0` instead of `(self)` for shorter code
@Subview
var bottomStripe = { RectangleView(color: $0.bottomColor) }
@Subview(.pin, {
$0.distribution = .fillEqually
})
var flagStack = _VerticalStack { (self) in
self.topStripe
self.bottomStripe
}
}
let ukraineFlag = TwoColorVerticalFlag(topColor: .systemBlue, bottomColor: .systemYellow)
let polandFlag = TwoColorVerticalFlag(topColor: .white, bottomColor: .red)
或者您可以通过在堆栈块本身内定义子视图来非常接近 SwiftUI
final class TwoColorVerticalFlag: Superview {
let topColor: UIColor
let bottomColor: UIColor
@Subview(.pin, {
$0.distribution = .fillEqually
})
var flagStack = _VerticalStack { (self) in
RectangleView(color: self.topColor)
RectangleView(color: self.bottomColor)
}
}
可能性是无限的。 鼓励进行实验。
如果由于任何原因您不想为所有自定义视图采用新的基类 (Superview
),您也可以将 @Subview
与自定义 UIView
子类一起使用。 为此,您有两种选择
AddsSubview
协议的遵循super.init
之后调用 resolveAllEnclosedProperties()
函数final class HelloWorld: UIView, AddsSubviews {
@Subview(.pin, {
$0.text = "Hello, world"
})
var label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
// this is the important part:
self.resolveAllEnclosedProperties()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
AddsSubview
协议的遵循Subviews
函数并列出所有 @Subview
属性final class HelloWorld: UIView, AddsSubviews {
@Subview(.pin, {
$0.backgroundColor = .black
})
var background = UIView()
@Subview(of: \.background, .alignCenter, {
$0.text = "Hello, world"
$0.textColor = .white
})
var label = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
// this is the important part:
Subviews {
$background // make sure you're using the `$` prefix
$label
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
使用您自己的 UIViewController
子类的方法是相同的
AddsChildrenViewControllers
和/或 AddsSubviews
viewDidLoad()
中,在 super.viewDidLoad()
之后和任何自定义代码之前,调用 resolveAllEnclosedProperties()
(半自动模式)或 Children
/ Subviews
(手动模式)// Semi-automatic mode
final class EmptyStateVC: UIViewController, AddsChildrenViewControllers, AddsSubviews {
@Child(.safeAreaPin, {
$0.view.backgroundColor = .systemGray
})
var backgroundVC = UIViewController()
@Subview(.pin)
var emptyStateView = EmptyStateView()
override func viewDidLoad() {
super.viewDidLoad()
// important part:
resolveAllEnclosedProperties()
// your code here
}
}
// Manual mode
final class EmptyStateVC: UIViewController, AddsChildrenViewControllers, AddsSubviews {
@Child(.safeAreaPin, {
$0.view.backgroundColor = .systemGray
})
var backgroundVC = UIViewController()
@Subview(.pin)
var emptyStateView = EmptyStateView()
override func viewDidLoad() {
super.viewDidLoad()
// important part:
Children {
$backgroundVC // don't forget the "$"!
}
Subviews {
$emptyStateView
}
// your code here
}
}
Superview
/ ParentViewController
基类@Subview
/ @Child
@Subview
/ @Child
(of:
)@Subview
/ @Child
:快速布局选项 (.pin
等)@Subview
/ @Child
:基本配置块@Subview
/ @Child
:带有 self
的动态配置块@Subview
/ @Child
:带有 self
的动态创建块@ArrangedSubview
与 UIStackView
一起使用_VerticalStack
/ _HorizontalStack
Superview
/ ParentViewController
的情况下使用 @Subview
/ @Child
@Subview
/ @Child
:替换行为 (onReplace:
)@Subview
/ @Child
:延迟初始化Enclosed
类型ViewLayoutOption
(快速布局选项)答:我喜欢 UIKit,并且仍然每天都乐于使用它。 以其纯粹的形式,UIKit 使用起来可能相当笨拙,并且通常需要大量的样板代码。 Subviews 最初是一个实验,旨在看看我可以利用 Swift 类型系统到什么程度,才能使 UIKit 使用起来更有趣。 在我的副项目中使用了 Subviews 将近一年之后,我现在无法想象没有它的生活。
答:也许吧。 我并没有强迫你使用它。 也许有人会像我一样喜欢使用 Subviews。 其他人可能会发现其代码中使用的一些技术对他们自己的工作有趣且有价值。 任何代码都应该被分享。
答:谢谢! 请随时与我分享您的经验或任何反馈。 如果您对项目的未来方向有任何想法,请联系我(联系方式:@dreymonde)
答:主要使用两个很酷的 Swift 功能
我正在考虑写一系列文章来解释 Subviews 的内部运作。 敬请关注。
答:是的,显然任何使用反射的东西的性能都会比没有反射的东西差。 但是,在自定义视图子类的大多数用例中,性能下降将是微不足道的。 您可能会惊讶地发现 SwiftUI 大量使用了反射(尽管是更高级的反射),而且似乎并没有困扰任何人。
如果您担心潜在的性能问题,您可以在手动模式下使用 Subviews,请参阅 在不继承 Superview
的情况下使用 @Subview
。
答:我有一些副项目已经在生产环境中运行这段代码一年多了。 所以从我的角度来看,是的,它适用于生产环境。
从技术上讲,它使用了一些带有“下划线前缀”的 Swift API,这些 API 将来可能会更改 - 但它也是 Apple 自己的代码使用的相同 API。 它们不会消失。
也就是说,Subviews 本身尚未处于“1.0”状态。 更新可能会发生重大更改。 它还提供了一些带有下划线前缀的公共 API - 这些是实验性的,可能会更改或删除。
所有与反射相关的代码都是稳定和官方的。 没有什么不正当的行为。
答:Subviews *就是* 100% UIKit & Auto Layout。 您可以将您自己的 UIKit 代码放在任何地方。
答:尚未。 如果您对此感兴趣,请告诉我。
答:向乌克兰捐款。 谢谢