万物皆可长按:SwiftUI 5.0(iOS 17)极简原生实现任意视图长按惯性加速功能

万物皆可长按:SwiftUI 5.0(iOS 17)极简原生实现任意视图长按惯性加速功能_第1张图片

概览

在 SwiftUI 中与视图进行各种花样交互是 App 具有良好体验不可或缺的一环。

比如,我们希望按钮能在用户长按后产生惯性加速度行为,并想把这一行为扩展到 SwiftUI 中的任意视图中去。

万物皆可长按:SwiftUI 5.0(iOS 17)极简原生实现任意视图长按惯性加速功能_第2张图片

以前,要想实现任意视图的长按加速,我们需要自己写额外代码,费时又费力。

不过,从 SwiftUI 5.0 开始, 为视图准备了长按加速的原生实现,我们仅需 1 行代码即可搞定它。

想知道如何“万物皆可长按”吗?

闲言少叙,Let‘s go!!!


低版本 SwiftUI 中长按加速的实现

在 SwiftUI 5.0 之前,只有 Stepper 视图默认支持长按加速,要想实现任意视图的长按加速功能,我们必须自己动手“丰衣足食”。

其基本思路是:

  • 创建计时器(较高精度)
  • 监听视图被按下事件
  • 在计时器回调中根据长按持续时间计算加速级别并应用加速
  • 在视图停止被按下时重置状态

其部分对应代码如下:

// 计时器跳动间隔0.05秒
static private let TimerInterval = 0.05
// 第1档加速在长按1秒后触发
private let level1AccelerateSeconds = Self.TimerInterval * 20
// 第2档加速在长按2秒后触发
private let level2AccelerateSeconds = Self.TimerInterval * 40
// 第3档(最高档)加速在长按3秒后触发
private let level3AccelerateSeconds = Self.TimerInterval * 60
// 当前加速档
@State private var currentAccelerateLevel = 0

// 用来取消计时器
@State private var cancel: AnyCancellable!
// 当前已长按了多少秒
@State private var pressingSeconds = 0.0
// 每个计时器间隔都会加1,作为阈值来区分不同档位的速度(见随后的代码)
@State private var count = 0

func createTimer() {
    // 若有需要先销毁计时器防止重复创建   
    destroyTimer()
    
    cancel = Timer.publish(every: Self.TimerInterval, on: .main, in: .common).autoconnect().receive(on: DispatchQueue.main).sink {_ in
        
        if pressingSeconds > level3AccelerateSeconds {
            
            currentAccelerateLevel = 3
            // 第3档每次累加100
            v1 &+= 100
            
        }else if pressingSeconds > level2AccelerateSeconds {
            
            currentAccelerateLevel = 2
            // 第2档每次累加10
            v1 &+= 10
            
            pressingSeconds += Self.TimerInterval
        }else if pressingSeconds > level1AccelerateSeconds {
            
            currentAccelerateLevel = 1
            // 第1档每次累加1
            v1 &+= 1
            
            pressingSeconds += Self.TimerInterval
        }else{
            
            // 0档每4次(4个计时器的心跳)累加1
            if count % 4 == 0 {
                v1 &+= 1
            }
            
            pressingSeconds += Self.TimerInterval
        }
        
        // 递增count的值,注意这里我们使用 &+ 运算符来防止Int值溢出
        count &+= 1
    }
}

func destroyTimer() {
	// 只有在计时器被订阅时才销毁它
    if cancel != nil {
        cancel.cancel()
        cancel = nil
        pressingSeconds = 0
        currentAccelerateLevel = 0
    }
}

如上代码所示,我们为长按实现了 3 档加速级别。

如此一来,我们只需在任意视图上应用这一加速行为即可:

Circle()
    .fill(Color.red)
    .gesture(DragGesture(minimumDistance: 0).updating($draggingSize){v,s,t in
        s = v.translation
    }.onEnded {_ in
        isDragging = false
		// 长按结束时销毁计时器
		destroyTimer()
        // 递增变量的值
        value &+= 1
    }.onChanged {_ in      
        isDragging = true
        // 长按开始时创建计时器
        createTimer()
    }, including: .all)

关于 SwiftUI 5.0 之前视图长按加速的实现,请小伙伴们移步到我的专题文章观赏:

  • SwiftUI手势(Gesture)进阶 : 实现任意视图的长按惯性加速行为

SwiftUI 5.0:大道至简?

从 SwiftUI 5.0(iOS 17.0)开始,我们已不需要自已手动实现长按加速功能的代码了, 新增了 .buttonRepeatBehavior() 视图修改器为我们来解决此事:

@available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *)
extension View {

