亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

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

 更新時間:2023年02月16日 09:59:34   作者:東坡肘子  
這篇文章主要為大家介紹了IOS開發(fā)自定義Button的外觀和交互行為示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪

正文

通過 Style 改變組件的外觀或行為是 SwiftUI 提供的一項非常強大的功能。本文將介紹如何通過創(chuàng)建符合 ButtonStyle 或 PrimitiveButtonStyle 協(xié)議的實現(xiàn),自定義 Button 的外觀以及交互行為。

可在 此處 獲取本文的范例代碼

定制 Button 的外觀

按鈕是 UI 設(shè)計中經(jīng)常會使用到的組件。相較于 UIKit ,SwiftUI 通過 Button 視圖,讓開發(fā)者以少量的代碼便可完成按鈕的創(chuàng)建工作。

Button(action: signIn) {
    Text("Sign In")
}

多數(shù)情況下,開發(fā)者通過為 Button 的 label 參數(shù)提供不同的視圖來定制按鈕的外觀。

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 定制交互動畫

遺憾的是,上面的代碼無法修改按鈕在點擊后的按壓效果。幸好,SwiftUI 提供了 ButtonStyle 協(xié)議可以幫助我們定制交互動畫。

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 十分類似。通過 ButtonStyleConfiguration 提供的信息,開發(fā)者只需實現(xiàn) makeBody 方法,即可完成交互動畫的定制工作。

  • label:目標按鈕的當前視圖,通常對應著 Button 視圖中的 label 參數(shù)內(nèi)容
  • role:iOS 15 后新增的參數(shù),用于標識按鈕的角色( 取消或具備破壞性)
  • isPressed:當前按鈕的按壓狀態(tài),該信息是多數(shù)人使用 ButtonStyle 的原動力
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 來調(diào)整交互動畫
            .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()
    }
}

通過 buttonStyle 修飾器應用于 Button 視圖

Button(action: { pressAction("rounded and shadow") }, label: { label })
       .buttonStyle(.roundedAndShadow)

創(chuàng)建一個通用性好 ButtonStyle 實現(xiàn)需要考慮很多條件,例如:role、controlSize、動態(tài)字體尺寸、色彩模式等等方面。同 ViewModifier 一樣,可以通過環(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 默認的交互行為是在松開按鈕的同時執(zhí)行 Button 指定的操作。并且,在點擊按鈕后,只要手指( 鼠標 )不松開,無論移動到哪里( 移動到 Button 視圖之外 ),松開后仍會執(zhí)行指定操作。

盡管 Button 的默認手勢與 TapGestur 單擊操作類似,但 Button 的手勢是一種不可撤銷的操作。而 TapGesture 在不松開手指的情況下,如果移動到可點擊區(qū)域外,SwiftUI 將不會調(diào)用 onEnded 閉包中的操作。

假如,我們想達成與 TapGesture 類似的效果( 可撤銷按鈕 ),則可以通過 SwiftUI 提供的另一個協(xié)議 PrimitiveButtonStyle 來實現(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 要求開發(fā)者必須通過自行完成交互操作邏輯,并在適當?shù)臅r機調(diào)用 trigger 方法( 可以理解為 Button 的 action 參數(shù)對應的閉包 )。

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)
            // 獲取點擊狀態(tài)
            .gesture(drag)
            .simultaneousGesture(TapGesture().onEnded{
                configuration.trigger() // 執(zhí)行 Button 指定的操作
            })
    }
}
extension PrimitiveButtonStyle where Self == CancellableButtonStyle {
    static var cancellable:CancellableButtonStyle {
        CancellableButtonStyle()
    }
}

