mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-11-26 18:21:00 +00:00
Centralize haptic feedbacks
This commit is contained in:
parent
262f7288ad
commit
bb6910cd83
10 changed files with 47 additions and 27 deletions
|
@ -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)
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
30
Packages/Env/Sources/Env/HapticManager.swift
Normal file
30
Packages/Env/Sources/Env/HapticManager.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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.
|
||||||
|
|
Loading…
Reference in a new issue