    /// Sets whether buttons in this view should repeatedly trigger their
    /// actions on prolonged interactions.
    ///
    /// Apply this to buttons that increment or decrement a value or perform
    /// some other inherently iterative operation. Interactions such as
    /// pressing-and-holding on the button, holding the button's keyboard
    /// shortcut, or holding down the space key while the button is focused will
    /// trigger this repeat behavior.
    ///
    ///     Button {
    ///         playbackSpeed.advance(by: 1)
    ///     } label: {
    ///         Label("Speed up", systemImage: "hare")
    ///     }
    ///     .buttonRepeatBehavior(.enabled)
    ///
    /// This affects all system button styles, as well as automatically
    /// affects custom `ButtonStyle` conforming types. This does not
    /// automatically apply to custom `PrimitiveButtonStyle` conforming types,
    /// and the ``EnvironmentValues.buttonRepeatBehavior`` value should be used
    /// to adjust their custom gestures as appropriate.
    ///
    /// - Parameter behavior: A value of `enabled` means that buttons should
    ///   enable repeating behavior and a value of `disabled` means that buttons
    ///   should disallow repeating behavior.
    public func buttonRepeatBehavior(_ behavior: ButtonRepeatBehavior) -> some View
}

在按钮上我们可以使用 .buttonRepeatBehavior() 修改器方法来实现长按加速功能:

Button(action: {
    val_1 += 1
}) {
    Text("增加")
}
.buttonRepeatBehavior(.enabled)
.buttonStyle(GrowButtonStyle())
.controlSize(.regular)
.font(.headline)

.buttonRepeatBehavior() 唯一形参的类型为 ButtonRepeatBehavior ,我们有 3 种选择:

/// Use values of this type with the ``View/buttonRepeatBehavior(_:)``
/// modifier.
@available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *)
public struct ButtonRepeatBehavior : Hashable, Sendable {

    /// The automatic repeat behavior.
    public static let automatic: ButtonRepeatBehavior

    /// Repeating button actions will be enabled.
    public static let enabled: ButtonRepeatBehavior

    /// Repeating button actions will be disabled.
    public static let disabled: ButtonRepeatBehavior
}

但遗憾的是,我们仍无法在任意 SwiftUI 视图上应用该修改器方法(用 Tap 或 LongPress 手势)来实现长按加速功能:

VStack {
    Text("长按我试试")
}
.contentShape(Rectangle())
/* 同样不可以长按加速
.onLongPressGesture {
    print("+2")
}*/
.onTapGesture {
    // 不可以长按加速
    print("+1")
}
.buttonRepeatBehavior(.enabled)

.buttonRepeatBehavior() 修改器方法的名称似乎暗示着它只能被用在按钮上。

不过别急,因为在 SwiftUI 中几乎任何视图都可以成为按钮的内容,所以要想在任意视图上实现长按加速功能也不是多难的事儿:

Button {
    val_1 += 1
} label: {
    VStack(alignment: .leading, spacing: 8.0) {
        HStack {
            Image(systemName: "person.fill.questionmark")
                .foregroundStyle(Color.yellow.gradient)
            Spacer()
            Text("刘备·玄德")
        }
        .foregroundStyle(.red)
        .font(.headline.weight(.bold))
        
        Text("三国大事,分久必合合久必分")
            .foregroundStyle(.black.gradient)
            .font(.subheadline)
    }
}
.buttonRepeatBehavior(.enabled)
.buttonStyle(.borderless)
.padding()
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 10.0))

如果小伙伴们不希望视图被按下去时呈现出按钮被按下的效果,我们还可以在按钮上应用 .plain 样式来让用户觉得这不是一个按钮:

Button { ... } label: {
    VStack {...}
}
.buttonRepeatBehavior(.enabled)
// 按钮被按下去时外观无任何变化!
.buttonStyle(.plain)

SwiftUI 对 Shape 渲染的简化

在 SwiftUI 5.0 之前,要想同时对形状进行描边(stroke)和填充(fill),我们需要利用视图的 .background() 方法:

Circle()
    .strokeBorder(.red, lineWidth: 20)
    .background(Circle().fill(.orange))
    .frame(width: 150, height: 150)

或者,我们也可以利用 ZStack :

ZStack {
    Circle()
        .fill(.orange)

    Circle()
        .strokeBorder(.red, lineWidth: 20)
}
.frame(width: 150, height: 150)

不过,从 SwiftUI 5.0 开始我们再也无需绞尽脑汁来实现任意形状同时描边和填充的操作了!

我们只需“顺其自然”即可:

Circle()
    .stroke(.red, lineWidth: 20)
    .fill(.orange)
    .frame(width: 150, height: 150)

万物皆可长按:SwiftUI 5.0(iOS 17)极简原生实现任意视图长按惯性加速功能_第3张图片

我们甚至可以任意重复组合 .fill() 和 .stroke() 方法来实现一个彩虹圈圈:

