From 4ea600403bd0895241a275e691957490a2bd26cb Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Tue, 9 Apr 2024 16:41:47 +0200 Subject: [PATCH] Move all UI*FeedbackGenerators to FeedbackGenerator and disable them for now (IOS-247) (#1267) * Move all UI*FeedbackGenerators to FeedbackGenerator and disable them for now (IOS-247) * Fix copyright header * Remove empty private constructor --- .../Provider/DataSourceFacade+Block.swift | 6 +-- .../Provider/DataSourceFacade+Bookmark.swift | 3 +- .../Provider/DataSourceFacade+Favorite.swift | 5 +-- .../Provider/DataSourceFacade+Follow.swift | 6 +-- .../Provider/DataSourceFacade+Mute.swift | 5 +-- .../Provider/DataSourceFacade+Reblog.swift | 5 +-- .../Provider/DataSourceFacade+Status.swift | 5 +-- .../Provider/DataSourceFacade+Translate.swift | 3 +- ...tatusTableViewControllerNavigateable.swift | 5 +-- ...omeTimelineViewModel+LoadLatestState.swift | 3 +- .../PickServer/CategoryPickerSection.swift | 11 ++--- ...ckServerServerSectionTableHeaderView.swift | 3 +- .../Root/MainTab/MainTabBarController.swift | 6 +-- Mastodon/Supporting Files/SceneDelegate.swift | 4 ++ .../MastodonCore/FeedbackGenerator.swift | 40 +++++++++++++++++++ .../Service/PhotoLibraryService.swift | 22 +++++----- 16 files changed, 81 insertions(+), 51 deletions(-) create mode 100644 MastodonSDK/Sources/MastodonCore/FeedbackGenerator.swift diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift index b5d59ef67..600119d75 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Block.swift @@ -15,8 +15,7 @@ extension DataSourceFacade { dependency: NeedsDependency & AuthContextProvider, account: Mastodon.Entity.Account ) async throws -> Mastodon.Entity.Relationship { - let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() - await selectionFeedbackGenerator.selectionChanged() + FeedbackGenerator.shared.generate(.selectionChanged) let apiService = dependency.context.apiService let authBox = dependency.authContext.mastodonAuthenticationBox @@ -39,8 +38,7 @@ extension DataSourceFacade { dependency: NeedsDependency & AuthContextProvider, account: Mastodon.Entity.Account ) async throws -> Mastodon.Entity.Empty { - let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() - await selectionFeedbackGenerator.selectionChanged() + FeedbackGenerator.shared.generate(.selectionChanged) let apiService = dependency.context.apiService let authBox = dependency.authContext.mastodonAuthenticationBox diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Bookmark.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Bookmark.swift index 5433eb543..0eb772adf 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Bookmark.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Bookmark.swift @@ -17,8 +17,7 @@ extension DataSourceFacade { provider: NeedsDependency & AuthContextProvider & DataSourceProvider, status: MastodonStatus ) async throws { - let selectionFeedbackGenerator = UISelectionFeedbackGenerator() - selectionFeedbackGenerator.selectionChanged() + FeedbackGenerator.shared.generate(.selectionChanged) let updatedStatus = try await provider.context.apiService.bookmark( record: status, diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Favorite.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Favorite.swift index 8e55198eb..c2e0a7931 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Favorite.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Favorite.swift @@ -16,9 +16,8 @@ extension DataSourceFacade { provider: DataSourceProvider & AuthContextProvider, status: MastodonStatus ) async throws { - let selectionFeedbackGenerator = UISelectionFeedbackGenerator() - selectionFeedbackGenerator.selectionChanged() - + FeedbackGenerator.shared.generate(.selectionChanged) + let updatedStatus = try await provider.context.apiService.favorite( status: status, authenticationBox: provider.authContext.mastodonAuthenticationBox diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift index eebe23454..e54cd7cc3 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Follow.swift @@ -25,8 +25,7 @@ extension DataSourceFacade { return try await withCheckedThrowingContinuation { continuation in Task { @MainActor in let performAction = { - let selectionFeedbackGenerator = UISelectionFeedbackGenerator() - selectionFeedbackGenerator.selectionChanged() + FeedbackGenerator.shared.generate(.selectionChanged) let response = try await dependency.context.apiService.toggleFollow( account: account, @@ -84,8 +83,7 @@ extension DataSourceFacade { notificationView: NotificationView, query: Mastodon.API.Account.FollowRequestQuery ) async throws { - let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() - await selectionFeedbackGenerator.selectionChanged() + FeedbackGenerator.shared.generate(.selectionChanged) let userID = notification.account.id let state: MastodonFollowRequestState = notification.followRequestState diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift index 11a975381..d5845cbee 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Mute.swift @@ -14,9 +14,8 @@ extension DataSourceFacade { dependency: NeedsDependency & AuthContextProvider, account: Mastodon.Entity.Account ) async throws -> Mastodon.Entity.Relationship { - let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() - await selectionFeedbackGenerator.selectionChanged() - + FeedbackGenerator.shared.generate(.selectionChanged) + let response = try await dependency.context.apiService.toggleMute( authenticationBox: dependency.authContext.mastodonAuthenticationBox, account: account diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Reblog.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Reblog.swift index f927e8f0e..a11636a5b 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Reblog.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Reblog.swift @@ -47,9 +47,8 @@ private extension DataSourceFacade { provider: DataSourceProvider & AuthContextProvider, status: MastodonStatus ) async throws { - let selectionFeedbackGenerator = UISelectionFeedbackGenerator() - selectionFeedbackGenerator.selectionChanged() - + FeedbackGenerator.shared.generate(.selectionChanged) + let updatedStatus = try await provider.context.apiService.reblog( status: status, authenticationBox: provider.authContext.mastodonAuthenticationBox diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift index 00dedb0ad..ddac7f14d 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift @@ -98,9 +98,8 @@ extension DataSourceFacade { switch action { case .reply: - let selectionFeedbackGenerator = UISelectionFeedbackGenerator() - selectionFeedbackGenerator.selectionChanged() - + FeedbackGenerator.shared.generate(.selectionChanged) + let composeViewModel = ComposeViewModel( context: provider.context, authContext: provider.authContext, diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Translate.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Translate.swift index 2523be1b4..7890dfb9e 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Translate.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Translate.swift @@ -22,8 +22,7 @@ extension DataSourceFacade { provider: Provider, status: MastodonStatus ) async throws -> Mastodon.Entity.Translation? { - let selectionFeedbackGenerator = await UISelectionFeedbackGenerator() - await selectionFeedbackGenerator.selectionChanged() + FeedbackGenerator.shared.generate(.selectionChanged) do { let value = try await provider.context diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewControllerNavigateable.swift b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewControllerNavigateable.swift index cfc3e07f3..a850be86d 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewControllerNavigateable.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewControllerNavigateable.swift @@ -85,9 +85,8 @@ extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvid private func replyStatus() async { guard let status = await statusRecord() else { return } - let selectionFeedbackGenerator = UISelectionFeedbackGenerator() - selectionFeedbackGenerator.selectionChanged() - + FeedbackGenerator.shared.generate(.selectionChanged) + let composeViewModel = ComposeViewModel( context: self.context, authContext: authContext, diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift index aca9f5f45..ae57f947f 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadLatestState.swift @@ -161,8 +161,7 @@ extension HomeTimelineViewModel.LoadLatestState { viewModel.timelineIsEmpty.value = latestStatusIDs.isEmpty && statuses.isEmpty if !isUserInitiated { - await UIImpactFeedbackGenerator(style: .light) - .impactOccurred() + FeedbackGenerator.shared.generate(.impact(.light)) } } catch { diff --git a/Mastodon/Scene/Onboarding/PickServer/CategoryPickerSection.swift b/Mastodon/Scene/Onboarding/PickServer/CategoryPickerSection.swift index 5c1d96083..26656f7b3 100644 --- a/Mastodon/Scene/Onboarding/PickServer/CategoryPickerSection.swift +++ b/Mastodon/Scene/Onboarding/PickServer/CategoryPickerSection.swift @@ -8,6 +8,7 @@ import UIKit import MastodonAsset import MastodonLocalization +import MastodonCore enum CategoryPickerSection: Equatable, Hashable { case main @@ -36,13 +37,13 @@ extension CategoryPickerSection { let allLanguagesAction = UIAction(title: L10n.Scene.ServerPicker.Language.all) { _ in viewModel.selectedLanguage.value = nil - UISelectionFeedbackGenerator().selectionChanged() + FeedbackGenerator.shared.generate(.selectionChanged) cell.titleLabel.text = L10n.Scene.ServerPicker.Button.language } let languageActions = viewModel.allLanguages.value.compactMap { language in UIAction(title: language.language ?? language.locale) { action in - UISelectionFeedbackGenerator().selectionChanged() + FeedbackGenerator.shared.generate(.selectionChanged) viewModel.selectedLanguage.value = language.locale cell.titleLabel.text = language.language } @@ -64,19 +65,19 @@ extension CategoryPickerSection { let doesntMatterAction = UIAction(title: L10n.Scene.ServerPicker.SignupSpeed.all) { _ in viewModel.manualApprovalRequired.value = nil cell.titleLabel.text = L10n.Scene.ServerPicker.Button.signupSpeed - UISelectionFeedbackGenerator().selectionChanged() + FeedbackGenerator.shared.generate(.selectionChanged) } let manualApprovalAction = UIAction(title: L10n.Scene.ServerPicker.SignupSpeed.manuallyReviewed) { action in viewModel.manualApprovalRequired.value = true cell.titleLabel.text = action.title - UISelectionFeedbackGenerator().selectionChanged() + FeedbackGenerator.shared.generate(.selectionChanged) } let instantSignupAction = UIAction(title: L10n.Scene.ServerPicker.SignupSpeed.instant) { action in viewModel.manualApprovalRequired.value = false cell.titleLabel.text = action.title - UISelectionFeedbackGenerator().selectionChanged() + FeedbackGenerator.shared.generate(.selectionChanged) } let signupSpeedMenu = UIMenu(title: L10n.Scene.ServerPicker.Button.signupSpeed, diff --git a/Mastodon/Scene/Onboarding/PickServer/View/PickServerServerSectionTableHeaderView.swift b/Mastodon/Scene/Onboarding/PickServer/View/PickServerServerSectionTableHeaderView.swift index a29a4ca74..ac5ec76c5 100644 --- a/Mastodon/Scene/Onboarding/PickServer/View/PickServerServerSectionTableHeaderView.swift +++ b/Mastodon/Scene/Onboarding/PickServer/View/PickServerServerSectionTableHeaderView.swift @@ -10,6 +10,7 @@ import Tabman import MastodonAsset import MastodonUI import MastodonLocalization +import MastodonCore protocol PickServerServerSectionTableHeaderViewDelegate: AnyObject { func pickServerServerSectionTableHeaderView(_ headerView: PickServerServerSectionTableHeaderView, collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) @@ -97,7 +98,7 @@ extension PickServerServerSectionTableHeaderView { extension PickServerServerSectionTableHeaderView: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - UISelectionFeedbackGenerator().selectionChanged() + FeedbackGenerator.shared.generate(.selectionChanged) collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .centeredHorizontally) delegate?.pickServerServerSectionTableHeaderView(self, collectionView: collectionView, didSelectItemAt: indexPath) diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index 5cd0d9d7c..f83c60608 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -47,7 +47,7 @@ class MainTabBarController: UITabBarController { @Published var avatarURL: URL? // haptic feedback - private let selectionFeedbackGenerator = UIImpactFeedbackGenerator(style: .medium) + private let feedbackGenerator = FeedbackGenerator.shared init( context: AppContext, @@ -249,7 +249,7 @@ extension MainTabBarController { @objc private func composeButtonDidPressed(_ sender: Any) { - selectionFeedbackGenerator.impactOccurred() + feedbackGenerator.generate(.impact(.medium)) guard let authContext = self.authContext else { return } let composeViewModel = ComposeViewModel( context: context, @@ -382,7 +382,7 @@ extension MainTabBarController: UITabBarControllerDelegate { // Different tab has been selected, send haptic feedback if viewController.tabBarItem.tag != tabBarController.selectedIndex { - selectionFeedbackGenerator.impactOccurred() + feedbackGenerator.generate(.impact(.medium)) } // Assert index is as same as the tab rawValue. This check needs to be done `shouldSelect` diff --git a/Mastodon/Supporting Files/SceneDelegate.swift b/Mastodon/Supporting Files/SceneDelegate.swift index 75aa27c6e..403be1f64 100644 --- a/Mastodon/Supporting Files/SceneDelegate.swift +++ b/Mastodon/Supporting Files/SceneDelegate.swift @@ -22,10 +22,14 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { var coordinator: SceneCoordinator? var savedShortCutItem: UIApplicationShortcutItem? + + let feedbackGenerator = FeedbackGenerator.shared func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let windowScene = scene as? UIWindowScene else { return } + feedbackGenerator.isEnabled = false // Disable Haptic Feedback for now + #if DEBUG let window = TouchesVisibleWindow(windowScene: windowScene) self.window = window diff --git a/MastodonSDK/Sources/MastodonCore/FeedbackGenerator.swift b/MastodonSDK/Sources/MastodonCore/FeedbackGenerator.swift new file mode 100644 index 000000000..966956584 --- /dev/null +++ b/MastodonSDK/Sources/MastodonCore/FeedbackGenerator.swift @@ -0,0 +1,40 @@ +// Copyright © 2024 Mastodon gGmbH. All rights reserved. + +import UIKit + +public class FeedbackGenerator { + + private let lightImpactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light) + private let mediumImpactFeedbackGenerator = UIImpactFeedbackGenerator(style: .medium) + private let notificationFeedbackGenerator = UINotificationFeedbackGenerator() + private let selectionFeedbackGenerator = UISelectionFeedbackGenerator() + + public enum Impact { + case light, medium + } + + public enum Feedback { + case impact(Impact) + case notification(UINotificationFeedbackGenerator.FeedbackType) + case selectionChanged + } + + public static let shared = FeedbackGenerator() + public var isEnabled = true + + public func generate(_ feedback: Feedback) { + guard isEnabled else { return } + DispatchQueue.main.async { [self] in + switch feedback { + case .impact(.light): + lightImpactFeedbackGenerator.impactOccurred() + case .impact(.medium): + mediumImpactFeedbackGenerator.impactOccurred() + case let .notification(type): + notificationFeedbackGenerator.notificationOccurred(type) + case .selectionChanged: + selectionFeedbackGenerator.selectionChanged() + } + } + } +} diff --git a/MastodonSDK/Sources/MastodonCore/Service/PhotoLibraryService.swift b/MastodonSDK/Sources/MastodonCore/Service/PhotoLibraryService.swift index a6a2f721c..0432547e3 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/PhotoLibraryService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/PhotoLibraryService.swift @@ -32,9 +32,7 @@ extension PhotoLibraryService { extension PhotoLibraryService { public func save(imageSource source: ImageSource) -> AnyPublisher { - let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light) - let notificationFeedbackGenerator = UINotificationFeedbackGenerator() - + let feedbackGenerator = FeedbackGenerator.shared let imageDataPublisher: AnyPublisher = { switch source { @@ -50,13 +48,13 @@ extension PhotoLibraryService { PhotoLibraryService.save(imageData: data) } .handleEvents(receiveSubscription: { _ in - impactFeedbackGenerator.impactOccurred() + feedbackGenerator.generate(.impact(.light)) }, receiveCompletion: { completion in switch completion { case .failure: - notificationFeedbackGenerator.notificationOccurred(.error) + feedbackGenerator.generate(.notification(.error)) case .finished: - notificationFeedbackGenerator.notificationOccurred(.success) + feedbackGenerator.generate(.notification(.success)) } }) .eraseToAnyPublisher() @@ -67,10 +65,8 @@ extension PhotoLibraryService { extension PhotoLibraryService { public func copy(imageSource source: ImageSource) -> AnyPublisher { - - let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light) - let notificationFeedbackGenerator = UINotificationFeedbackGenerator() - + let feedbackGenerator = FeedbackGenerator.shared + let imageDataPublisher: AnyPublisher = { switch source { case .url(let url): @@ -85,13 +81,13 @@ extension PhotoLibraryService { PhotoLibraryService.copy(imageData: data) } .handleEvents(receiveSubscription: { _ in - impactFeedbackGenerator.impactOccurred() + feedbackGenerator.generate(.impact(.light)) }, receiveCompletion: { completion in switch completion { case .failure: - notificationFeedbackGenerator.notificationOccurred(.error) + feedbackGenerator.generate(.notification(.error)) case .finished: - notificationFeedbackGenerator.notificationOccurred(.success) + feedbackGenerator.generate(.notification(.success)) } }) .eraseToAnyPublisher()