Change haptics.
This commit is contained in:
parent
1748f48165
commit
5aed19bf05
|
@ -24,7 +24,7 @@ public struct Instance: Codable {
|
|||
public let shortDescription: String?
|
||||
|
||||
/// A plain-text description defined by the admin.
|
||||
public let description: String?
|
||||
public let description: Html?
|
||||
|
||||
/// The URL for the thumbnail image.
|
||||
public let thumbnail: URL?
|
||||
|
|
|
@ -3,68 +3,49 @@
|
|||
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
|
||||
import CoreHaptics
|
||||
import UIKit
|
||||
|
||||
public final class HapticService: ObservableObject {
|
||||
public static let shared = HapticService()
|
||||
|
||||
private let hapticEngine: CHHapticEngine?
|
||||
private var needsToRestart = false
|
||||
public class HapticService {
|
||||
public static let shared: HapticService = .init()
|
||||
|
||||
/// Fires a transient haptic event with the given intensity and sharpness (0-1).
|
||||
public func touch(intensity: Float = 0.75, sharpness: Float = 0.5) {
|
||||
do {
|
||||
let event = CHHapticEvent(
|
||||
eventType: .hapticTransient,
|
||||
parameters: [
|
||||
CHHapticEventParameter(parameterID: .hapticIntensity, value: intensity),
|
||||
CHHapticEventParameter(parameterID: .hapticSharpness, value: sharpness)
|
||||
],
|
||||
relativeTime: 0)
|
||||
|
||||
let pattern = try CHHapticPattern(events: [event], parameters: [])
|
||||
let player = try hapticEngine?.makePlayer(with: pattern)
|
||||
|
||||
if needsToRestart {
|
||||
try? start()
|
||||
}
|
||||
|
||||
try player?.start(atTime: CHHapticTimeImmediate)
|
||||
} catch {
|
||||
ErrorService.shared.handle(error, message: "Haptic service failed.")
|
||||
}
|
||||
public enum HapticType {
|
||||
case buttonPress
|
||||
case dataRefresh(intensity: CGFloat)
|
||||
case notification(_ type: UINotificationFeedbackGenerator.FeedbackType)
|
||||
case tabSelection
|
||||
case timeline
|
||||
}
|
||||
|
||||
private let selectionGenerator = UISelectionFeedbackGenerator()
|
||||
private let impactGenerator = UIImpactFeedbackGenerator(style: .heavy)
|
||||
private let notificationGenerator = UINotificationFeedbackGenerator()
|
||||
|
||||
private init() {
|
||||
hapticEngine = try? CHHapticEngine()
|
||||
hapticEngine?.resetHandler = resetHandler
|
||||
hapticEngine?.stoppedHandler = restartHandler
|
||||
hapticEngine?.playsHapticsOnly = true
|
||||
try? start()
|
||||
selectionGenerator.prepare()
|
||||
impactGenerator.prepare()
|
||||
}
|
||||
|
||||
/// Stops the internal CHHapticEngine. Should be called when your app enters the background.
|
||||
public func stop(completionHandler: CHHapticEngine.CompletionHandler? = nil) {
|
||||
hapticEngine?.stop(completionHandler: completionHandler)
|
||||
}
|
||||
@MainActor
|
||||
public func fireHaptic(of type: HapticType) {
|
||||
guard supportsHaptics else { return }
|
||||
|
||||
/// Starts the internal CHHapticEngine. Should be called when your app enters the foreground.
|
||||
public func start() throws {
|
||||
try hapticEngine?.start()
|
||||
needsToRestart = false
|
||||
}
|
||||
|
||||
private func resetHandler() {
|
||||
do {
|
||||
try start()
|
||||
} catch {
|
||||
needsToRestart = true
|
||||
switch type {
|
||||
case .buttonPress:
|
||||
impactGenerator.impactOccurred()
|
||||
case let .dataRefresh(intensity):
|
||||
impactGenerator.impactOccurred(intensity: intensity)
|
||||
case let .notification(type):
|
||||
notificationGenerator.notificationOccurred(type)
|
||||
case .tabSelection:
|
||||
selectionGenerator.selectionChanged()
|
||||
case .timeline:
|
||||
selectionGenerator.selectionChanged()
|
||||
}
|
||||
}
|
||||
|
||||
private func restartHandler(_ reasonForStopping: CHHapticEngine.StoppedReason? = nil) {
|
||||
resetHandler()
|
||||
public var supportsHaptics: Bool {
|
||||
CHHapticEngine.capabilitiesForHardware().supportsHaptics
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -90,14 +90,10 @@ struct VernissageApp: App {
|
|||
}
|
||||
.navigationViewStyle(.stack)
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
|
||||
try? HapticService.shared.start()
|
||||
Task {
|
||||
await self.loadInBackground()
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)) { _ in
|
||||
HapticService.shared.stop()
|
||||
}
|
||||
.onReceive(timer) { time in
|
||||
Task {
|
||||
await self.loadInBackground()
|
||||
|
|
|
@ -89,6 +89,7 @@ struct MainView: View {
|
|||
ToolbarItem(placement: .principal) {
|
||||
Menu {
|
||||
Button {
|
||||
HapticService.shared.fireHaptic(of: .tabSelection)
|
||||
viewMode = .home
|
||||
} label: {
|
||||
HStack {
|
||||
|
@ -98,6 +99,7 @@ struct MainView: View {
|
|||
}
|
||||
|
||||
Button {
|
||||
HapticService.shared.fireHaptic(of: .tabSelection)
|
||||
viewMode = .trending
|
||||
} label: {
|
||||
HStack {
|
||||
|
@ -107,6 +109,7 @@ struct MainView: View {
|
|||
}
|
||||
|
||||
Button {
|
||||
HapticService.shared.fireHaptic(of: .tabSelection)
|
||||
viewMode = .local
|
||||
} label: {
|
||||
HStack {
|
||||
|
@ -116,6 +119,7 @@ struct MainView: View {
|
|||
}
|
||||
|
||||
Button {
|
||||
HapticService.shared.fireHaptic(of: .tabSelection)
|
||||
viewMode = .federated
|
||||
} label: {
|
||||
HStack {
|
||||
|
@ -127,6 +131,7 @@ struct MainView: View {
|
|||
Divider()
|
||||
|
||||
Button {
|
||||
HapticService.shared.fireHaptic(of: .tabSelection)
|
||||
viewMode = .profile
|
||||
} label: {
|
||||
HStack {
|
||||
|
@ -136,6 +141,7 @@ struct MainView: View {
|
|||
}
|
||||
|
||||
Button {
|
||||
HapticService.shared.fireHaptic(of: .tabSelection)
|
||||
viewMode = .notifications
|
||||
} label: {
|
||||
HStack {
|
||||
|
@ -163,6 +169,7 @@ struct MainView: View {
|
|||
Menu {
|
||||
ForEach(self.dbAccounts) { account in
|
||||
Button {
|
||||
HapticService.shared.fireHaptic(of: .buttonPress)
|
||||
self.tryToSwitch(account)
|
||||
} label: {
|
||||
if self.applicationState.account?.id == account.id {
|
||||
|
@ -176,7 +183,7 @@ struct MainView: View {
|
|||
Divider()
|
||||
|
||||
Button {
|
||||
HapticService.shared.touch()
|
||||
HapticService.shared.fireHaptic(of: .buttonPress)
|
||||
self.routerPath.presentedSheet = .settings
|
||||
} label: {
|
||||
Label("Settings", systemImage: "gear")
|
||||
|
@ -208,7 +215,7 @@ struct MainView: View {
|
|||
if viewMode == .local || viewMode == .home || viewMode == .federated || viewMode == .trending {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
HapticService.shared.touch()
|
||||
HapticService.shared.fireHaptic(of: .buttonPress)
|
||||
self.routerPath.presentedSheet = .newStatusEditor
|
||||
} label: {
|
||||
Image(systemName: "square.and.pencil")
|
||||
|
@ -238,8 +245,6 @@ struct MainView: View {
|
|||
}
|
||||
|
||||
private func tryToSwitch(_ account: AccountData) {
|
||||
HapticService.shared.touch()
|
||||
|
||||
Task {
|
||||
// Verify access token correctness.
|
||||
let authorizationSession = AuthorizationSession()
|
||||
|
|
|
@ -72,7 +72,7 @@ struct PhotoEditorView: View {
|
|||
}
|
||||
|
||||
private func update() async {
|
||||
HapticService.shared.touch()
|
||||
HapticService.shared.fireHaptic(of: .buttonPress)
|
||||
self.hideKeyboard()
|
||||
|
||||
if let uploadedAttachment = self.photoAttachment.uploadedAttachment {
|
||||
|
|
|
@ -61,6 +61,8 @@ struct PlaceSelectorView: View {
|
|||
|
||||
ForEach(self.places, id: \.id) { place in
|
||||
Button {
|
||||
HapticService.shared.fireHaptic(of: .buttonPress)
|
||||
|
||||
self.place = place
|
||||
self.dismiss()
|
||||
} label: {
|
||||
|
|
|
@ -36,6 +36,8 @@ struct SignInView: View {
|
|||
.disableAutocorrection(true)
|
||||
|
||||
Button("Sign in") {
|
||||
HapticService.shared.fireHaptic(of: .buttonPress)
|
||||
|
||||
let baseAddress = self.getServerAddress(uri: self.serverAddress)
|
||||
self.signIn(baseAddress: baseAddress)
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ struct ActionButton<Label>: View where Label: View {
|
|||
var body: some View {
|
||||
Button {
|
||||
Task {
|
||||
HapticService.shared.touch()
|
||||
HapticService.shared.fireHaptic(of: .buttonPress)
|
||||
withAnimation {
|
||||
self.isDuringAction = true
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ struct ActionMenu<Label: View, Content: View>: View {
|
|||
}
|
||||
} primaryAction: {
|
||||
Task {
|
||||
HapticService.shared.touch()
|
||||
HapticService.shared.fireHaptic(of: .buttonPress)
|
||||
defer {
|
||||
Task { @MainActor in
|
||||
withAnimation {
|
||||
|
|
|
@ -25,6 +25,7 @@ struct ImageUploadView: View {
|
|||
if photoAttachment.error != nil {
|
||||
Menu {
|
||||
Button(role: .destructive) {
|
||||
HapticService.shared.fireHaptic(of: .buttonPress)
|
||||
self.delete()
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
|
@ -54,12 +55,14 @@ struct ImageUploadView: View {
|
|||
} else {
|
||||
Menu {
|
||||
Button {
|
||||
HapticService.shared.fireHaptic(of: .buttonPress)
|
||||
self.open()
|
||||
} label: {
|
||||
Label("Edit", systemImage: "pencil")
|
||||
}
|
||||
|
||||
Button(role: .destructive) {
|
||||
HapticService.shared.fireHaptic(of: .buttonPress)
|
||||
self.delete()
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
|
|
|
@ -69,7 +69,7 @@ struct ImageRow: View {
|
|||
}
|
||||
|
||||
self.showThumbImage = true
|
||||
HapticService.shared.touch()
|
||||
HapticService.shared.fireHaptic(of: .buttonPress)
|
||||
}
|
||||
|
||||
if showThumbImage {
|
||||
|
|
|
@ -134,7 +134,7 @@ struct ImageRowAsync: View {
|
|||
}
|
||||
|
||||
self.showThumbImage = true
|
||||
HapticService.shared.touch()
|
||||
HapticService.shared.fireHaptic(of: .buttonPress)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -26,6 +26,8 @@ struct SupportView: View {
|
|||
}
|
||||
Spacer()
|
||||
Button(product.displayPrice) {
|
||||
HapticService.shared.fireHaptic(of: .buttonPress)
|
||||
|
||||
Task {
|
||||
await tipsStore.purchase(product)
|
||||
}
|
||||
|
|
|
@ -25,6 +25,8 @@ struct ThanksView: View {
|
|||
.foregroundColor(.viewTextColor)
|
||||
|
||||
Button("Close") {
|
||||
HapticService.shared.fireHaptic(of: .buttonPress)
|
||||
|
||||
withAnimation(.spring()) {
|
||||
self.routerPath.presentedOverlay = nil
|
||||
}
|
||||
|
|
|
@ -9,6 +9,8 @@ import PixelfedKit
|
|||
import NukeUI
|
||||
|
||||
struct InstanceRow: View {
|
||||
@EnvironmentObject var routerPath: RouterPath
|
||||
|
||||
private let instance: Instance
|
||||
private let action: (String) -> Void
|
||||
|
||||
|
@ -46,6 +48,7 @@ struct InstanceRow: View {
|
|||
Spacer()
|
||||
|
||||
Button("Sign in") {
|
||||
HapticService.shared.fireHaptic(of: .buttonPress)
|
||||
self.action(instance.uri)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
|
@ -53,8 +56,12 @@ struct InstanceRow: View {
|
|||
}
|
||||
}
|
||||
|
||||
Text(instance.description ?? "")
|
||||
.font(.caption)
|
||||
if let description = instance.description {
|
||||
MarkdownFormattedText(description.asMarkdown, withFontSize: 14)
|
||||
.environment(\.openURL, OpenURLAction { url in
|
||||
routerPath.handle(url: url)
|
||||
})
|
||||
}
|
||||
|
||||
if let stats = instance.stats {
|
||||
HStack {
|
||||
|
|
Loading…
Reference in New Issue