Circle()
    .stroke(.blue, lineWidth: 45)
    .stroke(.green, lineWidth: 35)
    .stroke(.yellow, lineWidth: 25)
    .stroke(.orange, lineWidth: 15)
    .stroke(.red, lineWidth: 5)
    .fill(.mint)
    .frame(width: 300, height: 300)
    .padding()

效果如下:

万物皆可长按:SwiftUI 5.0(iOS 17)极简原生实现任意视图长按惯性加速功能_第4张图片

现在,不管多复杂形状的涂色和描边我们也不怕了,棒棒哒!!!

源代码

友情提示:本文中的代码需要使用 Xcode 15 beta 编译运行。

全部源代码在此:

import SwiftUI

struct GrowButtonStyle: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .padding(.vertical, 8)
            .padding(.horizontal)
            .foregroundStyle(.white.gradient)
            .background(Color.blue, in: RoundedRectangle(cornerRadius: 8))
            .scaleEffect(configuration.isPressed ? CGSize(width: 1.2, height: 1.2) : CGSize(width: 1.0, height: 1.0))
            .brightness(configuration.isPressed ? 0.1 : 0.0)
    }
}

struct ContentView: View {
    
    static let randomColors = [Color.red, .indigo, .green, .blue, .black, .brown, .orange, .gray, .red, .yellow, .purple, .pink, .cyan, .mint, .teal]
    
    @State var stepperValue = 0
    @State var val_0 = 0
    @State var val_1 = 0
    @State var talent = Self.randomColors.randomElement()!
    
    var body: some View {
        NavigationStack {
            Form {
                Text("大熊猫侯佩 @ csdn")
                    .font(.headline)
                    .foregroundStyle(.gray)
                
                Section("自带长按加速的 Stepper 视图") {
                    Stepper("战斗力: \(stepperValue)", value: $stepperValue)
                }.textCase(nil)
                
                Section("按钮默认无长按加速机制") {
                    LabeledContent {
                        Button(action: {
                            val_0 += 1
                        }) {
                            Text("增加")
                        }
                        .buttonStyle(GrowButtonStyle())
                        .controlSize(.regular)
                        .font(.headline)
                    } label: {
                        Text("智力: \(val_0)")
                    }
                }
                
                if #available(iOS 17, *) {
                    Section("SwiftUI 5.0(iOS 17)任意视图加速") {
                        LabeledContent {
                            Button(action: {
                                val_1 += 1
                            }) {
                                Text("增加")
                            }
                            .buttonRepeatBehavior(.enabled)
                            .buttonStyle(GrowButtonStyle())
                            .controlSize(.regular)
                            .font(.headline)
                        } label: {
                            Text("魅力: \(val_1)")
                        }
                        
                        LabeledContent {
                            Button {
                                val_1 += 1
                            } label: {
                                VStack(alignment: .leading, spacing: 8.0) {
                                    HStack {
                                        Image(systemName: "person.fill.questionmark")
                                            .foregroundStyle(Color.yellow.gradient)
                                        Spacer()
                                        Text("刘备·玄德")
                                    }
                                    .foregroundStyle(.red)
                                    .font(.headline.weight(.bold))
                                    
                                    
                                    Text("三国大事,分久必合合久必分")
                                        .foregroundStyle(.black.gradient)
                                        .font(.subheadline)
                                }
                            }
                            .buttonRepeatBehavior(.enabled)
                            .buttonStyle(.borderless)
                            .padding()
                            .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 10.0)
                            )
                            .controlSize(.regular)
                            .font(.headline)
                            .containerRelativeFrame(.horizontal, count: 2, spacing: 8)
                        } label: {
                            Text("魅力: \(val_1)")
                        }
                        
                        LabeledContent {
                            Button {
                                talent = Self.randomColors.randomElement()!
                            } label: {
                                Circle()
                                    .fill(talent)
                                    .stroke(Color.brown, lineWidth: 3.0)
                                    .frame(width: 99)
                            }
                            .buttonStyle(.plain)
                            .buttonRepeatBehavior(.enabled)
                        } label: {
                            Text("随机天赋")
                        }
                        
                        /*
                        VStack {
                            Text("长按我试试")
                        }
                        .contentShape(Rectangle())
                        /* 同样不可以长按加速
                        .onLongPressGesture {
                            print("+2")
                        }*/
                        .onTapGesture {
                            // 不可以长按加速
                            print("+1")
                        }
                        .buttonRepeatBehavior(.enabled)
                        */
                    }
                }
            }.navigationTitle("视图长按加速 DEMO")
        }
    }
}

总结

在本篇博文中,我们讨论了 SwiftUI 5.0(iOS 17)中如何仅使用一行代码就搞定任意视图的长按惯性加速功能,并介绍了 SwiftUI 5.0 中形状(Shape)新的渲染机制。

感谢观赏,再会!

你可能感兴趣的:(Apple开发入门,SwiftUI,5.0,iOS,17.0,长按加速,按钮,形状,stroke,fill)