Skip to content

Instantly share code, notes, and snippets.

@shengchl
Created December 16, 2019 00:38
Show Gist options
  • Save shengchl/1971bb71ff288c2a92327e51f47fce30 to your computer and use it in GitHub Desktop.
Save shengchl/1971bb71ff288c2a92327e51f47fce30 to your computer and use it in GitHub Desktop.
Custom Toggle SwiftUI View with Haptic Touch-like gesture
//
// HapticTouchToggle.swift
// Created by @shengchalover on 15.12.2019.
// License: MIT
// Gesture mechanics inspired by DraggableCover from MovieSwift by Thomas Ricouard _ 19/06/2019 Apache License 2.0
import SwiftUI
///pass c as root view to UIHostingController in SceneDelegate
let c = HapticTouchToggle()
struct HapticTouchToggle: View {
enum LongPressState { case inactive, pressing, haptic }
private let hapticFeedback = UIImpactFeedbackGenerator(style: .heavy)
init() { self.hapticFeedback.prepare() }
@State private var flashLightOn = false
@GestureState private var longPressState: LongPressState = .inactive
var body: some View {
// MARK: - gesture
let hapticTouch = LongPressGesture(minimumDuration: 0.15).sequenced(before: DragGesture(minimumDistance: .leastNormalMagnitude))
.updating($longPressState) { value, state, _ in
switch value {
//long press recognized
case .first(true): state = .pressing
//longpress ended, drag has not begun. If disabled, behavior resembles springboard haptic ptress
case .second(true, nil): state = .pressing
//longpress ended, drag has begun.
case .second(true, /*let dragValue*/ _):
//fires only once in a gesture session;
if state != .haptic {
//async prevents runtime error; seems like safe workaround (https://swiftui-lab.com/state-changes/)
DispatchQueue.main.async {
self.flashLightOn.toggle()
self.hapticFeedback.impactOccurred(intensity: 1.0)
}
state = .haptic
}
default: state = .inactive
}
}
.onEnded { _ in self.hapticFeedback.impactOccurred(intensity: 0.8) }
// MARK: - view
return FlashlightToggle.gesture(hapticTouch)
}
// MARK: - main view
private var FlashlightToggle: some View {
ZStack(alignment: .center) {
Circle()
.foregroundColor(.init(.secondarySystemBackground))
.opacity(0.9)
FlashLightImage
}
.scaleEffect(hapticScale)
.frame(width: 85, height: 85)
.animation(.interpolatingSpring(mass: 1, stiffness: 150, damping: 15, initialVelocity: 5))
}
// MARK: - supporting view
private var FlashLightImage: some View {
flashLightOn ?
Image(systemName: "flashlight.on.fill")
.font(.system(size: 100))
.scaleEffect(0.35)
:
Image(systemName: "flashlight.off.fill")
.font(.system(size: 100))
.scaleEffect(0.35)
}
// MARK: - dynamic view parameters
private var hapticScale: CGFloat { isBeingPressed ? (hasHapticTouchOccured ? 1.5 : 1.2) : 1.0 }
private var isBeingPressed: Bool { hapticTouchState.0 ? true : false }
private var hasHapticTouchOccured: Bool { hapticTouchState.1 ? true : false }
private var hapticTouchState: (pressing: Bool, hapticTouch: Bool) {
let pressing = (pressing: true, hapticTouch: false)
let pressingWithHaptic = (pressing: true, hapticTouch: true)
let notPressing = (pressing: false, hapticTouch: false)
switch self.longPressState {
case .pressing: return pressing
case .haptic: return pressingWithHaptic
case .inactive: return notPressing
}
}
// MARK: -
}
struct HapticTouchButton_Previews: PreviewProvider {
static var previews: some View {
HapticTouchToggle()
}
}
@shengchl
Copy link
Author

probably quite fragile and outdated

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment