欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

IOS開(kāi)發(fā)自定義Button的外觀(guān)和交互行為示例詳解

 更新時(shí)間:2023年02月16日 09:59:34   作者:東坡肘子  
這篇文章主要為大家介紹了IOS開(kāi)發(fā)自定義Button的外觀(guān)和交互行為示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jì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)文章

最新評(píng)論