Created
December 16, 2019 00:38
-
-
Save shengchl/1971bb71ff288c2a92327e51f47fce30 to your computer and use it in GitHub Desktop.
Custom Toggle SwiftUI View with Haptic Touch-like gesture
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
probably quite fragile and outdated