如何在SwiftUI中使用ForEach而不是List进行滑动删除

lfapxunr  于 2023-10-15  发布在  Swift
关注(0)|答案(3)|浏览(148)

我正在SwiftUI中使用ForEach创建一个自定义列表。我的目标是做一个滑动删除手势,而不是将ForEach嵌入到列表中。
这是我目前为止的代码:

import SwiftUI

struct ContentView: View {
let list = ["item1", "item2", "item3", "item4", "item5", "item6"]

var body: some View {
    VStack {
        List{
            
            ForEach(list, id: \.self) { item in
                Text(item)
                    .foregroundColor(.white)
                    .frame(maxWidth: .infinity)
                    .padding()
                    .background(Color.red)
                    .cornerRadius(20)
                    .padding()
                
            }
        }
    }
  }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

我似乎找不到一个手势,将允许我做一个滑动删除,而不使用列表视图。
我还想做一个自定义删除按钮是什么显示当用户滑动一个项目向左(如下图)。

mwngjboj

mwngjboj1#

这是我解决你问题的办法。您应该添加DragGesture并为每行创建offset。请记住,由var声明的变量不能被改变。你必须在之前添加@State

struct ContentView: View {
    @State var list = ["item1", "item2", "item3", "item4", "item5", "item6"]
    @State private var offsets = [CGSize](repeating: CGSize.zero, count: 6)
    var body: some View {
        VStack {
            ForEach(list.indices, id: \.self) { index in
                
                HStack {
                Text(list[index])
                    .foregroundColor(.white)
                    .frame(maxWidth: .infinity)
                    .padding()
                    .background(Color.red)
                    .cornerRadius(20)
                    .padding()
                    
                    Button(action: {
                        self.list.remove(at: index)
                        self.offsets.remove(at: index)
                    }) {
                        Image(systemName: "xmark")
                    }
                }
                .padding(.trailing, -40)
                .offset(x: offsets[index].width)
                .gesture(
                    DragGesture()
                        .onChanged { gesture in
                            self.offsets[index] = gesture.translation
                            if offsets[index].width > 50 {
                                self.offsets[index] = .zero
                            }
                        }
                        .onEnded { _ in
                            if self.offsets[index].width < -100 {
                                self.list.remove(at: index)
                                self.offsets.remove(at: index)
                            }
                            else if self.offsets[index].width < -50 {
                                self.offsets[index].width = -50
                            }
                        }
                )
            }
        }
    }
}
wqlqzqxt

wqlqzqxt2#

基于现有的解决方案:
1.添加了相同的行为滑动,因为它的列表与动画。
1.在代码中提供了注解,以清楚地理解那里发生了什么。
您可以更新顶部的所有常量为您的值,它会工作得很好。并且还可以将.padding(.trailing, -40)更新为您的值(默认情况下隐藏删除按钮)。

struct SomeContentView: View {

@State var list = ["item1", "item2", "item3", "item4", "item5", "item6"]
@State var offsets = [CGSize](repeating: CGSize.zero, count: 6)

private let swipeLeftLimit: CGFloat = -50
private let swipeRightLimit: CGFloat = 50
private let swipeLeftLimitToShow: CGFloat = -30
private let swipeRightLimitToHide: CGFloat = 30

var body: some View {
    VStack {
        ForEach(list.indices, id: \.self) { index in
            
            HStack {
            Text(list[index])
                .foregroundColor(.white)
                .frame(maxWidth: .infinity)
                .padding()
                .background(Color.red)
                .cornerRadius(20)
                .padding()
                
                Button(action: {
                    self.list.remove(at: index)
                    self.offsets.remove(at: index)
                }) {
                    Image(systemName: "xmark")
                }
            }
            .padding(.trailing, -40)
            .offset(x: offsets[index].width)
            .gesture(
                DragGesture()
                    .onChanged { gesture in
                        // Prevent swipe to the right in default position
                        if offsets[index].width == 0 && gesture.translation.width > 0 {
                            return
                        }
                        
                        // Prevent drag more than swipeLeftLimit points
                        if gesture.translation.width < swipeLeftLimit {
                            return
                        }
                        
                        // Prevent swipe againt to the left if it's already swiped
                        if offsets[index].width == swipeLeftLimit && gesture.translation.width < 0 {
                            return
                        }
                        
                        // If view already swiped to the left and we start dragging to the right
                        // Firstly will check if it's swiped left
                        if offsets[index].width >= swipeLeftLimit {
                            // And here checking if swiped to the right more than swipeRightLimit points
                            // If more - need to set the view to zero position
                            if gesture.translation.width > swipeRightLimit {
                                self.offsets[index] = .zero
                                return
                            }
                            
                            // Check if only swiping to the right - update distance by minus swipeLeftLimit points
                            if offsets[index].width != 0 && gesture.translation.width > 0 {
                                self.offsets[index] = .init(width: swipeLeftLimit + gesture.translation.width,
                                                            height: gesture.translation.height)
                                return
                            }
                        }
                            
                        self.offsets[index] = gesture.translation
                    }
                    .onEnded { gesture in
                        withAnimation {
                            // Left swipe handle:
                            if self.offsets[index].width < swipeLeftLimitToShow {
                                self.offsets[index].width = swipeLeftLimit
                                return
                            }
                            if self.offsets[index].width < swipeLeftLimit {
                                self.offsets[index].width = swipeLeftLimit
                                return
                            }
                            
                            // Right swipe handle:
                            if gesture.translation.width > swipeRightLimitToHide {
                                self.offsets[index] = .zero
                                return
                            }
                            if gesture.translation.width < swipeRightLimitToHide {
                                self.offsets[index].width = swipeLeftLimit
                                return
                            }
                            
                            self.offsets[index] = .zero
                        }
                    }
            )
        }
    }
}

您也可以轻松地将'onChange'移动到单独的方法private func handleOnChangeGesture(_ gesture: DragGesture.Value, index: Int) {...paste code here...},并为'onEnd'提供相同的单独方法。

tcbh2hod

tcbh2hod3#

我有点迟到了,但我最近为Views创建了一个简单的滑动手势extension。据我测试,它工作得很好,适用于所有类型的Views
这是整个事情的gist,如果你喜欢在Github上看到它。
这是您将用于将Swipe功能附加到视图的View extension

import SwiftUI

extension View {
  
