long press + drag

SwiftUIGesturesCombine.sequenced ⟩ long press + drag

⭐️ 「長按後才允許拖曳」 (Long Press + Drag)

要按住持續一秒後,才能開始拖曳,如果提前拖曳,手勢會直接變成 .inactive 狀態。

📗 參考:Mastering SwiftUI, Ch. 17: Using Gestures (p.380)

import SwiftUI
import PlaygroundSupport
PlaygroundPage.current.setLiveView(ContentView())

struct ContentView: View {
    var body: some View {
        VStack {
            Color.pink
                .frame(50)
                .longpressDraggable()    // ⭐️ long press + drag
        }
        .border(.secondary)
    }
}

// -----------------------------
//     👔 LongPressDraggable
// -----------------------------

// view modifier
struct LongPressDraggable: ViewModifier {
    
    // ⭐️ (long press + drag) gesture state
    // ❗️ @GestureState will reset itself when gesture ends.
    // • `DragState` defined below
    @GestureState private var state = DragState.inactive
    
    // ⭐️ vew offset
    @State private var offset = CGSize.zero              
    
    // ⭐️ modify view content
    func body(content: Content) -> some View {
        content
            // ⭐️ dimmer/smaller when pressed
            .opacity(state.isPressing ? 0.5 : 1)
            .scaleEffect(state.isPressing ? 0.8 : 1)
            .animation(.easeInOut, value: state.isPressing)
            
            // ⭐️ change hue when starting dragging
            .hueRotation(state.isDragging ? .degrees(90) : .degrees(0))
            
            // ⭐️ total offset = view offset + drag offset
            .offset(offset + state.offset)
            
            // ⭐️ apply long press + drag gesture
            .gesture(longpressDrag)
    }//end: body
    
    /// ⭐️ long press + drag
    var longpressDrag: some Gesture {
        
        // -------------------------------------------------
        // ⭐️ Long Press + Drag
        // ⭐️ long press for at least 1 sec before dragging
        // -------------------------------------------------
        LongPressGesture(minimumDuration: 1.0)
            .sequenced(before: DragGesture())
            
            // ⭐️ update gesture state
            // • value: SequenceGesture<LongPressGesture, DragGesture>.Value
            // • state: LongPressDraggable.DragState
            // • transaction: animation "context" (ignored)
            .updating($state) { value, state, _ in
                // value = (longpressValue, dragValue)
                switch value {
                
                // ⭐️ (long) press detected
                case .first(true): 
                    state = .pressing
                    
                // ⭐️ long press confirmed, 
                // ⭐️ drag may or may NOT (yet) be detected❗️ 
                case .second(true, let drag):
                    state = .dragging(offset: drag?.translation ?? .zero) 
                    
                // ⭐️ ignore other cases
                default: break
                }
            }
            // ⭐️ long press + drag gesture ended
            .onEnded { value in
                // ⭐️ if drag detected, update view offset
                guard case .second(true, let drag?) = value else { return }
                offset += drag.translation
            }
    }
    
}

extension LongPressDraggable {
    
    // ----------------------------------------------------------------
    // 🔸 注意:
    //    enum 不能命名為 `State` (或 `GestureState`),否則會產生錯誤:
    //      「 ⛔️ Error: enum 'State' cannot be used as an attribute 」
    //    此時 @State (或 @GestureState) 就沒辦法使用❗️ 
    // ----------------------------------------------------------------
    
    /// ⭐️ LongPressDraggable.GestureState:
    /// gesture state for LongPress -> Drag
    enum DragState: CustomStringConvertible {
        
        // state cases
        case inactive 
        case pressing 
        case dragging(offset: CGSize)
        
        // ---------------------------
        //     instance properties
        // ---------------------------
        
        /// state.offset
        var offset: CGSize {
            switch self { 
            case .inactive, .pressing : return CGSize.zero 
            case .dragging(let offset): return offset 
            }
        }
        
        /// state.isPressing
        var isPressing: Bool { 
            switch self {
            case .pressing: return true 
            default       : return false
            } 
        }
        
        /// state.isPressing
        var isDragging: Bool { 
            switch self {
            case .dragging: return true 
            default       : return false
            } 
        }
        
        /// CustomStringConvertible
        var description: String {
            switch self {
            case .pressing: return ".pressing" 
            case .dragging: return ".dragging"
            case .inactive: return ".inactive"
            } 
        }
    }
}

// ---------------------------------------
//     🌀 View + .longpressDraggable()
// ---------------------------------------

extension View {
    func longpressDraggable() -> some View { 
        modifier(LongPressDraggable()) 
    }
}

Last updated

Was this helpful?