diff --git a/PixelfedKit/Sources/PixelfedKit/Entities/Instance.swift b/PixelfedKit/Sources/PixelfedKit/Entities/Instance.swift index e0e297d..404b6a1 100644 --- a/PixelfedKit/Sources/PixelfedKit/Entities/Instance.swift +++ b/PixelfedKit/Sources/PixelfedKit/Entities/Instance.swift @@ -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? diff --git a/Vernissage/Haptics/HapticService.swift b/Vernissage/Haptics/HapticService.swift index bf53c0d..7c4e94c 100644 --- a/Vernissage/Haptics/HapticService.swift +++ b/Vernissage/Haptics/HapticService.swift @@ -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 } - } diff --git a/Vernissage/VernissageApp.swift b/Vernissage/VernissageApp.swift index 1c8248e..02d36f8 100644 --- a/Vernissage/VernissageApp.swift +++ b/Vernissage/VernissageApp.swift @@ -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() diff --git a/Vernissage/Views/MainView.swift b/Vernissage/Views/MainView.swift index 3252d02..001847c 100644 --- a/Vernissage/Views/MainView.swift +++ b/Vernissage/Views/MainView.swift @@ -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() diff --git a/Vernissage/Views/PhotoEditorView.swift b/Vernissage/Views/PhotoEditorView.swift index c80d802..a45256b 100644 --- a/Vernissage/Views/PhotoEditorView.swift +++ b/Vernissage/Views/PhotoEditorView.swift @@ -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 { diff --git a/Vernissage/Views/PlaceSelectorView.swift b/Vernissage/Views/PlaceSelectorView.swift index 3f26cbb..7104280 100644 --- a/Vernissage/Views/PlaceSelectorView.swift +++ b/Vernissage/Views/PlaceSelectorView.swift @@ -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: { diff --git a/Vernissage/Views/SignInView.swift b/Vernissage/Views/SignInView.swift index f38e0e4..4f59de8 100644 --- a/Vernissage/Views/SignInView.swift +++ b/Vernissage/Views/SignInView.swift @@ -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) } diff --git a/Vernissage/Widgets/ActionButton.swift b/Vernissage/Widgets/ActionButton.swift index 49c7860..b3fe4d3 100644 --- a/Vernissage/Widgets/ActionButton.swift +++ b/Vernissage/Widgets/ActionButton.swift @@ -22,7 +22,7 @@ struct ActionButton