用于为任何 SwiftUI View 创建完全可定制的滑动操作的库,类似于 Apple 的 swipeActions(edge:allowsFullSwipe:content:)
,该操作从 iOS 15 开始可用,并且仅在 List 中可用 🤷🏼♂️。你可以在以 iOS 13 为目标的任何视图(例如 Text
或 VStack
)的项目中使用 SwipeActions
。
👨🏻💻 欢迎订阅 telegram 上的 SwiftUI dev 频道。
功能支持 | 此 SPM | 其他 |
---|---|---|
SwiftUI | 🟢 | 🟢 |
SPM | 🟢 | 🟢 |
MacOS | 🟢 | 🔴 |
iOS 13.0 | 🟢 | 🔴 |
RTL 语言 | 🟢 | 🔴 |
完全滑动模式 | 🟢 | 🔴 |
灵活性 | 🟢 | 🟡 |
触感反馈 | 🟢 | 🟡 |
易用性 | 🟢 | 🔴 |
要使用 SwiftPM 将 SwipeActions
集成到您的项目中,请将以下内容添加到您的 Package.swift
中
dependencies: [
.package(url: "https://github.com/c-villain/SwipeActions", from: "0.1.0"),
],
或者通过 XcodeGen 插入到您的 project.yml
中
name: YourProjectName
options:
deploymentTarget:
iOS: 13.0
packages:
SwipeActions:
url: https://github.com/c-villain/SwipeActions
from: 0.1.0
targets:
YourTarget:
type: application
...
dependencies:
- package: SwipeActions
不同类型的菜单
两种类型都可以升级为完全滑动
在 .addSwipeAction { ... }
修饰符内部使用 Leading { ... }
和 Trailing { ... }
闭包
import SwipeActions
struct YourView: View {
var body: some View {
ScrollView {
LazyVStack {
ForEach(1...100, id: \.self) { cell in
Text("Cell \(cell)")
.frame(height: 50, alignment: .center)
.frame(maxWidth: .infinity)
.contentShape(Rectangle())
.addSwipeAction {
Leading { //<= HERE
Button {
print("edit \(cell)")
} label: {
Image(systemName: "pencil")
.foregroundColor(.white)
}
.frame(width: 60, height: 50, alignment: .center)
.contentShape(Rectangle())
.background(Color.green)
}
Trailing { //<= HERE
HStack(spacing: 0) {
Button {
print("remove \(cell)")
} label: {
Image(systemName: "trash")
.foregroundColor(.white)
}
.frame(width: 60, height: 50, alignment: .center)
.contentShape(Rectangle())
.background(Color.red)
Button {
print("Inform \(cell)")
} label: {
Image(systemName: "bell.slash.fill")
.foregroundColor(.white)
}
.frame(width: 60, height: 50, alignment: .center)
.background(Color.blue)
}
}
}
}
}
}
}
}
不要忘记您的操作通常是子视图,特别是按钮或其他东西。请安排它们
YourView()
.addSwipeAction(edge: .trailing) {
HStack(spacing: 0) { // <= 👀 Look here
Rectangle()
.fill(Color.green.opacity(0.8))
.frame(width: 8.0, height: 80)
Button {
} label: {
Image(systemName: "message")
.foregroundColor(.white)
.frame(width: 60, height: 80)
.contentShape(Rectangle())
}
.background(Color.blue)
}
}
使用 .addSwipeAction(edge: ) { ... }
修饰符,edge
- HorizontalAlignment
值输入参数 - 带有使用 .leading
或 .trailing
的两种情况
import SwipeActions
struct YourView: View {
var body: some View {
ScrollView {
LazyVStack {
ForEach(1...100, id: \.self) { cell in
Text("Cell \(cell)")
.frame(height: 50, alignment: .center)
.frame(maxWidth: .infinity)
.contentShape(Rectangle())
.addSwipeAction(edge: .trailing) { // <= choose here .trailing or .leading
HStack(spacing: 0) {
Button {
print("remove \(cell)")
} label: {
Image(systemName: "trash")
.foregroundColor(.white)
}
.frame(width: 60, height: 50, alignment: .center)
.contentShape(Rectangle())
.background(Color.red)
Button {
print("Inform \(cell)")
} label: {
Image(systemName: "bell.slash.fill")
.foregroundColor(.white)
}
.frame(width: 60, height: 50, alignment: .center)
.background(Color.blue)
}
}
}
}
}
}
}
将 SwipeState
变量添加到您的 View
,并将其作为 binding
传递到 .addSwipeAction(state:)
中
struct YourView: View {
@State var state: SwipeState = .untouched // <= HERE
var body: some View {
ScrollView {
VStack(spacing: 2) {
ForEach(1 ... 30, id: \.self) { cell in
Text("Cell \(cell)")
.addSwipeAction(state: $state) { // <= HERE
....
}
}
}
}
}
}
对于完全滑动,请使用修饰符 .addFullSwipeAction(menu:swipeColor:swipeRole:state:content:action:)
基本上,完全滑动操作有两个主要的 SwipeRole
:.destructive
(默认)和其他一个。
此角色用于关闭/隐藏/移除单元格。
struct YourView: View {
@State var range: [Int] = [1,2,3,4,5,6,7,8,9,10]
var body: some View {
ScrollView {
VStack(spacing: 2) {
ForEach(range, id: \.self) { cell in
Text("Cell \(cell)")
.addFullSwipeAction(
menu: .slided,
swipeColor: .red) { // <= Color is the same as last button in Trailing for full effect
Leading {
...
}
Trailing {
...
Button {
withAnimation {
if let index = range.firstIndex(of: cell) {
range.remove(at: index)
}
}
} label: {
Image(systemName: "trash")
.foregroundColor(.white)
}
.contentShape(Rectangle())
.frame(width: 60)
.frame(maxHeight: .infinity)
.background(Color.red) // <=== Look here
}
} action: { // <= action for full swiping
withAnimation {
if let index = range.firstIndex(of: cell) {
range.remove(at: index)
}
}
}
}
}
}
}
}
此角色用于对单元格执行某些操作。
struct YourView: View { ]
var body: some View {
ScrollView {
VStack(spacing: 2) {
ForEach(1...10, id: \.self) { cell in
Text("Cell \(cell)")
.addFullSwipeAction(menu: .slided,
swipeColor: .green, // <=== Color is the same as last button in Trailing for full effect
swipeRole: .defaults) { // <=== Add this parameter
Leading {
...
}
Trailing {
HStack(spacing: 0) {
...
Button {
} label: {
Image(systemName: "trash")
.foregroundColor(.white)
}
.contentShape(Rectangle())
.frame(width: 60)
.frame(maxHeight: .infinity)
.background(Color.green) // <=== Look here
}
}
} action: { // <=== action for full swiping
...
}
}
}
}
}
}
使用 .frame(maxHeight: .infinity)
YourView()
.addSwipeAction(menu: .slided, edge: .trailing) {
Button {
...
} label: {
Image("trash")
.font(.system(size: 20.0))
.foregroundColor(.white)
.frame(width: 68, alignment: .center)
.frame(maxHeight: .infinity) // <= Look here
.background(.red)
}
}
对于与 .slided
类型一起使用,没有 限制或任何建议!
对于 .swiped
,使用 非透明 颜色层或与 alfa = 1.0
相同的颜色
ForEach(1 ... 30, id: \.self) { cell in
Text("Cell \(cell)")
.padding()
.frame(height: 80)
.frame(maxWidth: .infinity)
//.background(Color.green.opacity(0.2)) // <== ❌ DON'T USE SUCH WAY!
//.background(Color(red: 0.841, green: 0.956, blue: 0.868)) // <== ✅ USE THIS WAY!
.background( // <== OR THIS WAY!
ZStack {
Color(UIColor.systemBackground) // non-transparent color layer
Color.green.opacity(0.2)
}
)
.contentShape(Rectangle())
.listStyle(.plain)
.addSwipeAction(menu: .swiped, // <=== SWIPED TYPE
state: $state) {
Leading {
...
}
}
...
}
基本上,如果您的应用程序的最低部署目标是 iOS 15,我建议使用 Apple 的 滑动操作 用于 List。无论如何,您都可以使用这个。
由于使用 List
的一些特性,您应该
为单元格宽度指定一个框架,例如 .frame(width: UIScreen.main.bounds.size.width - 32, height: 80)
,并为滑动操作上的按钮指定一个框架,例如 .frame(width: 60, height: 80)
。请注意,框架中的高度应相同!
为单元格添加修饰符 .onTapGesture { ... }
以覆盖对滑动操作按钮的点击
为单元格添加修饰符 .listRowInsets(EdgeInsets())
List(elements) { e in
Text(e.name)
.frame(width: UIScreen.main.bounds.size.width - 32, height: 80) // <= HERE
.background(Color(UIColor.systemBackground))
.onTapGesture { // <=== HERE
print("on cell tap!")
}
.addSwipeAction(menu: .swiped,
edge: .trailing,
state: $state) {
Button {
print("remove")
} label: {
Image(systemName: "trash")
.foregroundColor(.white)
}
.frame(width: 60, height: 80, alignment: .center) // <= HERE
.contentShape(Rectangle())
.background(Color.red)
}
.listRowInsets(EdgeInsets()) // <=== HERE
}
.padding(16)
在示例中查找代码。
为了避免在没有水平填充的视图后立即开始显示滑动操作中的内容的效果
在 .addSwipeAction { ... }
中,添加一个填充了与根视图相同颜色的 Rectangle
YourView()
.frame(height: 80)
.frame(maxWidth: .infinity)
.background(Color.green.opacity(0.8)) // <= Look here
.addSwipeAction(edge: .trailing) {
HStack(spacing: 0) {
Rectangle() // <=== HERE!
.fill(Color.green.opacity(0.8)) // <= 💡 Don't forget!
.frame(width: 8.0, height: 80)
Button {
} label: {
Image(systemName: "message")
.foregroundColor(.white)
}
.frame(width: 60, height: 80)
.contentShape(Rectangle())
.background(Color.blue)
}
}
由于 SwiftUI 难以检测滑动视图和打开上下文菜单的手势,我建议您在 .addSwipeAction
(或 addFullSwipeAction
)之后使用 .contextMenu
YourView()
.frame(height: 80)
.frame(maxWidth: .infinity)
.contentShape(Rectangle())
.padding()
.background(Color(UIColor.systemBackground))
.addFullSwipeAction(...) { ... } // <=== Look here!
.contextMenu { ... }
实际上,如果您不使用 .contentShape(Rectangle())
,您也可以在 .addSwipeAction
(或 addFullSwipeAction
)之前添加 .contextMenu
YourView()
.frame(height: 80)
.frame(maxWidth: .infinity)
//.contentShape(Rectangle()) // <=== Look here!
.padding()
.contextMenu { ... } // <=== Look here!
.background(Color(UIColor.systemBackground))
.addFullSwipeAction(...) { ... } // <=== Look here!
使用修饰符 .swipeHint
应用此修饰符的位置取决于菜单的类型。
对于 .slided
类型的菜单,严格在 .addSwipeAction
之后添加 .swipeHint
👇🏻
ForEach(range, ...) {
YourCell()
...
.addFullSwipeAction(
menu: .slided, // <== LOOK HERE
swipeColor: .red,
state: $state) {
Leading {
...
}
Trailing {
...
}
}
.swipeHint(cell == range.first, hintOffset: 120.0) // for trailing <== LOOK HERE
.swipeHint(cell == range[1] , hintOffset: -120.0) // for leading <== LOOK HERE
...
}
对于 .swiped
类型的菜单,严格在 .addSwipeAction
之前添加 .swipeHint
👇🏻
ForEach(range, ...) {
YourCell()
...
.swipeHint(cell == range.first, hintOffset: 120.0) // for trailing <== LOOK HERE
.swipeHint(cell == range[1], hintOffset: -120.0) // for leading <== LOOK HERE
.addFullSwipeAction(
menu: .swiped, // <== Look here
swipeColor: .red,
state: $state) {
Leading {
...
}
Trailing {
...
}
}
...
}
由于 SwiftUI 的理念通常不太可能控制多点触控,特别是拖动多个单元格。无论如何,我们可以使用特殊的视图修饰符禁用多点触控:.allowMultitouching(false)
基于 UIKit。在使用滑动操作之前严格添加此修饰符
...
YourView(...)
.allowMultitouching(false) // <= Look here
.addSwipeAction( ...) {
...
}
...
默认情况下,此标志为 true。使用此修饰符将重复 telegram 的行为,即您在多点触控期间只能拖动一个单元格。
实际上,该问题仅存在于完全滑动模式下。在默认模式下,您可以拖动多个单元格,但在结束触摸后,只会打开一个单元格。
但由于此解决方案基于 UIKit,您无法使用 drawingGroup()
优化 YourView
的渲染
...
YourView(...)
.allowMultitouching(false) // <= look here
.addSwipeAction( ...) {
...
}
.drawingGroup() // <= ❌ DON'T DO THAT
...
你肯定会得到这个
实际上,渲染将由 SwiftUI 引擎优化... 为了完全控制,您可以将视图修饰符 .identifier(your id)
添加到 YourView(...)
要控制视图的 id 以进行优化,您应该使用修饰符 .identifier(your id)
ForEach(...) { cell in
YourView(cell)
.addSwipeAction(...) {}
.identifier(cell) // <= Look here
}
实际上,如果您忘记添加标识符,请不要担心,系统会自动添加 id。
要控制滑动灵敏度(拖动的最小距离),您应该使用修饰符 swipeSensitive
。
ForEach(...) { cell in
YourView(cell)
.addSwipeAction(...) {}
.swipeSensitive(.medium) // <= Look here. You can control it here
}
.swipeSensitive(.medium) // <= Look here. Or here!
如果您忘记添加灵敏度,请不要担心,系统将提供其自己的默认 low
值。实际上,这是最舒适的行为。
此库支持从右到左的语言,如阿拉伯语和希伯来语。
通过将 .environment(\.layoutDirection, .rightToLeft)
添加到带有滑动操作的视图来检查它。
struct ContentView: View {
var body: some View {
LazyVStack {
ForEach(0..<5) { index in
Text("Item \(index)")
.swipeActions {
Button("Delete") {
print("Item deleted")
}
.tint(.red)
}
}
}
.environment(\.layoutDirection, .rightToLeft) // <= Look here
}
}
默认情况下,在完全滑动模式下,操作具有触感反馈。
要禁用它,请使用 .allowFullSwipeHaptics(false)
YourView()
.addFullSwipeAction(...) { ... }
.allowFullSwipeHaptics(false) // <= Look here
您可以使用 .fullSwipeHapticFeedback(:)
轻松更改此反馈类型。
YourView()
.addFullSwipeAction(...) { ... }
.fullSwipeHapticFeedback(.medium()) // <= Look here
您可以轻松地将触感反馈添加到滑动操作中的自己的按钮。
YourView()
.addFullSwipeAction(...) {
...
Trailing {
Button {
HapticsProvider.sendHapticFeedback(.heavy()) // <= Look here
...
} label: { ... }
}
}
SwipeActions 包是在 MIT 许可证下发布的。
感谢 Prafulla Singh 通过他的 SwiftUI 教程 提供的灵感。