文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

IOS开发自定义Button的外观和交互行为示例详解

2023-02-16 12:01

关注

正文

通过 Style 改变组件的外观或行为是 SwiftUI 提供的一项非常强大的功能。本文将介绍如何通过创建符合 ButtonStyle 或 PrimitiveButtonStyle 协议的实现,自定义 Button 的外观以及交互行为。

可在 此处 获取本文的范例代码

定制 Button 的外观

按钮是 UI 设计中经常会使用到的组件。相较于 UIKit ,SwiftUI 通过 Button 视图,让开发者以少量的代码便可完成按钮的创建工作。

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

多数情况下,开发者通过为 Button 的 label 参数提供不同的视图来定制按钮的外观。

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 协议可以帮助我们定制交互动画。

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 协议的使用方式与 ViewModifier 十分类似。通过 ButtonStyleConfiguration 提供的信息,开发者只需实现 makeBody 方法,即可完成交互动画的定制工作。

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()
        	// 根据 isPressing 来调整交互动画
            .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)

创建一个通用性好 ButtonStyle 实现需要考虑很多条件,例如:role、controlSize、动态字体尺寸、色彩模式等等方面。同 ViewModifier 一样,可以通过环境值获取更多信息:

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 默认的交互行为是在松开按钮的同时执行 Button 指定的操作。并且,在点击按钮后,只要手指( 鼠标 )不松开,无论移动到哪里( 移动到 Button 视图之外 ),松开后仍会执行指定操作。

尽管 Button 的默认手势与 TapGestur 单击操作类似,但 Button 的手势是一种不可撤销的操作。而 TapGesture 在不松开手指的情况下,如果移动到可点击区域外,SwiftUI 将不会调用 onEnded 闭包中的操作。

假如,我们想达成与 TapGesture 类似的效果( 可撤销按钮 ),则可以通过 SwiftUI 提供的另一个协议 PrimitiveButtonStyle 来实现。

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 要求开发者必须通过自行完成交互操作逻辑,并在适当的时机调用 trigger 方法( 可以理解为 Button 的 action 参数对应的闭包 )。

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)
            // 获取点击状态
            .gesture(drag)
            .simultaneousGesture(TapGesture().onEnded{
                configuration.trigger() // 执行 Button 指定的操作
            })
    }
}
extension PrimitiveButtonStyle where Self == CancellableButtonStyle {
    static var cancellable:CancellableButtonStyle {
        CancellableButtonStyle()
    }
}

或许有人会说,既然上面的代码可以通过 DragGesture 模拟获取到点击状态,那么完全可以不使用 PrimitiveButtonStyle 实现同样的效果。如此一来使用 Style 的优势在哪里呢

无论是双击、长按、甚至通过体感触发,开发者均可以通过 PrimitiveButtonStyle 协议定制自己的按钮交互逻辑。

系统预置的 Style

从 iOS 15 开始,SwiftUI 在原有 PlainButtonStyle、DefaultButtonStyle 的基础上,提供了更加丰富的预置 Style。

其中,PlainButtonStyle 除了可以应用于 Button 外,同时也会对 List 以及 Form 的单元格行为造成影响。默认情况下,即使单元格的视图中包含了多个按钮,SwiftUI 也只会将 List 的单元格视作一个按钮( 点击后同时调用所有按钮的操作 )。通过为 List 设置 PlainButtonStyle 风格,便可以调整这一行为,让一个单元格中的多个按钮可以被分别触发。

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

注意事项

VStack {
    Button("11"){print("1")} // plain
    Button("22"){print("2")} // borderless
        .buttonStyle(.borderless)
    Button("33"){print("3")} // borderedProminent
        .buttonStyle(.borderedProminent)
        .buttonStyle(.borderless)
}
.buttonStyle(.plain)

为按钮添加 Trigger

在 SwiftUI 中,为了判断某个按钮是否被按下( 尤其是系统按钮 ),我们通常会通过设置并行手势来添加 trigger :

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

不过,上述方法在 macOS 下不起作用 。通过 Style ,我们可以在设置按钮样式时为其添加触发器:

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 也一样可以实现:

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: {})
        }
    }
}

总结

尽管自定义 Style 的效果显著,但遗憾的是,目前 SwiftUI 仅开放了少数的组件样式协议供开发者自定义使用,并且提供的属性也很有限。希望在未来的版本中,SwiftUI 可以为开发者提供更加强大的自定义组件能力。

以上就是IOS开发自定义Button的外观和交互行为示例详解的详细内容,更多关于IOS自定义Button外观交互的资料请关注编程网其它相关文章!

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-移动开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