IOS开发自定义Button的外观和交互行为示例详解
<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li>正文</li><li>定制 Button 的外观</li><li>使用 ButtonStyle 定制交互动画</li><li>使用 PrimitiveButtonStyle 定制交互行为</li><li>系统预置的 Style</li><li>注意事项</li><li>为按钮添加 Trigger</li><li>总结</li></ul></div><p class="maodian"></p><h2>正文</h2><div class="cros igoods"><div class="goodsin" data-img="https://img14.360buyimg.com/pop/jfs/t5992/50/1038616804/55219/4e703907/592e8ae1N4e21f573.jpg" data-name="移动开发丛书 Swift 3核心技术与开发实践:Swift从入门到精通" data-owner="京东自营" data-price="59.2" data-tgid="38" data-url="https://union-click.jd.com/jdc?e=&p=JF8BAMoJK1olXwUFU1xcAUoRA18IGFoTWgACVG4ZVxNJXF9RXh5UHw0cSgYYXBcIWDoXSQVJQwYBVVhaDksXHDZNRwYlHUFfNzYvXQ13WScBbFJ3OgdVNSJDeEcbM2gNHF4dXwMBZF5eDkwXAmoIK2sVXDZQOobrvpOysnPcsdTA1ZEyVW5dD00QB28LG18UWg8HZF5VDHtUVypcWBhdbTYyV25tOEsnAF9WdVpGWwUAUF9eZhZDBDkIWwBFMwYKVFpVCUIfB18KGloXXzYy"></div></div>
<p>通过 Style 改变组件的外观或行为是 SwiftUI 提供的一项非常强大的功能。本文将介绍如何通过创建符合 ButtonStyle 或 PrimitiveButtonStyle 协议的实现,自定义 Button 的外观以及交互行为。</p>
<p>可在 此处 获取本文的范例代码</p>
<p class="maodian"></p><h2>定制 Button 的外观</h2>
<p>按钮是 UI 设计中经常会使用到的组件。相较于 UIKit ,SwiftUI 通过 Button 视图,让开发者以少量的代码便可完成按钮的创建工作。</p>
<div class="jb51code"><pre class="brush:java;">Button(action: signIn) {
Text("Sign In")
}
</pre></div>
<p>多数情况下,开发者通过为 Button 的 <code>label</code> 参数提供不同的视图来定制按钮的外观。</p>
<div class="jb51code"><pre class="brush:java;">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") })
</pre></div>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202302/2023021608585807.gif" /></p>
<p class="maodian"></p><h2>使用 ButtonStyle 定制交互动画</h2>
<p>遗憾的是,上面的代码无法修改按钮在点击后的按压效果。幸好,SwiftUI 提供了 ButtonStyle 协议可以帮助我们定制交互动画。</p>
<div class="jb51code"><pre class="brush:java;">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
}
</pre></div>
<p>ButtonStyle 协议的使用方式与 ViewModifier 十分类似。通过 <code>ButtonStyleConfiguration</code> 提供的信息,开发者只需实现 <code>makeBody</code> 方法,即可完成交互动画的定制工作。</p>
<ul><li>label:目标按钮的当前视图,通常对应着 Button 视图中的 label 参数内容</li><li>role:iOS 15 后新增的参数,用于标识按钮的角色( 取消或具备破坏性)</li><li>isPressed:当前按钮的按压状态,该信息是多数人使用 ButtonStyle 的原动力</li></ul>
<div class="jb51code"><pre class="brush:java;">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()
}
}
</pre></div>
<p>通过 buttonStyle 修饰器应用于 Button 视图</p>
<div class="jb51code"><pre class="brush:java;">Button(action: { pressAction("rounded and shadow") }, label: { label })
.buttonStyle(.roundedAndShadow)
</pre></div>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202302/2023021608585808.gif" /></p>
<p>创建一个通用性好 ButtonStyle 实现需要考虑很多条件,例如:role、controlSize、动态字体尺寸、色彩模式等等方面。同 ViewModifier 一样,可以通过环境值获取更多信息:</p>
<div class="jb51code"><pre class="brush:java;">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)
}
</pre></div>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202302/2023021608585809.jpg" /></p>
<p class="maodian"></p><h2>使用 PrimitiveButtonStyle 定制交互行为</h2>
<p>在 SwiftUI 中,Button 默认的交互行为是在松开按钮的同时执行 Button 指定的操作。并且,在点击按钮后,只要手指( 鼠标 )不松开,无论移动到哪里( 移动到 Button 视图之外 ),松开后仍会执行指定操作。</p>
<p>尽管 Button 的默认手势与 <code>TapGestur</code> 单击操作类似,但 Button 的手势是一种不可撤销的操作。而 TapGesture 在不松开手指的情况下,如果移动到可点击区域外,SwiftUI 将不会调用 onEnded 闭包中的操作。</p>
<p>假如,我们想达成与 TapGesture 类似的效果( 可撤销按钮 ),则可以通过 SwiftUI 提供的另一个协议 PrimitiveButtonStyle 来实现。</p>
<div class="jb51code"><pre class="brush:java;">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()
}
</pre></div>
<p>PrimitiveButtonStyle 与 ButtonStyle 两者之间最大的不同是,PrimitiveButtonStyle 要求开发者必须通过自行完成交互操作逻辑,并在适当的时机调用 trigger 方法( 可以理解为 Button 的 action 参数对应的闭包 )。</p>
<div class="jb51code"><pre class="brush:java;">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()
}
}
</pre></div>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202302/20230216085858010.gif" /></p>
<p>或许有人会说,既然上面的代码可以通过 DragGesture 模拟获取到点击状态,那么完全可以不使用 PrimitiveButtonStyle 实现同样的效果。如此一来<strong>使用 Style 的优势在哪里呢</strong>?</p>
<ul><li>ButtonStyle 和 PrimitiveButtonStyle 是专门针对按钮的样式 API ,它们不仅可以应用于 Button 视图,也可以应用于很多 SwiftUI 预置的系统按钮功能之上,例如:EditButton、Share、Link、NavigationLink( 不在 List 中) 等。</li><li><code>keyboardShortcut</code> 修饰器也只能应用于 Button,视图 + TapGesture 无法设定快捷键。</li></ul>
<p>无论是双击、长按、甚至通过体感触发,开发者均可以通过 PrimitiveButtonStyle 协议定制自己的按钮交互逻辑。</p>
<p class="maodian"></p><h2>系统预置的 Style</h2>
<p>从 iOS 15 开始,SwiftUI 在原有 PlainButtonStyle、DefaultButtonStyle 的基础上,提供了更加丰富的预置 Style。</p>
<ul><li>PlainButtonStyle:不对 Button 视图添加任何修饰</li><li>BorderlessButtonStyle:多数情况下的默认样式,在未指定文字颜色的情况下,将文字修改为强调色</li><li>BorderedButtonStyle:为按钮添加圆角矩形背景,使用 tint 颜色作为背景色</li><li>BorderedProminentButtonStyle:为按钮添加圆角矩形背景,背景颜色为系统强调色</li></ul>
<p>其中,PlainButtonStyle 除了可以应用于 Button 外,同时也会对 List 以及 Form 的单元格行为造成影响。默认情况下,即使单元格的视图中包含了多个按钮,SwiftUI 也只会将 List 的单元格视作一个按钮( 点击后同时调用所有按钮的操作 )。通过为 List 设置 PlainButtonStyle 风格,便可以调整这一行为,让一个单元格中的多个按钮可以被分别触发。</p>
<div class="jb51code"><pre class="brush:java;">List {
HStack {
Button("11"){print("1")}
Button("22"){print("2")}
}
}
.buttonStyle(.plain)
</pre></div>
<p class="maodian"></p><h2>注意事项</h2>
<ul><li>同 ViewModifier 不同,ButtonStyle 并不支持串联,Button 只会采用最靠近的 Style</li></ul>
<div class="jb51code"><pre class="brush:java;">VStack {
Button("11"){print("1")} // plain
Button("22"){print("2")} // borderless
.buttonStyle(.borderless)
Button("33"){print("3")} // borderedProminent
.buttonStyle(.borderedProminent)
.buttonStyle(.borderless)
}
.buttonStyle(.plain)
</pre></div>
<ul><li>某些按钮样式在不同的上下文中的行为和外观会有较大差别,甚至不起作用。例如:无法为 List 中的 NavigationLink 设置样式</li><li>在 Button 的 label 视图或 ButtonStyle 实现中添加的手势操作( 例如 TapGesture )将导致 Button 不再调用其指定的闭包操作,附加手势需在 Button 之外添加( 例如下文的 simultaneousGesture 实现 )</li></ul>
<p class="maodian"></p><h2>为按钮添加 Trigger</h2>
<p>在 SwiftUI 中,为了判断某个按钮是否被按下( 尤其是系统按钮 ),我们通常会通过设置并行手势来添加 trigger :</p>
<div class="jb51code"><pre class="brush:java;">EditButton()
.buttonStyle(.roundedAndShadowPro)
.simultaneousGesture(TapGesture().onEnded{ print("pressed")}) // 设置并行手势
.withTitle("edit button with simultaneous trigger")
</pre></div>
<p>不过,上述方法在 macOS 下不起作用 。通过 Style ,我们可以在设置按钮样式时为其添加触发器:</p>
<div class="jb51code"><pre class="brush:java;">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)
}
}
</pre></div>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202302/20230216085858011.gif" /></p>
<p>当然,用 PrimitiveButtonStyle 也一样可以实现:</p>
<div class="jb51code"><pre class="brush:java;">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: {})
}
}
}
</pre></div>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202302/20230216085858012.gif" /></p>
<p class="maodian"></p><h2>总结</h2>
<p>尽管自定义 Style 的效果显著,但遗憾的是,目前 SwiftUI 仅开放了少数的组件样式协议供开发者自定义使用,并且提供的属性也很有限。希望在未来的版本中,SwiftUI 可以为开发者提供更加强大的自定义组件能力。</p>
<p>以上就是IOS开发自定义Button的外观和交互行为示例详解的详细内容,更多关于IOS自定义Button外观交互的资料请关注琼殿技术社区其它相关文章!</p>
<div class="art_xg">
<b>您可能感兴趣的文章:</b><ul><li>IOS开发Objective-C Runtime使用示例详解</li><li>iOS数据持久化UserDefaults封装器使用详解</li><li>iOS数据持久化KeyChain数据操作详解</li><li>iOS13适配三指撤销和文案限长实例详解</li><li>iOS 16 CocoaAsyncSocket 崩溃修复详解</li><li>iOS开发蓝牙技术应用增加无线连接功能</li></ul>
</div>
</div>
<!--endmain-->
頁:
[1]