由于SwiftUI的可組合特性,我們一開始可能想要做的一件事就是定義和使用一個設(shè)計系統(tǒng):一旦我們有了一個設(shè)計系統(tǒng)鳖宾,創(chuàng)建視圖就變成了從系統(tǒng)中選擇正確的元素并將它們放置到屏幕上的問題篙梢。
在本文中顷帖,讓我們看看如何開始構(gòu)建設(shè)計系統(tǒng)的核心組件之一:TextField。
開始
設(shè)計團隊給app的text field設(shè)計了兩種不同的外觀,一個是default
默認狀態(tài)贬墩,另一個是error
告訴用戶出了問題榴嗅。
除了外觀之外,所有文本字段都有相同的組件:title
標題陶舞、placeholder
占位符和border
邊框嗽测。
兩種TextField的外觀:
default
和error
。
有了這些知識肿孵,我們繼續(xù)構(gòu)建我們自己的FSTextField
:
struct FSTextField: View {
var title: LocalizedStringKey
var placeholder: LocalizedStringKey = ""
@Binding var text: String
var appearance: Appearance = .default
enum Appearance {
case `default`
case error
}
var body: some View {
VStack {
HStack {
Text(title)
.bold()
Spacer()
}
TextField(
placeholder,
text: $text
)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(
RoundedRectangle(cornerRadius: 8, style: .continuous)
.strokeBorder(borderColor)
)
}
}
var borderColor: Color {
switch appearance {
case .default:
return .green
case .error:
return .red
}
}
}
FSTextField
被定義為一個上面有一個標題(一個Text
)的VStack唠粥,底部有一個SwiftUI的TextField
:這個聲明是清晰的,覆蓋了所有已知的情況颁井。
我們對FSTextField
很滿意:我們添加了幾個預(yù)覽.
一周之后
一周之后,我們發(fā)現(xiàn)設(shè)計做了兩種新的變化蠢护,第一個是在右上角顯示一個字形雅宾,與標題垂直對齊,另一個是在同一點顯示一條消息:
我們定義了兩個新視圖,FSGlyphTextField
和 FSMessageTextField
,代碼設(shè)計如下:
struct FSGlyphTextField: View {
var title: LocalizedStringKey
var symbolName: String
var systemColor: Color = Color(.label)
var placeholder: LocalizedStringKey = ""
@Binding var text: String
var appearance: FSTextField.Appearance = .default
var body: some View {
VStack {
HStack {
Text(title)
.bold()
Spacer()
Image(systemName: symbolName)
.foregroundColor(systemColor)
}
TextField(
...
)
}
}
var borderColor: Color {
...
}
}
struct FSMessageTextField: View {
var title: LocalizedStringKey
var message: LocalizedStringKey
@Binding var text: String
var appearance: FSTextField.Appearance = .default
var body: some View {
VStack {
HStack {
Text(title)
.bold()
Spacer()
Text(message)
.font(.caption)
}
TextField(
...
)
}
}
var borderColor: Color {
...
}
}
我們的設(shè)計系統(tǒng)現(xiàn)在定義了三個TextField
葵硕,而不是一個眉抬,如何做的更好呢?
又過了一周
設(shè)計師又修改了TextField
的展示方式懈凹,第一個沒有標題蜀变,而另一個有通常的標題并在后面的角落有一個按鈕:
我們可以再定義兩個文本字段視圖(像FSPlainTextField
和FSButtonTextField
一樣),然而介评,為每個變化創(chuàng)建新的視圖違背了設(shè)計的目的库北,當(dāng)設(shè)計發(fā)生變化,而我們必須更新標題字體或邊框顏色時们陆,會發(fā)生什么?
我們定義的TextField
越多寒瓦,就越難管理每個組件。
通用的TextField核心組件
在當(dāng)前的方法中坪仇,我們已經(jīng)利用了SwiftUI的可組合性杂腰,因為我們在構(gòu)建屏幕時使用了所有這些變體,但是為了更好椅文,在我們的TextField
定義中也使用可組合性喂很。
首先,看看當(dāng)前的變化皆刺,我們發(fā)現(xiàn)有一個常數(shù):text field本身少辣。讓我們從上面的定義中進一步封裝:
struct _FSTextField: View {
var placeholder: LocalizedStringKey = ""
@Binding var text: String
var borderColor: Color
var body: some View {
TextField(
placeholder,
text: $text
)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(
RoundedRectangle(cornerRadius: 8, style: .continuous)
.strokeBorder(borderColor)
)
}
}
我們的兩個
_FSTextField
變體。
_FSTextField
是SwiftUI的TextField
的包裝器羡蛾,我們的應(yīng)用程序設(shè)計應(yīng)用于它,我們用下劃線“_”前綴(_FSTextField
)定義這個視圖毒坛,以便清楚地表明不應(yīng)該直接使用這個視圖,而是其他視圖的實現(xiàn)細節(jié)。
如果我們用_FSTextField
替換之前的TextField
定義煎殷,這已經(jīng)有幫助了:
將來屯伞,當(dāng)我們想要更新文本字段的角半徑時,我們只需要在_FSTextField
內(nèi)更改它豪直,所有其他視圖將自動繼承更改劣摇。
通用的TextField可組合視圖
看看我們的text fields
變體,我們可以將它們分為兩類:
- 在
_FSTextField
之上有一些內(nèi)容的視圖(例如標題和符號) - 只有普通的
_FSTextField
和其他字段的視圖
讓我們定義一個涵蓋這兩種變體的新通用視圖FSTextField
:
struct FSTextField<TopContent: View>: View {
var placeholder: LocalizedStringKey = ""
@Binding var text: String
var appearance: Appearance = .default
var topContent: TopContent
init(
placeholder: LocalizedStringKey = "",
text: Binding<String>,
appearance: Appearance = .default,
@ViewBuilder topContent: () -> TopContent
) {
self.placeholder = placeholder
self._text = text
self.appearance = appearance
self.topContent = topContent()
}
enum Appearance {
case `default`
case error
}
var body: some View {
VStack {
topContent
_FSTextField(
placeholder: placeholder,
text: $text,
borderColor: borderColor
)
}
}
var borderColor: Color {
switch appearance {
case .default:
return .green
case .error:
return .red
}
}
}
FSTextField
是一個VStack
,它包含一個通用的頂部視圖TopContent
和我們的_FSTextField
在底部弓乙。
多虧了這個新定義末融,我們可以把任何視圖放在_FSTextField
之上,比如標簽Label
呢?
FSTextField(placeholder: "Placeholder", text: $text) {
Label("Label Title", systemImage: "star.fill")
}
最后暇韧,我們需要注意在_FSTextField
上面沒有內(nèi)容的視圖變化勾习,我們?nèi)绾谓鉀Q這個問題?
由于VStacks
忽略了EmptyViews
,如果我們想只顯示_FSTextField
而不顯示其他內(nèi)容懈玻,我們可以傳遞一個EmptyView
實例作為TopContent
:
FSTextField(
placeholder: "Placeholder",
text: $myText
) {
EmptyView()
}
它工作的原因:
- 一個帶有
EmptyView
和_FSTextField
的VStack
(視覺上)等同于一個只有_FSTextField
的VStack
- 任何只包含一個元素的堆棧(視覺上)都等價于只包含元素本身
因此:
var body: some View {
VStack {
EmptyView()
_FSTextField(...)
}
}
是一樣的:
var body: some View {
_FSTextField(...)
}
我們正在構(gòu)建這個設(shè)計系統(tǒng)巧婶,我們知道這些技巧/細節(jié),但是涂乌,我們不能要求我們的開發(fā)人員也有這樣的深層次的知識:為了讓他們的工作更簡單艺栈,我們可以創(chuàng)建一個FSTextField
擴展來隱藏這個VStack
+ EmptyView
組合。
extension FSTextField where TopContent == EmptyView {
init(
placeholder: LocalizedStringKey = "",
text: Binding<String>,
appearance: Appearance = .default
) {
self.placeholder = placeholder
self._text = text
self.appearance = appearance
self.topContent = EmptyView()
}
}
多虧了這個擴展湾盒,想只顯示文本字段的開發(fā)人員現(xiàn)在可以使用這個新的初始化器湿右,而不需要知道FSTextField
是如何實現(xiàn)的:
FSTextField(placeholder: "Placeholder", text: $myText)
通用TextField的初始化
所有其他文本字段都有某種TopContent
要顯示。
我們可以在這里停下來罚勾,每次都讓開發(fā)人員自己定義內(nèi)容,例如:
FSTextField(
placeholder: "Placeholder",
text: $myText
) {
HStack {
Text(title)
.bold()
Spacer()
}
}
然而毅人,由于所有這些變體都有一個帶有title-space-something
模式的TopContent
,我們可以用一個新的FSTextField
擴展:
extension FSTextField {
init<TopTrailingContent: View>(
title: LocalizedStringKey,
placeholder: LocalizedStringKey = "",
text: Binding<String>,
appearance: Appearance = .default,
@ViewBuilder topTrailingContent: () -> TopTrailingContent
) where TopContent == HStack<TupleView<(Text, Spacer, TopTrailingContent)>> {
self.placeholder = placeholder
self._text = text
self.appearance = appearance
self.topContent = {
HStack {
Text(title)
.bold()
Spacer()
topTrailingContent()
}
}()
}
}
這個新的初始化方法讓開發(fā)人員可以直接將標題文本作為初始化參數(shù)之一傳遞尖殃,然后有機會通過新的topTrailingContent
參數(shù)定義放置在頂部尾部角落的其他內(nèi)容堰塌。
例如,現(xiàn)在可以用以下代碼獲得我們的舊FSMessageTextField
的效果:
FSTextField(
title: "Title",
placeholder: "Placeholder",
text: $text, topTrailingContent: {
Text("Message")
.font(.caption)
})
如前所述分衫,如果我們的開發(fā)人員只想顯示一個_FSTextField
和一個標題,它們不需要知道它們可以傳遞一個EmptyView
實例作為topTrailingContent
參數(shù)场刑,因此最好創(chuàng)建一個新的擴展來處理這個場景:
extension FSTextField {
init(
title: LocalizedStringKey,
placeholder: LocalizedStringKey = "",
text: Binding<String>,
appearance: Appearance = .default
) where TopContent == HStack<TupleView<(Text, Spacer, EmptyView)>> {
self.init(
title: title,
placeholder: placeholder,
text: text,
appearance: appearance,
topTrailingContent: EmptyView.init
)
}
}
同樣,這是由于當(dāng)放置在堆棧中時EmptyView
會被忽略蚪战。
由于這個定義牵现,一個簡單的text field + title
組合(沒有頂部尾隨視圖)可以通過以下方式獲得:
FSTextField(title: "Title", placeholder: "Placeholder", text: $myText)
我們以前用新視圖定義的所有其他變量現(xiàn)在都可以通過FSTextField
直接獲得。
完整代碼如下:
struct _FSTextField: View {
var placeholder: LocalizedStringKey = ""
@Binding var text: String
var borderColor: Color
var body: some View {
TextField(
placeholder,
text: $text
)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(
RoundedRectangle(cornerRadius: 8, style: .continuous)
.strokeBorder(borderColor)
)
}
}
/* FSTextField */
struct FSTextField<TopContent: View>: View {
var placeholder: LocalizedStringKey = "Placeholder"
@Binding var text: String
var appearance: Appearance = .default
var topContent: TopContent
init(
placeholder: LocalizedStringKey = "",
text: Binding<String>,
appearance: Appearance = .default,
@ViewBuilder topContent: () -> TopContent
) {
self.placeholder = placeholder
self._text = text
self.appearance = appearance
self.topContent = topContent()
}
enum Appearance {
case `default`
case error
}
var body: some View {
VStack {
topContent
_FSTextField(
placeholder: placeholder,
text: $text,
borderColor: borderColor
)
}
}
var borderColor: Color {
switch appearance {
case .default:
return .green
case .error:
return .red
}
}
}
extension FSTextField where TopContent == EmptyView {
init(
placeholder: LocalizedStringKey = "",
text: Binding<String>,
appearance: Appearance = .default
) {
self.placeholder = placeholder
self._text = text
self.appearance = appearance
self.topContent = EmptyView()
}
}
extension FSTextField {
init<TopTrailingContent: View>(
title: LocalizedStringKey,
placeholder: LocalizedStringKey = "",
text: Binding<String>,
appearance: Appearance = .default,
@ViewBuilder topTrailingContent: () -> TopTrailingContent
) where TopContent == HStack<TupleView<(Text, Spacer, TopTrailingContent)>> {
self.placeholder = placeholder
self._text = text
self.appearance = appearance
self.topContent = {
HStack {
Text(title)
.bold()
Spacer()
topTrailingContent()
}
}()
}
}
extension FSTextField {
init(
title: LocalizedStringKey,
placeholder: LocalizedStringKey = "",
text: Binding<String>,
appearance: Appearance = .default
) where TopContent == HStack<TupleView<(Text, Spacer, EmptyView)>> {
self.init(
title: title,
placeholder: placeholder,
text: text,
appearance: appearance,
topTrailingContent: EmptyView.init
)
}
}
使用如下:
import SwiftUI
struct ContentView: View {
var body: some View {
Group {
FSTextField(
placeholder: "Placeholder",
text: .constant("")
) {
Text("Title Centered")
.bold()
}
FSTextField(
placeholder: "Placeholder",
text: .constant("")
)
FSTextField(
title: "Title",
text: .constant(""),
topTrailingContent: {
Text("Message")
.font(.caption)
})
}
}
}
總結(jié)
多虧了Swift和SwiftUI邀桑,我們才得以構(gòu)建一個堅實瞎疼、靈活、直觀的設(shè)計系統(tǒng)壁畸,幫助我們以前所未有的速度構(gòu)建贼急、組合和更新整個屏幕茅茂。
SwiftUI在很多定義上都使用了同樣的方法:
-
Button
的init(_ titleKey: LocalizedStringKey, action: @escaping () -> Void)
就是init(action: @escaping () -> Void, label: () -> Label)
的快捷初始化方法 - 我們還學(xué)習(xí)到了SwiftUI是如何在不需要的時候隱藏可選綁定的