或許有人會說,既然上面的代碼可以通過 DragGesture 模擬獲取到點擊狀態(tài),那么完全可以不使用 PrimitiveButtonStyle 實現(xiàn)同樣的效果。如此一來使用 Style 的優(yōu)勢在哪里呢?

  • ButtonStyle 和 PrimitiveButtonStyle 是專門針對按鈕的樣式 API ,它們不僅可以應用于 Button 視圖,也可以應用于很多 SwiftUI 預置的系統(tǒng)按鈕功能之上,例如:EditButton、Share、Link、NavigationLink( 不在 List 中) 等。
  • keyboardShortcut 修飾器也只能應用于 Button,視圖 + TapGesture 無法設(shè)定快捷鍵。

無論是雙擊、長按、甚至通過體感觸發(fā),開發(fā)者均可以通過 PrimitiveButtonStyle 協(xié)議定制自己的按鈕交互邏輯。

系統(tǒng)預置的 Style

從 iOS 15 開始,SwiftUI 在原有 PlainButtonStyle、DefaultButtonStyle 的基礎(chǔ)上,提供了更加豐富的預置 Style。

  • PlainButtonStyle:不對 Button 視圖添加任何修飾
  • BorderlessButtonStyle:多數(shù)情況下的默認樣式,在未指定文字顏色的情況下,將文字修改為強調(diào)色
  • BorderedButtonStyle:為按鈕添加圓角矩形背景,使用 tint 顏色作為背景色
  • BorderedProminentButtonStyle:為按鈕添加圓角矩形背景,背景顏色為系統(tǒng)強調(diào)色

其中,PlainButtonStyle 除了可以應用于 Button 外,同時也會對 List 以及 Form 的單元格行為造成影響。默認情況下,即使單元格的視圖中包含了多個按鈕,SwiftUI 也只會將 List 的單元格視作一個按鈕( 點擊后同時調(diào)用所有按鈕的操作 )。通過為 List 設(shè)置 PlainButtonStyle 風格,便可以調(diào)整這一行為,讓一個單元格中的多個按鈕可以被分別觸發(fā)。

List {
    HStack {
        Button("11"){print("1")}
        Button("22"){print("2")}
    }
}
.buttonStyle(.plain)

注意事項

  • 同 ViewModifier 不同,ButtonStyle 并不支持串聯(lián),Button 只會采用最靠近的 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)
  • 某些按鈕樣式在不同的上下文中的行為和外觀會有較大差別,甚至不起作用。例如:無法為 List 中的 NavigationLink 設(shè)置樣式
  • 在 Button 的 label 視圖或 ButtonStyle 實現(xiàn)中添加的手勢操作( 例如 TapGesture )將導致 Button 不再調(diào)用其指定的閉包操作,附加手勢需在 Button 之外添加( 例如下文的 simultaneousGesture 實現(xiàn) )

為按鈕添加 Trigger

在 SwiftUI 中,為了判斷某個按鈕是否被按下( 尤其是系統(tǒng)按鈕 ),我們通常會通過設(shè)置并行手勢來添加 trigger :

EditButton()
    .buttonStyle(.roundedAndShadowPro)
    .simultaneousGesture(TapGesture().onEnded{ print("pressed")}) // 設(shè)置并行手勢
    .withTitle("edit button with simultaneous trigger")

不過,上述方法在 macOS 下不起作用 。通過 Style ,我們可以在設(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)
    }
}

當然,用 PrimitiveButtonStyle 也一樣可以實現(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() // 原來的 action
                        trigger() // 新增的 action
                    } else {
                        print("release")
                    }
                }, perform: {})
        }
    }
}

總結(jié)

盡管自定義 Style 的效果顯著,但遺憾的是,目前 SwiftUI 僅開放了少數(shù)的組件樣式協(xié)議供開發(fā)者自定義使用,并且提供的屬性也很有限。希望在未來的版本中,SwiftUI 可以為開發(fā)者提供更加強大的自定義組件能力。

以上就是IOS開發(fā)自定義Button的外觀和交互行為示例詳解的詳細內(nèi)容,更多關(guān)于IOS自定義Button外觀交互的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評論