โœจlong press + drag

SwiftUI โŸฉ Gestures โŸฉ Combine โŸฉ .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