Button
無疑是swifttui中最受歡迎的元素之一巍沙,它也非常特別浮禾,因為它是唯一具有兩種不同風(fēng)格協(xié)議的組件:ButtonStyle
和PrimitiveButtonStyle
。
在本文中镀赌,讓我們探索關(guān)于按鈕樣式的所有知識摘能,以及更多內(nèi)容脂凶。
開始
SwiftUI有三種內(nèi)置樣式:DefaultButtonStyle
、BorderlessButtonStyle
和PlainButtonStyle
扫腺。
當(dāng)聲明一個簡單的按鈕時岗照,應(yīng)用DefaultButtonStyle
:
Button("Simple button") {
// button tapped
...
}
DefaultButtonStyle
本身并不是一種風(fēng)格:它是我們讓swiftUI為我們選擇風(fēng)格的方式(基于上下文、平臺笆环、父視圖等等)攒至。
實際的默認(rèn)樣式是BorderlessButtonStyle
,它會在按鈕頂部應(yīng)用藍色調(diào)躁劣,如果我們是在iOS14上應(yīng)用會有一些點擊迫吐、聚焦等視覺效果。
以下三個聲明是等價的:
Button("Simple button") {
...
}
Button("Simple button") {
...
}
.buttonStyle(DefaultButtonStyle())
Button("Simple button") {
...
}
.buttonStyle(BorderlessButtonStyle())
在iOS13中账忘,(藍色)色調(diào)應(yīng)用于按鈕
label
中聲明的圖像志膀,因此我們需要添加渲染修飾符(例如:Image("Image").renderingmode (.original))
或在圖像資產(chǎn)目錄中聲明正確的渲染。
從ios14只有模板圖像將被默認(rèn)著色鳖擒。
最后溉浙,SwiftUI提供了PlainButtonStyle
,它不帶顏色地顯示按鈕標(biāo)簽蒋荚,但仍然在不同的狀態(tài)下應(yīng)用視覺效果:
這些都是swiftUI在iOS中提供給我們的樣式:我們可以用ButtonStyle
和PrimitiveButtonStyle
創(chuàng)建新的樣式戳稽,讓我們從ButtonStyle
開始。
ButtonStyle
文檔建議我們在自己聲明按鈕外觀時使用ButtonStyle
期升,但按鈕交互的行為與任何其他標(biāo)準(zhǔn)按鈕一樣(也就是說惊奇,它的動作在點擊時被觸發(fā))。
public protocol ButtonStyle {
associatedtype Body: View
func makeBody(configuration: Self.Configuration) -> Self.Body
typealias Configuration = ButtonStyleConfiguration
}
ButtonStyle
的唯一要求是從makeBody(configuration:)
返回一個視圖吓妆,該函數(shù)接受一個ButtonStyleConfiguration
實例:
public struct ButtonStyleConfiguration {
public let label: ButtonStyleConfiguration.Label
public let isPressed: Bool
}
這個配置有兩個屬性:
-
label
是按鈕label
,例如赊时,如果我們的按鈕是Button(action: {}, label: { Text("Hello world") })
,那么Text("Hello world")
就是我們的label
行拢。 -
isPressed
是按鈕的當(dāng)前狀態(tài)祖秒,可以在ButtonStyle
的makeBody(configuration:)
中用于視覺效果
讓我們來定義幾個例子:
帶有圓角的ButtonStyle
:
struct RoundedRectangleButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
HStack {
Spacer()
configuration.label.foregroundColor(.black)
Spacer()
}
.padding()
.background(Color.yellow.cornerRadius(8))
.scaleEffect(configuration.isPressed ? 0.95 : 1)
}
}
帶有文字陰影的ButtonStyle
:
struct ShadowButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.shadow(
color: configuration.isPressed ? Color.blue : Color.black,
radius: 4, x: 0, y: 5
)
}
}
注意,當(dāng)點擊時,這些新按鈕沒有默認(rèn)效果:現(xiàn)在是時候在輸出按鈕中添加這些效果了竭缝。
這就是ButtonStyle
的全部內(nèi)容,它可以讓我們自定義任何按鈕的外觀房维,主要優(yōu)點是:
- 可以將相同的樣式應(yīng)用到多個按鈕而不需要重復(fù)代碼
- 接受
isPressed
事件 - 保持標(biāo)準(zhǔn)的交互/行為
應(yīng)用和組合多種樣式
Button
沒有接受ButtonStyleConfiguration
實例的初始化方法,當(dāng)組合多個樣式時抬纸,事情就變得復(fù)雜了咙俩。
根據(jù)我們當(dāng)前的聲明,應(yīng)用多個ButtonStyles
沒有效果湿故,只有最接近的樣式將被使用(其他樣式的makeBody(configuration:)
甚至不會被調(diào)用):
// 只有RoundedRectangleButtonStyle聲效了
Button("Rounded rectangle button style") {
// button tapped
...
}
.buttonStyle(RoundedRectangleButtonStyle())
.buttonStyle(ShadowButtonStyle())
.buttonStyle(BorderlessButtonStyle())
.buttonStyle(DefaultButtonStyle())
一個“解決辦法”是在ButtonStyle
makeBody(configuration:)
函數(shù)中返回一個新的Button
阿趁,例如,我們可以如下所示更新RoundedRectangleButtonStyle
:
struct RoundedRectangleButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
Button(action: {}, label: {
HStack {
Spacer()
configuration.label.foregroundColor(.black)
Spacer()
}
})
// 使所有的輕擊轉(zhuǎn)到原來的按鈕
.allowsHitTesting(false)
.padding()
.background(Color.yellow.cornerRadius(8))
.scaleEffect(configuration.isPressed ? 0.95 : 1)
}
}
有了這個新的定義坛猪,前面的例子就可以工作了:
Button("Rounded rectangle + shadow button style") {
// button tapped
...
}
.buttonStyle(RoundedRectangleButtonStyle())
.buttonStyle(ShadowButtonStyle())
這里有個缺點是脖阵,樣式僅可應(yīng)用于虛構(gòu)的、不可點擊的按鈕墅茉,因此不接收任何isPressed
事件命黔。
至少就目前而言,解決這些限制的一個簡單方法是創(chuàng)建并使用一種新的風(fēng)格就斤,它可以結(jié)合所需的效果悍募,例如:
struct RoundedRectangleWithShadowedLabelButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
HStack {
Spacer()
configuration.label.foregroundColor(.black)
.shadow(
color: configuration.isPressed ? Color.red : Color.black,
radius: 4, x: 0, y: 5
)
Spacer()
}
.padding()
.background(Color.yellow.cornerRadius(8))
.scaleEffect(configuration.isPressed ? 0.95 : 1)
}
}
我們可以這樣使用:
Button("Rounded rectangle + shadow button style") {
// button tapped
...
}
.buttonStyle(RoundedRectangleWithShadowedLabelButtonStyle())
PrimitiveButtonStyle
ButtonStyle
是關(guān)于自定義外觀和保持標(biāo)準(zhǔn)交互行為的,而PrimitiveButtonStyle
則允許我們自定義兩者洋机,這意味著由我們來定義按鈕外觀坠宴,并決定何時以及如何觸發(fā)按鈕動作。
PrimitiveButtonStyle
的定義幾乎與ButtonStyle
相同:
public protocol PrimitiveButtonStyle {
associatedtype Body : View
func makeBody(configuration: Self.Configuration) -> Self.Body
typealias Configuration = PrimitiveButtonStyleConfiguration
}
唯一的區(qū)別在于makeBody(configuration:)
參數(shù)槐秧,它現(xiàn)在是PrimitiveButtonStyleConfiguration
類型:
public struct PrimitiveButtonStyleConfiguration {
public let label: PrimitiveButtonStyleConfiguration.Label
public func trigger()
}
這個配置同樣帶有buttonlabel
屬性啄踊,但是isPressed
現(xiàn)在被trigger()
函數(shù)所取代:
調(diào)用trigger()
是我們調(diào)用按鈕操作的方式,現(xiàn)在由我們來定義正確的時間刁标。
例如颠通,如果我們希望一個按鈕只在雙擊時觸發(fā),我們可以定義以下樣式:
struct DoubleTapOnlyStyle: PrimitiveButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.onTapGesture(count: 2, perform: configuration.trigger)
}
}
我們可以像使用其他樣式一樣使用:
Button("Double tap me") {
// button double tapped
...
}
.buttonStyle(DoubleTapOnlyStyle())
應(yīng)用和組合多種基本樣式
與ButtonStyleConfiguration
不同膀懈,Button
有一個接受PrimitiveButtonStyleConfiguration
實例的初始化器顿锰,允許我們在同一個按鈕上組合/應(yīng)用多個基本樣式。
例如启搂,考慮以下樣式:
// 雙擊按鈕動作就會觸發(fā)
struct DoubleTapStyle: PrimitiveButtonStyle {
func makeBody(configuration: Configuration) -> some View {
Button(configuration) // <- Button instead of configuration.label
.onTapGesture(count: 2, perform: configuration.trigger)
}
}
// 點擊時觸發(fā)(即使在按鈕外終止)
struct SwipeButtonStyle: PrimitiveButtonStyle {
func makeBody(configuration: Configuration) -> some View {
Button(configuration)
.gesture(
DragGesture()
.onEnded { _ in
configuration.trigger()
}
)
}
}
當(dāng)每種樣式返回一個按鈕時硼控,它們可以組合并一起工作,沒有問題:
Button(
"Double tap or swipe",
action: {
// handle action here
...
}
)
.buttonStyle(DoubleTapStyle())
.buttonStyle(SwipeButtonStyle())
這種方法有一個小小的副作用:Button(configuration)
帶有默認(rèn)的按鈕交互和樣式胳赌,幸運的是我們可以通過定義另一種“plain”樣式來刪除這兩者牢撼。
struct PlainNoTapStyle: PrimitiveButtonStyle {
func makeBody(configuration: Configuration) -> some View {
Button(configuration)
.buttonStyle(PlainButtonStyle()) // 移除默認(rèn)樣式
.allowsHitTesting(false) // 取消事件觸發(fā)
.contentShape(Rectangle()) // 替換樣式及交互
}
}
如果我們現(xiàn)在把這個樣式添加到我們的按鈕定義中,我們只需要雙擊和滑動就可以真正地使它工作:
Button(
"Double tap or swipe",
action: {
// handle action here
...
}
)
.buttonStyle(DoubleTapStyle())
.buttonStyle(SwipeButtonStyle())
.buttonStyle(PlainNoTapStyle())
然而疑苫,我們可能希望大多數(shù)按鈕都能保持單點默認(rèn)交互熏版。
使用PrimitiveButtonStyle和ButtonStyle
我們已經(jīng)介紹了如何將每個ButtonStyle
對以前樣式的覆蓋纷责,而PrimitiveButtonStyle
允許我們組合多個樣式(在正確定義的情況下),那么將這兩種樣式結(jié)合起來呢?
我們可以同時應(yīng)用ButtonStyle
和一個或多個PrimitiveButtonStyle
撼短,例如:
Button(
"Primitive + button style",
action: {
// handle action here
...
}
)
// 即使把手指從按鈕上拖出來也能觸發(fā)按鈕事件
.buttonStyle(SwipeButtonStyle())
.buttonStyle(RoundedRectangleButtonStyle())
在這些情況下再膳,最后聲明ButtonStyle
(上面提到的RoundedRectangleButtonStyle
)是很重要的,否則它也會刪除原始ButtonStyle
曲横。
我們的ButtonStyle
將只接收標(biāo)準(zhǔn)點擊手勢上的isPressed
事件,由于PrimitiveButtonStyle
它不知道按鈕動作何時被觸發(fā)喂柒。我們有責(zé)任在需要的時候為這些樣式定義任何視覺效果。
總結(jié)
Button
是SwiftUI組件禾嫉,具有最簡單的交互:點擊按鈕觸發(fā)它們灾杰。
在這篇文章中,我們已經(jīng)看到了如何將任何按鈕變成具有完全不同外觀和手勢的更高級元素:大多數(shù)時候我們不需要超越自定義ButtonStyle
熙参,但是在需要的時候知道有更強大的工具總是好的吭露。