    public func onSwipe<T>(
        edge: HorizontalEdge = .leading,
        isEnabled: Bool = true,
        action: @escaping () -> Void,
        @ViewBuilder content: @escaping () -> T
    ) -> some View where T : View {
        
        SwipeView(
            edge: edge,
            isEnabled: isEnabled,
            action: action,
            topView: { self },
            bottomView: { content() }
        )
    }
}

这是滑动背后的整个UI逻辑:

//  SwipeView.swift
//
//  Created by Daniel Milkov on 12.10.23.
//  Copyright © 2023 orgName. All rights reserved.
//
import SwiftUI

/** Use via the View's ``onSwipe(edge:isEnabled:action:content:)`` extension */
// Logic based on https://developer.apple.com/forums/thread/123034
struct SwipeView<TopView: View, BottomView: View>: View {
    let edge: HorizontalEdge
    let isEnabled: Bool
    let action: () -> Void
    let topView: () -> TopView
    let bottomView: () -> BottomView
    
    @GestureState private var isDragging: Bool = false
    @State private var gestureState: GestureStatus = .idle
    @State private var offset: CGSize = .zero
    
    var body: some View {
        
        ZStack {
            HStack {
                let isLeading = edge == .leading
                let paddingSide = isLeading ? Edge.Set.leading : Edge.Set.trailing
                
                if (!isLeading) { Spacer() }
                bottomView()
                    .padding(paddingSide, 8)
                if (isLeading) { Spacer() }
            }
            .frame(
                maxWidth: .infinity,
                maxHeight: .infinity
            )
            .background(.blue)
            
            topView()
                .gesture(
                    DragGesture(minimumDistance: 50.0, coordinateSpace: .local)
                        .updating($isDragging) { _, isDragging, _ in
                            isDragging = true
                        }
                        .onChanged(onDragChange(_:))
                        .onEnded(onDragEnded(_:))
                )
                .onChange(of: gestureState) { state in
                    guard state == .started else { return }
                    gestureState = .active
                }
                .onChange(of: isDragging) { value in
                    if value, gestureState != .started {
                        gestureState = .started
                        onStart()
                    } else if !value, gestureState != .ended {
                        gestureState = .cancelled
                        onCancel()
                    }
                }
                .animation(.spring, value: offset.width)
                .offset(x: offset.width)
        }
    }
    
    private func onDragChange(_ value: DragGesture.Value) {
        guard gestureState == .started || gestureState == .active else { return }
        
        onUpdate(translation: value.translation)
    }
    
    private func onDragEnded(_ value: DragGesture.Value) {
        gestureState = .ended
        
        onEnd(
            translation: value.translation,
            velocity: value.velocity
        )
    }
    
    private func onStart() {
        // Do nothing for now
    }
    
    private func onCancel() {
        offset = .zero
    }
    
    private func onUpdate(translation: CGSize) {
        if (!allowGesture(isEnabled: isEnabled, edge: edge, translation: translation)) { return }
        
        offset = translation
    }
    
    private func onEnd(
        translation: CGSize,
        velocity: CGSize
    ) {
        if (gestureSuccessful(edge: edge, translation: translation, velocity: velocity)) {
            let screenWidth = UIScreen.main.bounds.size.width
            let animationEnd = (edge == .leading) ? screenWidth : -screenWidth
            // Animate TopView going to the end of the screen
            offset = CGSize(width: animationEnd, height: 0)
            
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
                // Give the animation time to play before invoking the action
                action()
            }
        } else {
            // Revert TopView to initial position
            offset = .zero
        }
        
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
            // Reset the position of the TopView after the animation's played
            offset = .zero
        }
    }
    
    private func allowGesture(
        isEnabled: Bool,
        edge: HorizontalEdge,
        translation: CGSize
    ) -> Bool {
        if (!isEnabled) { return false }
        
        return switch(translation.width, translation.height) {
            case (...0, -30...30): edge == .trailing
            case (0..., -30...30): edge == .leading
            default: false
        }
    }
    
    private func gestureSuccessful(
        edge: HorizontalEdge,
        translation: CGSize,
        velocity: CGSize
    ) -> Bool {
        let minSwipeDistance = (edge == .leading) ? 100.0 : -100.0
        let minSwipeVelocity = (edge == .leading) ? 1000.0 : -1000.0
        
        return switch(edge) {
            case .leading: translation.width > minSwipeDistance && velocity.width > minSwipeVelocity
            case .trailing: translation.width < minSwipeDistance && velocity.width < minSwipeVelocity
        }
    }
    
    private enum GestureStatus: Equatable {
        case idle
        case started
        case active
        case ended
        case cancelled
    }
}

这里有一个例子,你可以如何使用整个事情:

import SwiftUI

// Example usage
let canDelete = true
let rowId = UUID().uuidString

HStack {
  Text("Some row we want to swipe") 
}.onSwipe(
    isEnabled: canDelete,
    action: { deleteRow(rowId: rowId) },
    content: {
        Text("Delete")
    }
)

可能有更好的方法来处理animationsoffset,但我对Swift相当陌生,所以这是我现在能做的最好的。

相关问题