Centralize haptic feedbacks

This commit is contained in:
Thomas Ricouard 2023-02-02 16:56:42 +01:00
parent 262f7288ad
commit bb6910cd83
10 changed files with 47 additions and 27 deletions

View file

@ -29,8 +29,6 @@ struct IceCubesApp: App {
@State private var popToRootTab: Tab = .other @State private var popToRootTab: Tab = .other
@State private var sideBarLoadedTabs: Set<Tab> = Set() @State private var sideBarLoadedTabs: Set<Tab> = Set()
private let feedbackGenerator = UISelectionFeedbackGenerator()
private var availableTabs: [Tab] { private var availableTabs: [Tab] {
appAccountsManager.currentClient.isAuth ? Tab.loggedInTabs() : Tab.loggedOutTab() appAccountsManager.currentClient.isAuth ? Tab.loggedInTabs() : Tab.loggedOutTab()
} }
@ -142,7 +140,7 @@ struct IceCubesApp: App {
} }
} }
selectedTab = newTab selectedTab = newTab
feedbackGenerator.selectionChanged() HapticManager.shared.selectionChanged()
})) { })) {
ForEach(availableTabs) { tab in ForEach(availableTabs) { tab in
tab.makeContentView(popToRootTab: $popToRootTab) tab.makeContentView(popToRootTab: $popToRootTab)

View file

@ -10,8 +10,6 @@ public struct AppAccountsSelectorView: View {
@State private var accountsViewModel: [AppAccountViewModel] = [] @State private var accountsViewModel: [AppAccountViewModel] = []
let feedbackGenerator = UIImpactFeedbackGenerator()
private let accountCreationEnabled: Bool private let accountCreationEnabled: Bool
private let avatarSize: AvatarView.Size private let avatarSize: AvatarView.Size
@ -22,8 +20,6 @@ public struct AppAccountsSelectorView: View {
self.routerPath = routerPath self.routerPath = routerPath
self.accountCreationEnabled = accountCreationEnabled self.accountCreationEnabled = accountCreationEnabled
self.avatarSize = avatarSize self.avatarSize = avatarSize
feedbackGenerator.prepare()
} }
public var body: some View { public var body: some View {
@ -42,7 +38,7 @@ public struct AppAccountsSelectorView: View {
} }
} }
.onTapGesture { .onTapGesture {
feedbackGenerator.impactOccurred(intensity: 0.3) HapticManager.shared.impact()
} }
.onAppear { .onAppear {
refreshAccounts() refreshAccounts()
@ -82,7 +78,7 @@ public struct AppAccountsSelectorView: View {
appAccounts.currentAccount = viewModel.appAccount appAccounts.currentAccount = viewModel.appAccount
} }
feedbackGenerator.impactOccurred(intensity: 0.7) HapticManager.shared.impact()
} label: { } label: {
HStack { HStack {
if viewModel.account?.id == currentAccount.account?.id { if viewModel.account?.id == currentAccount.account?.id {
@ -96,6 +92,7 @@ public struct AppAccountsSelectorView: View {
if accountCreationEnabled { if accountCreationEnabled {
Divider() Divider()
Button { Button {
HapticManager.shared.impact()
routerPath.presentedSheet = .addAccount routerPath.presentedSheet = .addAccount
} label: { } label: {
Label("app-account.button.add", systemImage: "person.badge.plus") Label("app-account.button.add", systemImage: "person.badge.plus")
@ -105,6 +102,7 @@ public struct AppAccountsSelectorView: View {
if UIDevice.current.userInterfaceIdiom == .phone { if UIDevice.current.userInterfaceIdiom == .phone {
Divider() Divider()
Button { Button {
HapticManager.shared.impact()
routerPath.presentedSheet = .settings routerPath.presentedSheet = .settings
} label: { } label: {
Label("tab.settings", systemImage: "gear") Label("tab.settings", systemImage: "gear")

View file

@ -7,9 +7,8 @@ public extension View {
func statusEditorToolbarItem(routerPath: RouterPath, visibility: Models.Visibility) -> some ToolbarContent { func statusEditorToolbarItem(routerPath: RouterPath, visibility: Models.Visibility) -> some ToolbarContent {
ToolbarItem(placement: .navigationBarTrailing) { ToolbarItem(placement: .navigationBarTrailing) {
Button { Button {
let feedback = UISelectionFeedbackGenerator()
routerPath.presentedSheet = .newStatusEditor(visibility: visibility) routerPath.presentedSheet = .newStatusEditor(visibility: visibility)
feedback.selectionChanged() HapticManager.shared.impact()
} label: { } label: {
Image(systemName: "square.and.pencil") Image(systemName: "square.and.pencil")
} }
@ -21,7 +20,6 @@ public struct StatusEditorToolbarItem: ToolbarContent {
@EnvironmentObject private var routerPath: RouterPath @EnvironmentObject private var routerPath: RouterPath
let visibility: Models.Visibility let visibility: Models.Visibility
let feedbackGenerator = UISelectionFeedbackGenerator()
public init(visibility: Models.Visibility) { public init(visibility: Models.Visibility) {
self.visibility = visibility self.visibility = visibility
@ -31,7 +29,7 @@ public struct StatusEditorToolbarItem: ToolbarContent {
ToolbarItem(placement: .navigationBarTrailing) { ToolbarItem(placement: .navigationBarTrailing) {
Button { Button {
routerPath.presentedSheet = .newStatusEditor(visibility: visibility) routerPath.presentedSheet = .newStatusEditor(visibility: visibility)
feedbackGenerator.selectionChanged() HapticManager.shared.impact()
} label: { } label: {
Image(systemName: "square.and.pencil") Image(systemName: "square.and.pencil")
} }

View file

@ -0,0 +1,30 @@
import UIKit
public class HapticManager {
public static let shared: HapticManager = .init()
private let selectionGenerator = UISelectionFeedbackGenerator()
private let impactGenerator = UIImpactFeedbackGenerator(style: .heavy)
private let notificationGenerator = UINotificationFeedbackGenerator()
private init() {
selectionGenerator.prepare()
impactGenerator.prepare()
}
public func selectionChanged(){
selectionGenerator.selectionChanged()
}
public func impact() {
impactGenerator.impactOccurred()
}
public func impact(intensity: CGFloat) {
impactGenerator.impactOccurred(intensity: intensity)
}
public func notification(type: UINotificationFeedbackGenerator.FeedbackType){
notificationGenerator.notificationOccurred(type)
}
}

View file

@ -8,7 +8,6 @@ import SwiftUI
@MainActor @MainActor
public class StatusEditorViewModel: ObservableObject { public class StatusEditorViewModel: ObservableObject {
var mode: Mode var mode: Mode
let generator = UINotificationFeedbackGenerator()
var client: Client? var client: Client?
var currentAccount: Account? var currentAccount: Account?
@ -140,7 +139,7 @@ public class StatusEditorViewModel: ObservableObject {
case let .edit(status): case let .edit(status):
postStatus = try await client.put(endpoint: Statuses.editStatus(id: status.id, json: data)) postStatus = try await client.put(endpoint: Statuses.editStatus(id: status.id, json: data))
} }
generator.notificationOccurred(.success) HapticManager.shared.notification(type: .success)
if hasExplicitlySelectedLanguage, let selectedLanguage { if hasExplicitlySelectedLanguage, let selectedLanguage {
preferences?.markLanguageAsSelected(isoCode: selectedLanguage) preferences?.markLanguageAsSelected(isoCode: selectedLanguage)
} }
@ -152,7 +151,7 @@ public class StatusEditorViewModel: ObservableObject {
showPostingErrorAlert = true showPostingErrorAlert = true
} }
isPosting = false isPosting = false
generator.notificationOccurred(.error) HapticManager.shared.notification(type: .error)
return nil return nil
} }
} }

View file

@ -10,8 +10,6 @@ struct StatusActionsView: View {
@EnvironmentObject private var routerPath: RouterPath @EnvironmentObject private var routerPath: RouterPath
@ObservedObject var viewModel: StatusRowViewModel @ObservedObject var viewModel: StatusRowViewModel
let generator = UINotificationFeedbackGenerator()
@MainActor @MainActor
enum Actions: CaseIterable { enum Actions: CaseIterable {
case respond, boost, favorite, bookmark, share case respond, boost, favorite, bookmark, share
@ -158,7 +156,7 @@ struct StatusActionsView: View {
private func handleAction(action: Actions) { private func handleAction(action: Actions) {
Task { Task {
generator.notificationOccurred(.success) HapticManager.shared.notification(type: .success)
switch action { switch action {
case .respond: case .respond:
routerPath.presentedSheet = .replyToStatusEditor(status: viewModel.status) routerPath.presentedSheet = .replyToStatusEditor(status: viewModel.status)

View file

@ -422,6 +422,7 @@ public struct StatusRowView: View {
private var trailinSwipeActions: some View { private var trailinSwipeActions: some View {
Button { Button {
Task { Task {
HapticManager.shared.notification(type: .success)
if viewModel.isFavorited { if viewModel.isFavorited {
await viewModel.unFavorite() await viewModel.unFavorite()
} else { } else {
@ -434,6 +435,7 @@ public struct StatusRowView: View {
.tint(.yellow) .tint(.yellow)
Button { Button {
Task { Task {
HapticManager.shared.notification(type: .success)
if viewModel.isReblogged { if viewModel.isReblogged {
await viewModel.unReblog() await viewModel.unReblog()
} else { } else {
@ -449,6 +451,7 @@ public struct StatusRowView: View {
@ViewBuilder @ViewBuilder
private var leadingSwipeActions: some View { private var leadingSwipeActions: some View {
Button { Button {
HapticManager.shared.notification(type: .success)
routerPath.presentedSheet = .replyToStatusEditor(status: viewModel.status) routerPath.presentedSheet = .replyToStatusEditor(status: viewModel.status)
} label: { } label: {
Image(systemName: "arrowshape.turn.up.left") Image(systemName: "arrowshape.turn.up.left")

View file

@ -1,11 +1,10 @@
import Foundation import Foundation
import Models import Models
import SwiftUI import SwiftUI
import Env
@MainActor @MainActor
class PendingStatusesObserver: ObservableObject { class PendingStatusesObserver: ObservableObject {
let feedbackGenerator = UIImpactFeedbackGenerator(style: .light)
@Published var pendingStatusesCount: Int = 0 @Published var pendingStatusesCount: Int = 0
var disableUpdate: Bool = false var disableUpdate: Bool = false
@ -19,7 +18,7 @@ class PendingStatusesObserver: ObservableObject {
func removeStatus(status: Status) { func removeStatus(status: Status) {
if !disableUpdate, let index = pendingStatuses.firstIndex(of: status.id) { if !disableUpdate, let index = pendingStatuses.firstIndex(of: status.id) {
pendingStatuses.removeSubrange(index ... (pendingStatuses.count - 1)) pendingStatuses.removeSubrange(index ... (pendingStatuses.count - 1))
feedbackGenerator.impactOccurred() HapticManager.shared.selectionChanged()
} }
} }

View file

@ -25,8 +25,6 @@ public struct TimelineView: View {
@Binding var timeline: TimelineFilter @Binding var timeline: TimelineFilter
@Binding var scrollToTopSignal: Int @Binding var scrollToTopSignal: Int
private let feedbackGenerator = UIImpactFeedbackGenerator()
public init(timeline: Binding<TimelineFilter>, scrollToTopSignal: Binding<Int>) { public init(timeline: Binding<TimelineFilter>, scrollToTopSignal: Binding<Int>) {
_timeline = timeline _timeline = timeline
_scrollToTopSignal = scrollToTopSignal _scrollToTopSignal = scrollToTopSignal
@ -82,9 +80,9 @@ public struct TimelineView: View {
} }
} }
.refreshable { .refreshable {
feedbackGenerator.impactOccurred(intensity: 0.3) HapticManager.shared.impact(intensity: 0.3)
await viewModel.fetchStatuses() await viewModel.fetchStatuses()
feedbackGenerator.impactOccurred(intensity: 0.7) HapticManager.shared.impact(intensity: 0.7)
} }
.onChange(of: watcher.latestEvent?.id) { _ in .onChange(of: watcher.latestEvent?.id) { _ in
if let latestEvent = watcher.latestEvent { if let latestEvent = watcher.latestEvent {

View file

@ -212,7 +212,6 @@ extension TimelineViewModel: StatusesFetcher {
} else { } else {
// Append new statuses in the timeline indicator. // Append new statuses in the timeline indicator.
pendingStatusesObserver.pendingStatuses.insert(contentsOf: newStatuses.map { $0.id }, at: 0) pendingStatusesObserver.pendingStatuses.insert(contentsOf: newStatuses.map { $0.id }, at: 0)
pendingStatusesObserver.feedbackGenerator.impactOccurred()
// High chance the user is scrolled to the top. // High chance the user is scrolled to the top.
// We need to update the statuses state, and then scroll to the previous top most status. // We need to update the statuses state, and then scroll to the previous top most status.