SwiftUI:Button styles

Button無疑是swifttui中最受歡迎的元素之一巍沙,它也非常特別浮禾,因為它是唯一具有兩種不同風(fēng)格協(xié)議的組件:ButtonStylePrimitiveButtonStyle
在本文中镀赌,讓我們探索關(guān)于按鈕樣式的所有知識摘能,以及更多內(nèi)容脂凶。

開始

SwiftUI有三種內(nèi)置樣式:DefaultButtonStyleBorderlessButtonStylePlainButtonStyle扫腺。
當(dāng)聲明一個簡單的按鈕時岗照,應(yīng)用DefaultButtonStyle:

Button("Simple button") { 
  // button tapped
  ...
}
simplebutton.gif

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)用視覺效果:

plainbutton.gif

這些都是swiftUI在iOS中提供給我們的樣式:我們可以用ButtonStylePrimitiveButtonStyle創(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)祖秒,可以在ButtonStylemakeBody(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)
  }
}
roundedRectangle.gif

帶有文字陰影的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
      )
  }
}
shadow.gif

注意,當(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())
roundedRectangle.gif

一個“解決辦法”是在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())
roundedShadow.gif

這里有個缺點是脖阵,樣式僅可應(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())
roundedShadow2.gif

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())
gest.gif

然而疑苫,我們可能希望大多數(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())
combo.gif

在這些情況下再膳,最后聲明ButtonStyle(上面提到的RoundedRectangleButtonStyle)是很重要的,否則它也會刪除原始ButtonStyle曲横。

我們的ButtonStyle將只接收標(biāo)準(zhǔn)點擊手勢上的isPressed事件,由于PrimitiveButtonStyle它不知道按鈕動作何時被觸發(fā)喂柒。我們有責(zé)任在需要的時候為這些樣式定義任何視覺效果。

總結(jié)

Button是SwiftUI組件禾嫉,具有最簡單的交互:點擊按鈕觸發(fā)它們灾杰。
在這篇文章中,我們已經(jīng)看到了如何將任何按鈕變成具有完全不同外觀和手勢的更高級元素:大多數(shù)時候我們不需要超越自定義ButtonStyle熙参,但是在需要的時候知道有更強大的工具總是好的吭露。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市尊惰,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌泥兰,老刑警劉巖弄屡,帶你破解...
    沈念sama閱讀 219,110評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異鞋诗,居然都是意外死亡膀捷,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,443評論 3 395
  • 文/潘曉璐 我一進店門削彬,熙熙樓的掌柜王于貴愁眉苦臉地迎上來全庸,“玉大人,你說我怎么就攤上這事融痛『” “怎么了?”我有些...
    開封第一講書人閱讀 165,474評論 0 356
  • 文/不壞的土叔 我叫張陵雁刷,是天一觀的道長覆劈。 經(jīng)常有香客問我,道長沛励,這世上最難降的妖魔是什么责语? 我笑而不...
    開封第一講書人閱讀 58,881評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮目派,結(jié)果婚禮上坤候,老公的妹妹穿的比我還像新娘。我一直安慰自己企蹭,他們只是感情好白筹,可當(dāng)我...
    茶點故事閱讀 67,902評論 6 392
  • 文/花漫 我一把揭開白布智末。 她就那樣靜靜地躺著,像睡著了一般遍蟋。 火紅的嫁衣襯著肌膚如雪吹害。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,698評論 1 305
  • 那天虚青,我揣著相機與錄音它呀,去河邊找鬼。 笑死棒厘,一個胖子當(dāng)著我的面吹牛纵穿,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播奢人,決...
    沈念sama閱讀 40,418評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼谓媒,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了何乎?” 一聲冷哼從身側(cè)響起句惯,我...
    開封第一講書人閱讀 39,332評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎支救,沒想到半個月后抢野,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,796評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡各墨,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,968評論 3 337
  • 正文 我和宋清朗相戀三年指孤,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片贬堵。...
    茶點故事閱讀 40,110評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡恃轩,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出黎做,到底是詐尸還是另有隱情叉跛,我是刑警寧澤,帶...
    沈念sama閱讀 35,792評論 5 346
  • 正文 年R本政府宣布引几,位于F島的核電站昧互,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏伟桅。R本人自食惡果不足惜敞掘,卻給世界環(huán)境...
    茶點故事閱讀 41,455評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望楣铁。 院中可真熱鬧玖雁,春花似錦、人聲如沸盖腕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,003評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至劲厌,卻和暖如春膛薛,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背补鼻。 一陣腳步聲響...
    開封第一講書人閱讀 33,130評論 1 272
  • 我被黑心中介騙來泰國打工哄啄, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人风范。 一個月前我還...
    沈念sama閱讀 48,348評論 3 373
  • 正文 我出身青樓咨跌,卻偏偏與公主長得像,于是被迫代替她去往敵國和親硼婿。 傳聞我的和親對象是個殘疾皇子锌半,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,047評論 2 355

推薦閱讀更多精彩內(nèi)容