Change haptics.

This commit is contained in:
Marcin Czachursk 2023-02-19 12:49:44 +01:00
parent 1748f48165
commit 5aed19bf05
15 changed files with 67 additions and 67 deletions

View File

@ -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?

View File

@ -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
}
}

View File

@ -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()

View File

@ -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()

View File

@ -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 {

View File

@ -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: {

View File

@ -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)
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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")

View File

@ -69,7 +69,7 @@ struct ImageRow: View {
}
self.showThumbImage = true
HapticService.shared.touch()
HapticService.shared.fireHaptic(of: .buttonPress)
}
if showThumbImage {

View File

@ -134,7 +134,7 @@ struct ImageRowAsync: View {
}
self.showThumbImage = true
HapticService.shared.touch()
HapticService.shared.fireHaptic(of: .buttonPress)
}
}

View File

@ -26,6 +26,8 @@ struct SupportView: View {
}
Spacer()
Button(product.displayPrice) {
HapticService.shared.fireHaptic(of: .buttonPress)
Task {
await tipsStore.purchase(product)
}

View File

@ -25,6 +25,8 @@ struct ThanksView: View {
.foregroundColor(.viewTextColor)
Button("Close") {
HapticService.shared.fireHaptic(of: .buttonPress)
withAnimation(.spring()) {
self.routerPath.presentedOverlay = nil
}

View File

@ -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 {