IOS開(kāi)發(fā)自定義Button的外觀(guān)和交互行為示例詳解
正文
通過(guò) Style 改變組件的外觀(guān)或行為是 SwiftUI 提供的一項(xiàng)非常強(qiáng)大的功能。本文將介紹如何通過(guò)創(chuàng)建符合 ButtonStyle 或 PrimitiveButtonStyle 協(xié)議的實(shí)現(xiàn),自定義 Button 的外觀(guān)以及交互行為。
可在 此處 獲取本文的范例代碼
定制 Button 的外觀(guān)
按鈕是 UI 設(shè)計(jì)中經(jīng)常會(huì)使用到的組件。相較于 UIKit ,SwiftUI 通過(guò) Button 視圖,讓開(kāi)發(fā)者以少量的代碼便可完成按鈕的創(chuàng)建工作。
Button(action: signIn) { Text("Sign In") }
多數(shù)情況下,開(kāi)發(fā)者通過(guò)為 Button 的 label
參數(shù)提供不同的視圖來(lái)定制按鈕的外觀(guān)。
struct RoundedAndShadowButton<V>:View where V:View { let label:V let action: () -> Void init(label: V, action: @escaping () -> Void) { self.label = label self.action = action } var body: some View { Button { action() } label: { label .foregroundColor(.white) .padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)) .background( RoundedRectangle(cornerRadius: 10) .foregroundColor(.blue) ) .compositingGroup() .shadow(radius: 5,x:0,y:3) .contentShape(Rectangle()) } .buttonStyle(.plain) } } let label = Label("Press Me", systemImage: "digitalcrown.horizontal.press.fill") RoundedAndShadowButton(label: label, action: { pressAction("button view") })
使用 ButtonStyle 定制交互動(dòng)畫(huà)
遺憾的是,上面的代碼無(wú)法修改按鈕在點(diǎn)擊后的按壓效果。幸好,SwiftUI 提供了 ButtonStyle 協(xié)議可以幫助我們定制交互動(dòng)畫(huà)。
public protocol ButtonStyle { @ViewBuilder func makeBody(configuration: Self.Configuration) -> Self.Body typealias Configuration = ButtonStyleConfiguration } public struct ButtonStyleConfiguration { public let role: ButtonRole? public let label: ButtonStyleConfiguration.Label public let isPressed: Bool }
ButtonStyle 協(xié)議的使用方式與 ViewModifier 十分類(lèi)似。通過(guò) ButtonStyleConfiguration
提供的信息,開(kāi)發(fā)者只需實(shí)現(xiàn) makeBody
方法,即可完成交互動(dòng)畫(huà)的定制工作。
- label:目標(biāo)按鈕的當(dāng)前視圖,通常對(duì)應(yīng)著 Button 視圖中的 label 參數(shù)內(nèi)容
- role:iOS 15 后新增的參數(shù),用于標(biāo)識(shí)按鈕的角色( 取消或具備破壞性)
- isPressed:當(dāng)前按鈕的按壓狀態(tài),該信息是多數(shù)人使用 ButtonStyle 的原動(dòng)力
struct RoundedAndShadowButtonStyle:ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label .foregroundColor(.white) .padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)) .background( RoundedRectangle(cornerRadius: 10) .foregroundColor(.blue) ) .compositingGroup() // 根據(jù) isPressing 來(lái)調(diào)整交互動(dòng)畫(huà) .shadow(radius:configuration.isPressed ? 0 : 5,x:0,y: configuration.isPressed ? 0 :3) .scaleEffect(configuration.isPressed ? 0.95 : 1) .animation(.spring(), value: configuration.isPressed) } } // 快捷引用 extension ButtonStyle where Self == RoundedAndShadowButtonStyle { static var roundedAndShadow:RoundedAndShadowButtonStyle { RoundedAndShadowButtonStyle() } }
通過(guò) buttonStyle 修飾器應(yīng)用于 Button 視圖
Button(action: { pressAction("rounded and shadow") }, label: { label }) .buttonStyle(.roundedAndShadow)
創(chuàng)建一個(gè)通用性好 ButtonStyle 實(shí)現(xiàn)需要考慮很多條件,例如:role、controlSize、動(dòng)態(tài)字體尺寸、色彩模式等等方面。同 ViewModifier 一樣,可以通過(guò)環(huán)境值獲取更多信息:
struct RoundedAndShadowProButtonStyle:ButtonStyle { @Environment(\.controlSize) var controlSize func makeBody(configuration: Configuration) -> some View { configuration.label .foregroundColor(.white) .padding(getPadding()) .font(getFontSize()) .background( RoundedRectangle(cornerRadius: 10) .foregroundColor( configuration.role == .destructive ? .red : .blue) ) .compositingGroup() .overlay( VStack { if configuration.isPressed { RoundedRectangle(cornerRadius: 10) .fill(Color.white.opacity(0.5)) .blendMode(.hue) } } ) .shadow(radius:configuration.isPressed ? 0 : 5,x:0,y: configuration.isPressed ? 0 :3) .scaleEffect(configuration.isPressed ? 0.95 : 1) .animation(.spring(), value: configuration.isPressed) } func getPadding() -> EdgeInsets { let unit:CGFloat = 4 switch controlSize { case .regular: return EdgeInsets(top: unit * 2, leading: unit * 4, bottom: unit * 2, trailing: unit * 4) case .large: return EdgeInsets(top: unit * 3, leading: unit * 5, bottom: unit * 3, trailing: unit * 5) case .mini: return EdgeInsets(top: unit / 2, leading: unit * 2, bottom: unit/2, trailing: unit * 2) case .small: return EdgeInsets(top: unit, leading: unit * 3, bottom: unit, trailing: unit * 3) @unknown default: fatalError() } } func getFontSize() -> Font { switch controlSize { case .regular: return .body case .large: return .title3 case .small: return .callout case .mini: return .caption2 @unknown default: fatalError() } } } extension ButtonStyle where Self == RoundedAndShadowProButtonStyle { static var roundedAndShadowPro:RoundedAndShadowProButtonStyle { RoundedAndShadowProButtonStyle() } } // 使用 HStack { Button(role: .destructive, action: { pressAction("rounded and shadow pro") }, label: { label }) .buttonStyle(.roundedAndShadowPro) .controlSize(.large) Button(action: { pressAction("rounded and shadow pro") }, label: { label }) .buttonStyle(.roundedAndShadowPro) .controlSize(.small) }
使用 PrimitiveButtonStyle 定制交互行為
在 SwiftUI 中,Button 默認(rèn)的交互行為是在松開(kāi)按鈕的同時(shí)執(zhí)行 Button 指定的操作。并且,在點(diǎn)擊按鈕后,只要手指( 鼠標(biāo) )不松開(kāi),無(wú)論移動(dòng)到哪里( 移動(dòng)到 Button 視圖之外 ),松開(kāi)后仍會(huì)執(zhí)行指定操作。
盡管 Button 的默認(rèn)手勢(shì)與 TapGestur
單擊操作類(lèi)似,但 Button 的手勢(shì)是一種不可撤銷(xiāo)的操作。而 TapGesture 在不松開(kāi)手指的情況下,如果移動(dòng)到可點(diǎn)擊區(qū)域外,SwiftUI 將不會(huì)調(diào)用 onEnded 閉包中的操作。
假如,我們想達(dá)成與 TapGesture 類(lèi)似的效果( 可撤銷(xiāo)按鈕 ),則可以通過(guò) SwiftUI 提供的另一個(gè)協(xié)議 PrimitiveButtonStyle 來(lái)實(shí)現(xiàn)。
public protocol PrimitiveButtonStyle { @ViewBuilder func makeBody(configuration: Self.Configuration) -> Self.Body typealias Configuration = PrimitiveButtonStyleConfiguration } public struct PrimitiveButtonStyleConfiguration { public let role: ButtonRole? public let label: PrimitiveButtonStyleConfiguration.Label public func trigger() }
PrimitiveButtonStyle 與 ButtonStyle 兩者之間最大的不同是,PrimitiveButtonStyle 要求開(kāi)發(fā)者必須通過(guò)自行完成交互操作邏輯,并在適當(dāng)?shù)臅r(shí)機(jī)調(diào)用 trigger 方法( 可以理解為 Button 的 action 參數(shù)對(duì)應(yīng)的閉包 )。
struct CancellableButtonStyle:PrimitiveButtonStyle { @GestureState var isPressing = false func makeBody(configuration: Configuration) -> some View { let drag = DragGesture(minimumDistance: 0) .updating($isPressing, body: {_,pressing,_ in if !pressing { pressing = true} }) configuration.label .foregroundColor(.white) .padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)) .background( RoundedRectangle(cornerRadius: 10) .foregroundColor( configuration.role == .destructive ? .red : .blue) ) .compositingGroup() .shadow(radius:isPressing ? 0 : 5,x:0,y: isPressing ? 0 :3) .scaleEffect(isPressing ? 0.95 : 1) .animation(.spring(), value: isPressing) // 獲取點(diǎn)擊狀態(tài) .gesture(drag) .simultaneousGesture(TapGesture().onEnded{ configuration.trigger() // 執(zhí)行 Button 指定的操作 }) } } extension PrimitiveButtonStyle where Self == CancellableButtonStyle { static var cancellable:CancellableButtonStyle { CancellableButtonStyle() } }
或許有人會(huì)說(shuō),既然上面的代碼可以通過(guò) DragGesture 模擬獲取到點(diǎn)擊狀態(tài),那么完全可以不使用 PrimitiveButtonStyle 實(shí)現(xiàn)同樣的效果。如此一來(lái)使用 Style 的優(yōu)勢(shì)在哪里呢?
- ButtonStyle 和 PrimitiveButtonStyle 是專(zhuān)門(mén)針對(duì)按鈕的樣式 API ,它們不僅可以應(yīng)用于 Button 視圖,也可以應(yīng)用于很多 SwiftUI 預(yù)置的系統(tǒng)按鈕功能之上,例如:EditButton、Share、Link、NavigationLink( 不在 List 中) 等。
keyboardShortcut
修飾器也只能應(yīng)用于 Button,視圖 + TapGesture 無(wú)法設(shè)定快捷鍵。
無(wú)論是雙擊、長(zhǎng)按、甚至通過(guò)體感觸發(fā),開(kāi)發(fā)者均可以通過(guò) PrimitiveButtonStyle 協(xié)議定制自己的按鈕交互邏輯。
系統(tǒng)預(yù)置的 Style
從 iOS 15 開(kāi)始,SwiftUI 在原有 PlainButtonStyle、DefaultButtonStyle 的基礎(chǔ)上,提供了更加豐富的預(yù)置 Style。
- PlainButtonStyle:不對(duì) Button 視圖添加任何修飾
- BorderlessButtonStyle:多數(shù)情況下的默認(rèn)樣式,在未指定文字顏色的情況下,將文字修改為強(qiáng)調(diào)色
- BorderedButtonStyle:為按鈕添加圓角矩形背景,使用 tint 顏色作為背景色
- BorderedProminentButtonStyle:為按鈕添加圓角矩形背景,背景顏色為系統(tǒng)強(qiáng)調(diào)色
其中,PlainButtonStyle 除了可以應(yīng)用于 Button 外,同時(shí)也會(huì)對(duì) List 以及 Form 的單元格行為造成影響。默認(rèn)情況下,即使單元格的視圖中包含了多個(gè)按鈕,SwiftUI 也只會(huì)將 List 的單元格視作一個(gè)按鈕( 點(diǎn)擊后同時(shí)調(diào)用所有按鈕的操作 )。通過(guò)為 List 設(shè)置 PlainButtonStyle 風(fēng)格,便可以調(diào)整這一行為,讓一個(gè)單元格中的多個(gè)按鈕可以被分別觸發(fā)。
List { HStack { Button("11"){print("1")} Button("22"){print("2")} } } .buttonStyle(.plain)
注意事項(xiàng)
- 同 ViewModifier 不同,ButtonStyle 并不支持串聯(lián),Button 只會(huì)采用最靠近的 Style
VStack { Button("11"){print("1")} // plain Button("22"){print("2")} // borderless .buttonStyle(.borderless) Button("33"){print("3")} // borderedProminent .buttonStyle(.borderedProminent) .buttonStyle(.borderless) } .buttonStyle(.plain)
- 某些按鈕樣式在不同的上下文中的行為和外觀(guān)會(huì)有較大差別,甚至不起作用。例如:無(wú)法為 List 中的 NavigationLink 設(shè)置樣式
- 在 Button 的 label 視圖或 ButtonStyle 實(shí)現(xiàn)中添加的手勢(shì)操作( 例如 TapGesture )將導(dǎo)致 Button 不再調(diào)用其指定的閉包操作,附加手勢(shì)需在 Button 之外添加( 例如下文的 simultaneousGesture 實(shí)現(xiàn) )
為按鈕添加 Trigger
在 SwiftUI 中,為了判斷某個(gè)按鈕是否被按下( 尤其是系統(tǒng)按鈕 ),我們通常會(huì)通過(guò)設(shè)置并行手勢(shì)來(lái)添加 trigger :
EditButton() .buttonStyle(.roundedAndShadowPro) .simultaneousGesture(TapGesture().onEnded{ print("pressed")}) // 設(shè)置并行手勢(shì) .withTitle("edit button with simultaneous trigger")
不過(guò),上述方法在 macOS 下不起作用 。通過(guò) Style ,我們可以在設(shè)置按鈕樣式時(shí)為其添加觸發(fā)器:
struct TriggerActionStyle:ButtonStyle { let trigger:() -> Void init(trigger: @escaping () -> Void) { self.trigger = trigger } func makeBody(configuration: Configuration) -> some View { configuration.label .foregroundColor(.white) .padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)) .background( RoundedRectangle(cornerRadius: 10) .foregroundColor(.blue) ) .compositingGroup() .shadow(radius:configuration.isPressed ? 0 : 5,x:0,y: configuration.isPressed ? 0 :3) .scaleEffect(configuration.isPressed ? 0.95 : 1) .animation(.spring(), value: configuration.isPressed) .onChange(of: configuration.isPressed){ isPressed in if !isPressed { trigger() } } } } extension ButtonStyle where Self == TriggerActionStyle { static func triggerAction(trigger perform:@escaping () -> Void) -> TriggerActionStyle { .init(trigger: perform) } }
當(dāng)然,用 PrimitiveButtonStyle 也一樣可以實(shí)現(xiàn):
struct TriggerButton2: PrimitiveButtonStyle { var trigger: () -> Void func makeBody(configuration: PrimitiveButtonStyle.Configuration) -> some View { MyButton(trigger: trigger, configuration: configuration) } struct MyButton: View { @State private var pressed = false var trigger: () -> Void let configuration: PrimitiveButtonStyle.Configuration var body: some View { return configuration.label .foregroundColor(.white) .padding(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)) .background( RoundedRectangle(cornerRadius: 10) .foregroundColor(.blue) ) .compositingGroup() .shadow(radius: pressed ? 0 : 5, x: 0, y: pressed ? 0 : 3) .scaleEffect(pressed ? 0.95 : 1) .animation(.spring(), value: pressed) .onLongPressGesture(minimumDuration: 2.5, maximumDistance: .infinity, pressing: { pressing in withAnimation(.easeInOut(duration: 0.3)) { self.pressed = pressing } if pressing { configuration.trigger() // 原來(lái)的 action trigger() // 新增的 action } else { print("release") } }, perform: {}) } } }
總結(jié)
盡管自定義 Style 的效果顯著,但遺憾的是,目前 SwiftUI 僅開(kāi)放了少數(shù)的組件樣式協(xié)議供開(kāi)發(fā)者自定義使用,并且提供的屬性也很有限。希望在未來(lái)的版本中,SwiftUI 可以為開(kāi)發(fā)者提供更加強(qiáng)大的自定義組件能力。
以上就是IOS開(kāi)發(fā)自定義Button的外觀(guān)和交互行為示例詳解的詳細(xì)內(nèi)容,更多關(guān)于IOS自定義Button外觀(guān)交互的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Objective-C的入門(mén)學(xué)習(xí)筆記
Objective-C過(guò)去一直以來(lái)都是iOS應(yīng)用程序開(kāi)發(fā)的主要支持語(yǔ)言,雖然現(xiàn)在有了Swift,但需要調(diào)用的很多現(xiàn)有類(lèi)庫(kù)還是Objective-C寫(xiě)成的,值得學(xué)習(xí),下面一起來(lái)看一下這份粗淺的Objective-C的入門(mén)學(xué)習(xí)筆記:2016-05-05iOS中UITableview錯(cuò)位的問(wèn)題怎么修復(fù)
這篇文章主要介紹了iOS中UITableview錯(cuò)位的問(wèn)題以及修復(fù)方法,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友參考下吧2017-01-01iOS仿小紅書(shū)呼吸燈動(dòng)畫(huà)(核心動(dòng)畫(huà)和定時(shí)器)兩種方式實(shí)現(xiàn)
本篇文章主要介紹了iOS仿小紅書(shū)呼吸燈動(dòng)畫(huà)(核心動(dòng)畫(huà)和定時(shí)器)兩種方式實(shí)現(xiàn),非常具有實(shí)用價(jià)值,需要的朋友可以參考下2017-04-04iOS報(bào)Multiple?commands?produceMultiple錯(cuò)誤的解決方案
這篇文章主要為大家介紹了iOS報(bào)Multiple?commands?produceMultiple錯(cuò)誤的解決方案,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-11-11iOS搭建簡(jiǎn)易購(gòu)物車(chē)頁(yè)面
這篇文章主要為大家詳細(xì)介紹了iOS搭建簡(jiǎn)易購(gòu)物車(chē)頁(yè)面,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-08-08在uiview 的tableView中點(diǎn)擊cell進(jìn)入跳轉(zhuǎn)到另一個(gè)界面的實(shí)現(xiàn)方法
這篇文章主要介紹了在uiview 的tableView中點(diǎn)擊cell進(jìn)入跳轉(zhuǎn)到另一個(gè)界面的實(shí)現(xiàn)方法,首先重寫(xiě)uiviewcontrol方法,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-10-10iOS App開(kāi)發(fā)中Masonry布局框架的基本用法解析
這篇文章主要介紹了iOS App開(kāi)發(fā)中Masonry布局框架的基本用法解析,Masonry支持iOS和OSX的Auto Layout,在GitHub上的人氣很高,需要的朋友可以參考下2016-03-03iOS頁(yè)面跳轉(zhuǎn)及數(shù)據(jù)傳遞(三種)
本文主要介紹了iOS頁(yè)面跳轉(zhuǎn)的三種方法及數(shù)據(jù)傳遞的方法。具有很好的參考價(jià)值。下面跟著小編一起來(lái)看下吧2017-03-03