diff options
| author | Fuwn <[email protected]> | 2026-04-01 13:59:52 +0000 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-04-01 13:59:52 +0000 |
| commit | ddaa9cbfe0cc8d254d4901ad192918d91dd627d4 (patch) | |
| tree | b0d1cf6642ae97ce41ff570f2bcd4a06b359685e | |
| parent | Fix iOS install recipe app path resolution (diff) | |
| download | sora-testing-ddaa9cbfe0cc8d254d4901ad192918d91dd627d4.tar.xz sora-testing-ddaa9cbfe0cc8d254d4901ad192918d91dd627d4.zip | |
Migrate settings manager to Swift Observation
32 files changed, 412 insertions, 198 deletions
diff --git a/Sora/App/SoraApp.swift b/Sora/App/SoraApp.swift index 8fdfb4d..77a50c7 100644 --- a/Sora/App/SoraApp.swift +++ b/Sora/App/SoraApp.swift @@ -12,18 +12,18 @@ func debugPrint( @main struct SoraApp: App { - @StateObject private var settings = SettingsManager() + @State private var settings = SettingsManager() @ViewBuilder private func settingsContent() -> some View { SettingsView() - .environmentObject(settings) + .environment(settings) } var body: some Scene { WindowGroup { MainView() - .environmentObject(settings) + .environment(settings) } #if os(macOS) diff --git a/Sora/Data/Booru/Post/BooruPost.swift b/Sora/Data/Booru/Post/BooruPost.swift index 458faa6..fb8ab08 100644 --- a/Sora/Data/Booru/Post/BooruPost.swift +++ b/Sora/Data/Booru/Post/BooruPost.swift @@ -1,6 +1,6 @@ import Foundation -struct BooruPost: Identifiable, Hashable { +struct BooruPost: Identifiable, Hashable, Sendable { let id: String let height: Int let score: String diff --git a/Sora/Data/Settings/SettingsManager.swift b/Sora/Data/Settings/SettingsManager.swift index f59db1a..32a1b86 100644 --- a/Sora/Data/Settings/SettingsManager.swift +++ b/Sora/Data/Settings/SettingsManager.swift @@ -1,95 +1,158 @@ // swiftlint:disable file_length +import Observation @preconcurrency import SwiftUI @MainActor -class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_length +@Observable +class SettingsManager { // swiftlint:disable:this type_body_length + private enum StorageKey { + static let detailViewQuality = "detailViewQuality" + static let thumbnailQuality = "thumbnailQuality" + static let searchSuggestionsMode = "searchSuggestionsMode" + static let thumbnailGridColumns = "thumbnailGridColumns" + static let enableShareShortcut = "enableShareShortcut" + static let displayDetailsInformationBar = "displayDetailsInformationBar" + static let preloadedCarouselImages = "preloadedCarouselImages" + static let enableSync = "enableSync" + static let alternativeThumbnailGrid = "alternativeThumbnailGrid" + static let uniformThumbnailGrid = "uniformThumbnailGrid" + static let showHeldMoebooruPosts = "showHeldMoebooruPosts" + static let sendBooruUserAgent = "sendBooruUserAgent" + static let customBooruUserAgent = "customBooruUserAgent" + static let saveTagsToFile = "saveTagsToFile" + static let bookmarks = "bookmarks" + static let favorites = "favorites" + static let displayRatings = "displayRatings" + static let blurRatings = "blurRatings" + static let searchHistory = "searchHistory" + static let preferredBooru = "preferredBooru" + static let customProviders = "customProviders" + static let folders = "folders" + static let providerCredentials = "providerCredentials" + } + + @ObservationIgnored private let userDefaults: UserDefaults + // MARK: - Stored Properties - @AppStorage("detailViewQuality") - var detailViewQuality: BooruPostFileType = .original + var detailViewQuality: BooruPostFileType { + didSet { userDefaults.set(detailViewQuality.rawValue, forKey: StorageKey.detailViewQuality) } + } - @AppStorage("thumbnailQuality") - private var _thumbnailQuality: BooruPostFileType = .preview + var thumbnailQuality: BooruPostFileType { + didSet { userDefaults.set(thumbnailQuality.rawValue, forKey: StorageKey.thumbnailQuality) } + } - @AppStorage("searchSuggestionsMode") - var searchSuggestionsMode: SettingsSearchSuggestionsMode = .disabled + var searchSuggestionsMode: SettingsSearchSuggestionsMode { + didSet { + userDefaults.set( + searchSuggestionsMode.rawValue, + forKey: StorageKey.searchSuggestionsMode + ) + } + } - @AppStorage("thumbnailGridColumns") - var thumbnailGridColumns = 2 + var thumbnailGridColumns: Int { + didSet { userDefaults.set(thumbnailGridColumns, forKey: StorageKey.thumbnailGridColumns) } + } - @AppStorage("enableShareShortcut") - var enableShareShortcut = false + var enableShareShortcut: Bool { + didSet { userDefaults.set(enableShareShortcut, forKey: StorageKey.enableShareShortcut) } + } - @AppStorage("displayDetailsInformationBar") - var displayDetailsInformationBar = true + var displayDetailsInformationBar: Bool { + didSet { + userDefaults.set( + displayDetailsInformationBar, + forKey: StorageKey.displayDetailsInformationBar + ) + } + } - @AppStorage("preloadedCarouselImages") - var preloadedCarouselImages = 3 + var preloadedCarouselImages: Int { + didSet { userDefaults.set(preloadedCarouselImages, forKey: StorageKey.preloadedCarouselImages) } + } - @AppStorage("enableSync") - var enableSync: Bool = false + var enableSync: Bool { + didSet { userDefaults.set(enableSync, forKey: StorageKey.enableSync) } + } - @AppStorage("alternativeThumbnailGrid") - var alternativeThumbnailGrid = false + var alternativeThumbnailGrid: Bool { + didSet { + userDefaults.set(alternativeThumbnailGrid, forKey: StorageKey.alternativeThumbnailGrid) + } + } - @AppStorage("uniformThumbnailGrid") - private var _uniformThumbnailGrid: Bool = false + var uniformThumbnailGrid: Bool { + didSet { userDefaults.set(uniformThumbnailGrid, forKey: StorageKey.uniformThumbnailGrid) } + } - @AppStorage("showHeldMoebooruPosts") - var showHeldMoebooruPosts = false + var showHeldMoebooruPosts: Bool { + didSet { userDefaults.set(showHeldMoebooruPosts, forKey: StorageKey.showHeldMoebooruPosts) } + } - @AppStorage("sendBooruUserAgent") - var sendBooruUserAgent = true + var sendBooruUserAgent: Bool { + didSet { userDefaults.set(sendBooruUserAgent, forKey: StorageKey.sendBooruUserAgent) } + } - @AppStorage("customBooruUserAgent") - var customBooruUserAgent = "" + var customBooruUserAgent: String { + didSet { userDefaults.set(customBooruUserAgent, forKey: StorageKey.customBooruUserAgent) } + } - private var syncObservation: NSObjectProtocol? + @ObservationIgnored private var syncObservation: NSObjectProtocol? #if os(macOS) - @AppStorage("saveTagsToFile") - var saveTagsToFile = false + var saveTagsToFile: Bool { + didSet { userDefaults.set(saveTagsToFile, forKey: StorageKey.saveTagsToFile) } + } #endif // MARK: - Private Properties - private var bookmarksCache: [SettingsBookmark] = [] - private var favoritesCache: [SettingsFavoritePost] = [] - private var searchHistoryCache: [BooruSearchQuery] = [] - private var blurRatingsCache: [BooruRating] = [] - private var displayRatingsCache: [BooruRating] = [] - private var uniformThumbnailGridCache: Bool = false - private var thumbnailQualityCache: BooruPostFileType = .preview - private var isUpdatingCache = false - private var pendingSyncKeys: Set<SettingsSyncKey> = [] - private let syncCoordinator = SettingsSyncCoordinator() + private var bookmarksCache: [SettingsBookmark] + private var favoritesCache: [SettingsFavoritePost] + private var searchHistoryCache: [BooruSearchQuery] + private var blurRatingsCache: [BooruRating] + private var displayRatingsCache: [BooruRating] + @ObservationIgnored private var isUpdatingCache = false + @ObservationIgnored private var pendingSyncKeys: Set<SettingsSyncKey> = [] + @ObservationIgnored private let syncCoordinator = SettingsSyncCoordinator() // MARK: - Codable Properties - @AppStorage("bookmarks") - private var bookmarksData = Data() + private var bookmarksData: Data { + didSet { userDefaults.set(bookmarksData, forKey: StorageKey.bookmarks) } + } - @AppStorage("favorites") - private var favoritesData = Data() + private var favoritesData: Data { + didSet { userDefaults.set(favoritesData, forKey: StorageKey.favorites) } + } - @AppStorage("displayRatings") - private var displayRatingsData = SettingsManager.encode(BooruRating.allCases) ?? Data() + private var displayRatingsData: Data { + didSet { userDefaults.set(displayRatingsData, forKey: StorageKey.displayRatings) } + } - @AppStorage("blurRatings") - private var blurRatingsData = SettingsManager.encode([.explicit as BooruRating]) ?? Data() + private var blurRatingsData: Data { + didSet { userDefaults.set(blurRatingsData, forKey: StorageKey.blurRatings) } + } - @AppStorage("searchHistory") - private var searchHistoryData = Data() + private var searchHistoryData: Data { + didSet { userDefaults.set(searchHistoryData, forKey: StorageKey.searchHistory) } + } - @AppStorage("preferredBooru") - private var preferredBooruData = Data() + private var preferredBooruData: Data { + didSet { userDefaults.set(preferredBooruData, forKey: StorageKey.preferredBooru) } + } - @AppStorage("customProviders") - private var customProvidersData = Data() + private var customProvidersData: Data { + didSet { userDefaults.set(customProvidersData, forKey: StorageKey.customProviders) } + } - @AppStorage("folders") - private var foldersData = Data() + private var foldersData: Data { + didSet { userDefaults.set(foldersData, forKey: StorageKey.folders) } + } - @AppStorage("providerCredentials") - private var providerCredentialsData = Data() + private var providerCredentialsData: Data { + didSet { userDefaults.set(providerCredentialsData, forKey: StorageKey.providerCredentials) } + } // MARK: - Computed Properties var bookmarks: [SettingsBookmark] { @@ -107,7 +170,7 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l syncableData( key: "bookmarks", - localData: $bookmarksData, + localData: storageBinding(for: \.bookmarksData), newValue: sortedBookmarks, encodedData: payload?.encodedData ) { $0 } @@ -134,7 +197,7 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l syncableData( key: "favorites", - localData: $favoritesData, + localData: storageBinding(for: \.favoritesData), newValue: sortedFavorites, encodedData: payload?.encodedData ) { $0 } @@ -146,24 +209,6 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l } } - var uniformThumbnailGrid: Bool { - get { uniformThumbnailGridCache } - - set { - _uniformThumbnailGrid = newValue - uniformThumbnailGridCache = newValue - } - } - - var thumbnailQuality: BooruPostFileType { - get { thumbnailQualityCache } - - set { - _thumbnailQuality = newValue - thumbnailQualityCache = newValue - } - } - var displayRatings: [BooruRating] { get { displayRatingsCache } @@ -209,7 +254,7 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l syncableData( key: "searchHistory", - localData: $searchHistoryData, + localData: storageBinding(for: \.searchHistoryData), newValue: sortedSearchHistory, encodedData: payload?.encodedData ) { $0 } @@ -248,7 +293,7 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l syncableData( key: "customProviders", - localData: $customProvidersData, + localData: storageBinding(for: \.customProvidersData), newValue: newValue, ) { $0 } pendingSyncKeys.insert(.customProviders) @@ -275,7 +320,7 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l syncableData( key: "folders", - localData: $foldersData, + localData: storageBinding(for: \.foldersData), newValue: newValue, ) { $0 } } @@ -315,7 +360,7 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l syncableData( key: "providerAPIKeys", - localData: $providerCredentialsData, + localData: storageBinding(for: \.providerCredentialsData), newValue: mergedCredentials, ) { $0 } } @@ -361,7 +406,97 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l } // MARK: - Initialisation - init() { + init(userDefaults: UserDefaults = .standard) { + self.userDefaults = userDefaults + detailViewQuality = Self.stringValue( + forKey: StorageKey.detailViewQuality, + defaultValue: .original, + userDefaults: userDefaults + ) + thumbnailQuality = Self.stringValue( + forKey: StorageKey.thumbnailQuality, + defaultValue: .preview, + userDefaults: userDefaults + ) + searchSuggestionsMode = Self.stringValue( + forKey: StorageKey.searchSuggestionsMode, + defaultValue: .disabled, + userDefaults: userDefaults + ) + thumbnailGridColumns = Self.integerValue( + forKey: StorageKey.thumbnailGridColumns, + defaultValue: 2, + userDefaults: userDefaults + ) + enableShareShortcut = Self.boolValue( + forKey: StorageKey.enableShareShortcut, + defaultValue: false, + userDefaults: userDefaults + ) + displayDetailsInformationBar = Self.boolValue( + forKey: StorageKey.displayDetailsInformationBar, + defaultValue: true, + userDefaults: userDefaults + ) + preloadedCarouselImages = Self.integerValue( + forKey: StorageKey.preloadedCarouselImages, + defaultValue: 3, + userDefaults: userDefaults + ) + enableSync = Self.boolValue( + forKey: StorageKey.enableSync, + defaultValue: false, + userDefaults: userDefaults + ) + alternativeThumbnailGrid = Self.boolValue( + forKey: StorageKey.alternativeThumbnailGrid, + defaultValue: false, + userDefaults: userDefaults + ) + uniformThumbnailGrid = Self.boolValue( + forKey: StorageKey.uniformThumbnailGrid, + defaultValue: false, + userDefaults: userDefaults + ) + showHeldMoebooruPosts = Self.boolValue( + forKey: StorageKey.showHeldMoebooruPosts, + defaultValue: false, + userDefaults: userDefaults + ) + sendBooruUserAgent = Self.boolValue( + forKey: StorageKey.sendBooruUserAgent, + defaultValue: true, + userDefaults: userDefaults + ) + customBooruUserAgent = userDefaults.string(forKey: StorageKey.customBooruUserAgent) ?? "" + bookmarksCache = [] + favoritesCache = [] + searchHistoryCache = [] + blurRatingsCache = [] + displayRatingsCache = [] + bookmarksData = userDefaults.data(forKey: StorageKey.bookmarks) ?? Data() + favoritesData = userDefaults.data(forKey: StorageKey.favorites) ?? Data() + displayRatingsData = + userDefaults.data(forKey: StorageKey.displayRatings) + ?? Self.encode(BooruRating.allCases) + ?? Data() + blurRatingsData = + userDefaults.data(forKey: StorageKey.blurRatings) + ?? Self.encode([.explicit as BooruRating]) + ?? Data() + searchHistoryData = userDefaults.data(forKey: StorageKey.searchHistory) ?? Data() + preferredBooruData = userDefaults.data(forKey: StorageKey.preferredBooru) ?? Data() + customProvidersData = userDefaults.data(forKey: StorageKey.customProviders) ?? Data() + foldersData = userDefaults.data(forKey: StorageKey.folders) ?? Data() + providerCredentialsData = userDefaults.data(forKey: StorageKey.providerCredentials) ?? Data() + #if os(macOS) + saveTagsToFile = Self.boolValue( + forKey: StorageKey.saveTagsToFile, + defaultValue: false, + userDefaults: userDefaults + ) + #endif + syncObservation = NotificationCenter.default.addObserver( forName: NSUbiquitousKeyValueStore.didChangeExternallyNotification, object: NSUbiquitousKeyValueStore.default, @@ -377,9 +512,6 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l loadSearchHistoryCache() loadDisplayRatingsCache() loadBlurRatingsCache() - - uniformThumbnailGridCache = _uniformThumbnailGrid - thumbnailQualityCache = _thumbnailQuality } func updateBookmarks(_ newValue: [SettingsBookmark], encodedData: Data? = nil) async { @@ -394,7 +526,7 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l syncableData( key: "bookmarks", - localData: $bookmarksData, + localData: storageBinding(for: \.bookmarksData), newValue: sortedBookmarks, encodedData: resolvedEncodedData ) { $0 } @@ -418,7 +550,7 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l syncableData( key: "favorites", - localData: $favoritesData, + localData: storageBinding(for: \.favoritesData), newValue: sortedFavorites, encodedData: resolvedEncodedData ) { $0 } @@ -442,7 +574,7 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l syncableData( key: "searchHistory", - localData: $searchHistoryData, + localData: storageBinding(for: \.searchHistoryData), newValue: sortedSearchHistory, encodedData: payload?.encodedData ) { $0 } @@ -498,20 +630,62 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l syncableData( key: "providerAPIKeys", - localData: $providerCredentialsData, + localData: storageBinding(for: \.providerCredentialsData), newValue: mergedCredentials, ) { $0 } } // MARK: - Private Helpers + @MainActor private static func encode<T: Encodable>(_ value: T) -> Data? { SettingsCodec.encode(value) } + @MainActor private static func decode<T: Decodable>(_ type: T.Type, from data: Data) -> T? { SettingsCodec.decode(type, from: data) } + @MainActor + private static func boolValue( + forKey key: String, + defaultValue: Bool, + userDefaults: UserDefaults + ) -> Bool { + (userDefaults.object(forKey: key) as? Bool) ?? defaultValue + } + + @MainActor + private static func integerValue( + forKey key: String, + defaultValue: Int, + userDefaults: UserDefaults + ) -> Int { + (userDefaults.object(forKey: key) as? Int) ?? defaultValue + } + + @MainActor + private static func stringValue<Value: RawRepresentable>( + forKey key: String, + defaultValue: Value, + userDefaults: UserDefaults + ) -> Value where Value.RawValue == String { + guard let rawValue = userDefaults.string(forKey: key) else { + return defaultValue + } + + return Value(rawValue: rawValue) ?? defaultValue + } + + private func storageBinding(for keyPath: ReferenceWritableKeyPath<SettingsManager, Data>) + -> Binding<Data> + { + Binding( + get: { self[keyPath: keyPath] }, + set: { self[keyPath: keyPath] = $0 } + ) + } + private func syncableData<T: Codable>( key: String, localData: Data, @@ -584,18 +758,14 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l } private func performBatchedSync(for keys: Set<SettingsSyncKey>) { - var didChange = false - for key in keys { - didChange = triggerSyncIfNeeded(for: key) || didChange - } - - if didChange { - objectWillChange.send() + _ = triggerSyncIfNeeded(for: key) } } private func backupBookmarks() async { + let backupData = Self.encode(bookmarksCache) + await Task.detached { guard let cachesDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask) @@ -610,7 +780,7 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l let timestamp = Int(Date().timeIntervalSince1970) let backupFile = backupDirectory.appendingPathComponent("bookmarks_backup_\(timestamp).json") - if let data = await Self.encode(self.bookmarksCache) { + if let data = backupData { try? data.write(to: backupFile) } @@ -645,6 +815,8 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l } private func backupFavorites() async { + let backupData = Self.encode(favoritesCache) + await Task.detached { guard let cachesDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask) @@ -659,7 +831,7 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l let timestamp = Int(Date().timeIntervalSince1970) let backupFile = backupDirectory.appendingPathComponent("favorites_backup_\(timestamp).json") - if let data = await Self.encode(self.favoritesCache) { + if let data = backupData { try? data.write(to: backupFile) } @@ -929,47 +1101,33 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l } func syncFromCloud() { - if self.enableSync { - Task.detached { [weak self] in + if enableSync { + Task { [weak self] in guard let self else { return } if let data = NSUbiquitousKeyValueStore.default.data(forKey: "bookmarks") { - await MainActor.run { - self.bookmarksData = data - } + bookmarksData = data } if let data = NSUbiquitousKeyValueStore.default.data(forKey: "searchHistory") { - await MainActor.run { - self.searchHistoryData = data - } + searchHistoryData = data } if let data = NSUbiquitousKeyValueStore.default.data(forKey: "customProviders") { - await MainActor.run { - self.customProvidersData = data - } + customProvidersData = data } - await MainActor.run { - self.loadBookmarksCache() - self.loadSearchHistoryCache() - self.objectWillChange.send() - } + loadBookmarksCache() + loadSearchHistoryCache() } } } func triggerSyncIfNeededForAll() { let keysToSync: [SettingsSyncKey] = [.bookmarks, .favorites, .searchHistory, .customProviders] - var didChange = false for keyToSync in keysToSync { - didChange = triggerSyncIfNeeded(for: keyToSync) || didChange - } - - if didChange { - objectWillChange.send() + _ = triggerSyncIfNeeded(for: keyToSync) } } @@ -1238,7 +1396,7 @@ class SettingsManager: ObservableObject { // swiftlint:disable:this type_body_l syncableData( key: "folders", - localData: $foldersData, + localData: storageBinding(for: \.foldersData), newValue: updated, ) { $0 } } diff --git a/Sora/Views/BookmarkMenuButtonView.swift b/Sora/Views/BookmarkMenuButtonView.swift index 8446539..6cd6812 100644 --- a/Sora/Views/BookmarkMenuButtonView.swift +++ b/Sora/Views/BookmarkMenuButtonView.swift @@ -1,7 +1,8 @@ import SwiftUI struct BookmarkMenuButtonView: View { - @EnvironmentObject var settings: SettingsManager + @Environment(SettingsManager.self) + private var settings let tags: [String] let provider: BooruProvider var disableNewCollection = false diff --git a/Sora/Views/BookmarksView.swift b/Sora/Views/BookmarksView.swift index a20d9f0..bda4136 100644 --- a/Sora/Views/BookmarksView.swift +++ b/Sora/Views/BookmarksView.swift @@ -1,7 +1,8 @@ import SwiftUI struct BookmarksView: View { - @EnvironmentObject var settings: SettingsManager + @Environment(SettingsManager.self) + private var settings @Binding var selectedTab: Int var body: some View { @@ -26,6 +27,6 @@ struct BookmarksView: View { #Preview { BookmarksView(selectedTab: .constant(1)) - .environmentObject(SettingsManager()) + .environment(SettingsManager()) .environmentObject(BooruManager(.yandere)) } diff --git a/Sora/Views/FavoriteMenuButtonView.swift b/Sora/Views/FavoriteMenuButtonView.swift index a29b2b0..d1c78fe 100644 --- a/Sora/Views/FavoriteMenuButtonView.swift +++ b/Sora/Views/FavoriteMenuButtonView.swift @@ -1,7 +1,8 @@ import SwiftUI struct FavoriteMenuButtonView: View { - @EnvironmentObject var settings: SettingsManager + @Environment(SettingsManager.self) + private var settings @EnvironmentObject var manager: BooruManager let post: BooruPost var disableNewCollection = false diff --git a/Sora/Views/FavoritePostThumbnailView.swift b/Sora/Views/FavoritePostThumbnailView.swift index 41113b9..b1f727b 100644 --- a/Sora/Views/FavoritePostThumbnailView.swift +++ b/Sora/Views/FavoritePostThumbnailView.swift @@ -2,7 +2,8 @@ import NetworkImage import SwiftUI struct FavoritePostThumbnailView: View { - @EnvironmentObject var settings: SettingsManager + @Environment(SettingsManager.self) + private var settings let favorite: SettingsFavoritePost let onRemove: () -> Void private var networkImageLoader: BooruNetworkImageLoader { @@ -95,5 +96,5 @@ struct FavoritePostThumbnailView: View { ) FavoritePostThumbnailView(favorite: sampleFavorite) { () } - .environmentObject(SettingsManager()) + .environment(SettingsManager()) } diff --git a/Sora/Views/FavoritesView.swift b/Sora/Views/FavoritesView.swift index 007500b..50de8ed 100644 --- a/Sora/Views/FavoritesView.swift +++ b/Sora/Views/FavoritesView.swift @@ -3,7 +3,8 @@ import SwiftUI struct FavoritesView: View { // swiftlint:disable:this type_body_length - @EnvironmentObject var settings: SettingsManager + @Environment(SettingsManager.self) + private var settings @EnvironmentObject var manager: BooruManager @Binding var selectedTab: Int @State private var searchText: String = "" @@ -701,6 +702,6 @@ extension SettingsFavoritePost { #Preview { FavoritesView(selectedTab: .constant(1), isPresented: .constant(false)) - .environmentObject(SettingsManager()) + .environment(SettingsManager()) .environmentObject(BooruManager(.yandere)) } diff --git a/Sora/Views/Generic/GenericItemView.swift b/Sora/Views/Generic/GenericItemView.swift index 99c4f0a..a22d6c1 100644 --- a/Sora/Views/Generic/GenericItemView.swift +++ b/Sora/Views/Generic/GenericItemView.swift @@ -1,7 +1,8 @@ import SwiftUI struct GenericItemView<T: GenericItem>: View { - @EnvironmentObject var settings: SettingsManager + @Environment(SettingsManager.self) + private var settings let item: T let removeAction: (UUID) -> Void diff --git a/Sora/Views/Generic/GenericListView.swift b/Sora/Views/Generic/GenericListView.swift index d1c1436..58ae099 100644 --- a/Sora/Views/Generic/GenericListView.swift +++ b/Sora/Views/Generic/GenericListView.swift @@ -4,7 +4,8 @@ import SwiftUI // swiftlint:disable:next type_body_length struct GenericListView<T: Identifiable & Hashable & GenericItem>: View { - @EnvironmentObject private var settings: SettingsManager + @Environment(SettingsManager.self) + private var settings @EnvironmentObject private var manager: BooruManager @Binding var selectedTab: Int @State private var searchText: String = "" diff --git a/Sora/Views/MainView.swift b/Sora/Views/MainView.swift index 06e067b..834b192 100644 --- a/Sora/Views/MainView.swift +++ b/Sora/Views/MainView.swift @@ -1,6 +1,14 @@ import SwiftUI struct MainView: View { + private struct ManagerConfiguration: Equatable { + let provider: BooruProvider + let credentials: BooruProviderCredentials? + let sendUserAgent: Bool + let customUserAgent: String + let showHeldMoebooruPosts: Bool + } + private enum MainSidebarSection: Int, CaseIterable, Hashable { case posts = 0 case bookmarks = 1 @@ -33,28 +41,34 @@ struct MainView: View { } } - @EnvironmentObject var settings: SettingsManager + @Environment(SettingsManager.self) + private var settings @State private var selectedTab: Int = 0 @State private var selectedSidebarSection: MainSidebarSection? = .posts @State private var manager = BooruManager(.yandere) + private var managerConfiguration: ManagerConfiguration { + ManagerConfiguration( + provider: settings.preferredBooru, + credentials: settings.providerCredentials + .first { $0.provider == settings.preferredBooru }, + sendUserAgent: settings.sendBooruUserAgent, + customUserAgent: settings.customBooruUserAgent, + showHeldMoebooruPosts: settings.showHeldMoebooruPosts + ) + } + var body: some View { platformSpecificContent - .environmentObject(settings) + .environment(settings) .environmentObject(manager) - .onChange(of: settings.preferredBooru) { _, newState in - updateManager(newState) + .onChange(of: managerConfiguration) { _, newConfiguration in + updateManager(using: newConfiguration) } .onChange(of: manager.historyIndex) { _, _ in - if manager.isNavigatingHistory { - manager.selectedPost = nil - } + handleHistoryIndexChange() } .onAppear(perform: initializeManager) - .onChange(of: settings.providerCredentials) { updateManager(settings.preferredBooru) } - .onChange(of: settings.showHeldMoebooruPosts) { updateManager(settings.preferredBooru) } - .onChange(of: settings.sendBooruUserAgent) { updateManager(settings.preferredBooru) } - .onChange(of: settings.customBooruUserAgent) { updateManager(settings.preferredBooru) } #if os(macOS) .onChange(of: selectedSidebarSection) { _, newValue in guard let newValue else { return } @@ -159,17 +173,26 @@ struct MainView: View { #endif } - private func updateManager(_ provider: BooruProvider) { - let previousSearchText = manager.searchText + private func handleHistoryIndexChange() { + if manager.isNavigatingHistory { + manager.selectedPost = nil + } + } - manager = BooruManager( - provider, - credentials: settings.providerCredentials - .first { $0.provider == settings.preferredBooru }, - sendUserAgent: settings.sendBooruUserAgent, - customUserAgent: settings.customBooruUserAgent, - showHeldMoebooruPosts: settings.showHeldMoebooruPosts + private func makeManager(from configuration: ManagerConfiguration) -> BooruManager { + BooruManager( + configuration.provider, + credentials: configuration.credentials, + sendUserAgent: configuration.sendUserAgent, + customUserAgent: configuration.customUserAgent, + showHeldMoebooruPosts: configuration.showHeldMoebooruPosts ) + } + + private func updateManager(using configuration: ManagerConfiguration) { + let previousSearchText = manager.searchText + + manager = makeManager(from: configuration) manager.searchText = previousSearchText Task(priority: .userInitiated) { @@ -182,14 +205,7 @@ struct MainView: View { } private func initializeManager() { - manager = BooruManager( - settings.preferredBooru, - credentials: settings.providerCredentials - .first { $0.provider == settings.preferredBooru }, - sendUserAgent: settings.sendBooruUserAgent, - customUserAgent: settings.customBooruUserAgent, - showHeldMoebooruPosts: settings.showHeldMoebooruPosts - ) + manager = makeManager(from: managerConfiguration) Task(priority: .userInitiated) { if manager.posts.isEmpty { @@ -207,7 +223,7 @@ struct MainView: View { #endif MainView() - .environmentObject(SettingsManager()) + .environment(SettingsManager()) #if os(macOS) .frame( width: screenSize.width / widthCoefficient, diff --git a/Sora/Views/Post/Details/Carousel/PostDetailsCarouselView.swift b/Sora/Views/Post/Details/Carousel/PostDetailsCarouselView.swift index e43be47..55d6243 100644 --- a/Sora/Views/Post/Details/Carousel/PostDetailsCarouselView.swift +++ b/Sora/Views/Post/Details/Carousel/PostDetailsCarouselView.swift @@ -2,7 +2,8 @@ import SwiftUI struct PostDetailsCarouselView: View { @EnvironmentObject var manager: BooruManager - @EnvironmentObject var settings: SettingsManager + @Environment(SettingsManager.self) + private var settings let posts: [BooruPost] let focusedPost: BooruPost? let onFocusedPostChange: (BooruPost) -> Void diff --git a/Sora/Views/Post/Details/PostDetailsImageView.swift b/Sora/Views/Post/Details/PostDetailsImageView.swift index 5b954a6..009dbed 100644 --- a/Sora/Views/Post/Details/PostDetailsImageView.swift +++ b/Sora/Views/Post/Details/PostDetailsImageView.swift @@ -3,7 +3,8 @@ import SwiftUI import UserNotifications struct PostDetailsImageView<Placeholder: View>: View { // swiftlint:disable:this type_body_length - @EnvironmentObject var settings: SettingsManager + @Environment(SettingsManager.self) + private var settings @EnvironmentObject var manager: BooruManager var url: URL? @Binding var loadingState: BooruPostLoadingState diff --git a/Sora/Views/Post/Details/PostDetailsTagsView.swift b/Sora/Views/Post/Details/PostDetailsTagsView.swift index 72ba757..f843b6c 100644 --- a/Sora/Views/Post/Details/PostDetailsTagsView.swift +++ b/Sora/Views/Post/Details/PostDetailsTagsView.swift @@ -2,7 +2,8 @@ import SwiftUI struct PostDetailsTagsView: View { @EnvironmentObject var manager: BooruManager - @EnvironmentObject var settings: SettingsManager + @Environment(SettingsManager.self) + private var settings @Binding var isPresented: Bool @Binding var navigationPath: NavigationPath var tags: [String] @@ -101,5 +102,5 @@ struct PostDetailsTagsView: View { tags: ["hololive", "absurdres", "nekomimi"] ) .environmentObject(BooruManager(.danbooru)) - .environmentObject(SettingsManager()) + .environment(SettingsManager()) } diff --git a/Sora/Views/Post/Details/PostDetailsView.swift b/Sora/Views/Post/Details/PostDetailsView.swift index bb58c49..d8a77c6 100644 --- a/Sora/Views/Post/Details/PostDetailsView.swift +++ b/Sora/Views/Post/Details/PostDetailsView.swift @@ -2,7 +2,8 @@ import SwiftUI struct PostDetailsView: View { @EnvironmentObject var manager: BooruManager - @EnvironmentObject var settings: SettingsManager + @Environment(SettingsManager.self) + private var settings let post: BooruPost @Binding var navigationPath: NavigationPath @State private var loadingStage: BooruPostLoadingState = .loadingPreview diff --git a/Sora/Views/Post/Grid/PostGridBookmarkButtonView.swift b/Sora/Views/Post/Grid/PostGridBookmarkButtonView.swift index 2dadfc3..020e0ce 100644 --- a/Sora/Views/Post/Grid/PostGridBookmarkButtonView.swift +++ b/Sora/Views/Post/Grid/PostGridBookmarkButtonView.swift @@ -2,7 +2,8 @@ import SwiftUI struct PostGridBookmarkButtonView: View { @EnvironmentObject private var manager: BooruManager - @EnvironmentObject private var settings: SettingsManager + @Environment(SettingsManager.self) + private var settings let tags: [String] let provider: BooruProvider diff --git a/Sora/Views/Post/Grid/PostGridFavoriteButtonView.swift b/Sora/Views/Post/Grid/PostGridFavoriteButtonView.swift index bd6b6f9..c6ecd7f 100644 --- a/Sora/Views/Post/Grid/PostGridFavoriteButtonView.swift +++ b/Sora/Views/Post/Grid/PostGridFavoriteButtonView.swift @@ -2,7 +2,8 @@ import SwiftUI struct PostGridFavoriteButtonView: View { @EnvironmentObject private var manager: BooruManager - @EnvironmentObject private var settings: SettingsManager + @Environment(SettingsManager.self) + private var settings let post: BooruPost var isFavorited: Bool { @@ -40,6 +41,6 @@ struct PostGridFavoriteButtonView: View { ) PostGridFavoriteButtonView(post: samplePost) - .environmentObject(SettingsManager()) + .environment(SettingsManager()) .environmentObject(BooruManager(.yandere)) } diff --git a/Sora/Views/Post/Grid/PostGridSearchHistoryView.swift b/Sora/Views/Post/Grid/PostGridSearchHistoryView.swift index bc7c52a..fcadea1 100644 --- a/Sora/Views/Post/Grid/PostGridSearchHistoryView.swift +++ b/Sora/Views/Post/Grid/PostGridSearchHistoryView.swift @@ -1,7 +1,8 @@ import SwiftUI struct PostGridSearchHistoryView: View { - @EnvironmentObject var settings: SettingsManager + @Environment(SettingsManager.self) + private var settings @Binding var selectedTab: Int @Binding var isPresented: Bool @@ -30,6 +31,6 @@ struct PostGridSearchHistoryView: View { selectedTab: .constant(0), isPresented: .constant(true) ) - .environmentObject(SettingsManager()) + .environment(SettingsManager()) .environmentObject(BooruManager(.safebooru)) } diff --git a/Sora/Views/Post/Grid/PostGridThumbnailView.swift b/Sora/Views/Post/Grid/PostGridThumbnailView.swift index 5f7829e..fade59b 100644 --- a/Sora/Views/Post/Grid/PostGridThumbnailView.swift +++ b/Sora/Views/Post/Grid/PostGridThumbnailView.swift @@ -2,7 +2,8 @@ import NetworkImage import SwiftUI struct PostGridThumbnailView: View { - @EnvironmentObject var settings: SettingsManager + @Environment(SettingsManager.self) + private var settings @EnvironmentObject var manager: BooruManager let post: BooruPost let posts: [BooruPost] diff --git a/Sora/Views/Post/Grid/PostGridView.swift b/Sora/Views/Post/Grid/PostGridView.swift index 330e294..77e96aa 100644 --- a/Sora/Views/Post/Grid/PostGridView.swift +++ b/Sora/Views/Post/Grid/PostGridView.swift @@ -3,7 +3,8 @@ import SwiftUI struct PostGridView: View { // swiftlint:disable:this type_body_length - @EnvironmentObject var settings: SettingsManager + @Environment(SettingsManager.self) + private var settings @EnvironmentObject var manager: BooruManager @State private var isSearchHistoryPresented = false @State private var isSettingsPresented = false diff --git a/Sora/Views/Settings/Collections/SettingsCollectionsListView.swift b/Sora/Views/Settings/Collections/SettingsCollectionsListView.swift index c474b79..1cd9121 100644 --- a/Sora/Views/Settings/Collections/SettingsCollectionsListView.swift +++ b/Sora/Views/Settings/Collections/SettingsCollectionsListView.swift @@ -1,7 +1,8 @@ import SwiftUI struct SettingsCollectionsListView: View { - @EnvironmentObject private var settings: SettingsManager + @Environment(SettingsManager.self) + private var settings var body: some View { List { diff --git a/Sora/Views/Settings/Collections/SettingsCollectionsView.swift b/Sora/Views/Settings/Collections/SettingsCollectionsView.swift index a7ce5a7..f74fef9 100644 --- a/Sora/Views/Settings/Collections/SettingsCollectionsView.swift +++ b/Sora/Views/Settings/Collections/SettingsCollectionsView.swift @@ -1,7 +1,8 @@ import SwiftUI struct SettingsCollectionsView: View { - @EnvironmentObject private var settings: SettingsManager + @Environment(SettingsManager.self) + private var settings @State private var isDeleteConfirmationPresented = false @State private var isRenameAlertPresented = false diff --git a/Sora/Views/Settings/Section/SettingsSectionContentRatingsView.swift b/Sora/Views/Settings/Section/SettingsSectionContentRatingsView.swift index 40a8a7e..353824a 100644 --- a/Sora/Views/Settings/Section/SettingsSectionContentRatingsView.swift +++ b/Sora/Views/Settings/Section/SettingsSectionContentRatingsView.swift @@ -1,7 +1,8 @@ import SwiftUI struct SettingsSectionContentRatingsView: View { - @EnvironmentObject var settings: SettingsManager + @Environment(SettingsManager.self) + private var settings var body: some View { List { @@ -70,5 +71,5 @@ struct SettingsSectionContentRatingsView: View { #Preview { SettingsSectionContentRatingsView() - .environmentObject(SettingsManager()) + .environment(SettingsManager()) } diff --git a/Sora/Views/Settings/Section/SettingsSectionDebugView.swift b/Sora/Views/Settings/Section/SettingsSectionDebugView.swift index 83acb81..c97b545 100644 --- a/Sora/Views/Settings/Section/SettingsSectionDebugView.swift +++ b/Sora/Views/Settings/Section/SettingsSectionDebugView.swift @@ -1,7 +1,8 @@ import SwiftUI struct SettingsSectionDebugView: View { - @EnvironmentObject private var settingsManager: SettingsManager + @Environment(SettingsManager.self) + private var settingsManager var body: some View { Button(action: { diff --git a/Sora/Views/Settings/Section/SettingsSectionDetailsView.swift b/Sora/Views/Settings/Section/SettingsSectionDetailsView.swift index 8db6002..c386634 100644 --- a/Sora/Views/Settings/Section/SettingsSectionDetailsView.swift +++ b/Sora/Views/Settings/Section/SettingsSectionDetailsView.swift @@ -1,9 +1,12 @@ import SwiftUI struct SettingsSectionDetailsView: View { - @EnvironmentObject var settings: SettingsManager + @Environment(SettingsManager.self) + private var settings var body: some View { + @Bindable var settings = settings + Form { Section("Image Quality") { Picker("Image Quality", selection: $settings.detailViewQuality) { @@ -58,6 +61,6 @@ struct SettingsSectionDetailsView: View { #Preview { NavigationStack { SettingsSectionDetailsView() - .environmentObject(SettingsManager()) + .environment(SettingsManager()) } } diff --git a/Sora/Views/Settings/Section/SettingsSectionImportExportView.swift b/Sora/Views/Settings/Section/SettingsSectionImportExportView.swift index 381b6a4..846e9e5 100644 --- a/Sora/Views/Settings/Section/SettingsSectionImportExportView.swift +++ b/Sora/Views/Settings/Section/SettingsSectionImportExportView.swift @@ -2,7 +2,8 @@ import SwiftUI import UniformTypeIdentifiers struct SettingsSectionImportExportView: View { - @EnvironmentObject private var settings: SettingsManager + @Environment(SettingsManager.self) + private var settings @State private var isFileExporterPresented = false @State private var isFileImporterPresented = false @State private var bookmarksExportDocument: JSONFileDocument? diff --git a/Sora/Views/Settings/Section/SettingsSectionProviderView.swift b/Sora/Views/Settings/Section/SettingsSectionProviderView.swift index b9a7900..02a8be6 100644 --- a/Sora/Views/Settings/Section/SettingsSectionProviderView.swift +++ b/Sora/Views/Settings/Section/SettingsSectionProviderView.swift @@ -1,13 +1,16 @@ import SwiftUI struct SettingsSectionProviderView: View { - @EnvironmentObject var settings: SettingsManager + @Environment(SettingsManager.self) + private var settings @State private var showingCustomBooruSheet = false @State private var newDomain: String = "" @State private var newFlavor: BooruProviderFlavor = .danbooru @State private var domainError: String? var body: some View { + @Bindable var settings = settings + Form { Section("Source") { Picker("Website", selection: $settings.preferredBooru) { @@ -173,7 +176,9 @@ struct SettingsSectionProviderView: View { ) { Button("OK", role: .cancel) { () } } message: { - Text(domainError ?? "An unknown error occurred while validating the domain.") + if let domainError { + Text(domainError) + } } } @@ -271,6 +276,6 @@ struct SettingsSectionProviderView: View { #Preview { NavigationStack { SettingsSectionProviderView() - .environmentObject(SettingsManager()) + .environment(SettingsManager()) } } diff --git a/Sora/Views/Settings/Section/SettingsSectionSearchView.swift b/Sora/Views/Settings/Section/SettingsSectionSearchView.swift index b702cd4..e82f872 100644 --- a/Sora/Views/Settings/Section/SettingsSectionSearchView.swift +++ b/Sora/Views/Settings/Section/SettingsSectionSearchView.swift @@ -1,9 +1,12 @@ import SwiftUI struct SettingsSectionSearchView: View { - @EnvironmentObject var settings: SettingsManager + @Environment(SettingsManager.self) + private var settings var body: some View { + @Bindable var settings = settings + Picker("Suggestions", selection: $settings.searchSuggestionsMode) { ForEach(SettingsSearchSuggestionsMode.allCases, id: \.self) { type in Text(type.rawValue.capitalized).tag(type) diff --git a/Sora/Views/Settings/Section/SettingsSectionSettingsView.swift b/Sora/Views/Settings/Section/SettingsSectionSettingsView.swift index 784ee2a..ad7fe83 100644 --- a/Sora/Views/Settings/Section/SettingsSectionSettingsView.swift +++ b/Sora/Views/Settings/Section/SettingsSectionSettingsView.swift @@ -1,17 +1,22 @@ import SwiftUI struct SettingsSectionSettingsView: View { - @EnvironmentObject var settings: SettingsManager + @Environment(SettingsManager.self) + private var settings var body: some View { + @Bindable var settings = settings + Toggle(isOn: $settings.enableSync) { Text("Sync with iCloud") Text("Keep bookmarks, collections, search history, and sources in sync across your devices.") .font(.caption) } - .onChange(of: settings.enableSync) { _, _ in - settings.triggerSyncIfNeededForAll() + .onChange(of: settings.enableSync) { _, isEnabled in + if isEnabled { + settings.triggerSyncIfNeededForAll() + } } Button("Reset Settings") { diff --git a/Sora/Views/Settings/Section/SettingsSectionThumbnailsView.swift b/Sora/Views/Settings/Section/SettingsSectionThumbnailsView.swift index f8dffc8..dc9d87e 100644 --- a/Sora/Views/Settings/Section/SettingsSectionThumbnailsView.swift +++ b/Sora/Views/Settings/Section/SettingsSectionThumbnailsView.swift @@ -1,10 +1,13 @@ import SwiftUI struct SettingsSectionThumbnailsView: View { - @EnvironmentObject var settings: SettingsManager + @Environment(SettingsManager.self) + private var settings @State private var isShowingContentFiltering = false var body: some View { + @Bindable var settings = settings + Form { Section("Image Quality") { Picker("Thumbnail Quality", selection: $settings.thumbnailQuality) { @@ -62,6 +65,6 @@ struct SettingsSectionThumbnailsView: View { #Preview { NavigationStack { SettingsSectionThumbnailsView() - .environmentObject(SettingsManager()) + .environment(SettingsManager()) } } diff --git a/Sora/Views/Settings/SettingsView.swift b/Sora/Views/Settings/SettingsView.swift index 4ea60b8..ae5a620 100644 --- a/Sora/Views/Settings/SettingsView.swift +++ b/Sora/Views/Settings/SettingsView.swift @@ -56,6 +56,6 @@ struct SettingsView: View { #Preview { SettingsView() - .environmentObject(SettingsManager()) + .environment(SettingsManager()) .environmentObject(BooruManager(.yandere)) } diff --git a/SoraTests/SettingsManagerSyncTests.swift b/SoraTests/SettingsManagerSyncTests.swift index 8ed9d83..0ca99a2 100644 --- a/SoraTests/SettingsManagerSyncTests.swift +++ b/SoraTests/SettingsManagerSyncTests.swift @@ -247,7 +247,7 @@ final class SettingsManagerSyncTests: XCTestCase { ) } - func testManualFullSyncAggregatesChangeNotification() throws { + func testManualFullSyncAvoidsLegacyChangePublisher() throws { let source = try loadSource(at: "Sora/Data/Settings/SettingsManager.swift") let fullSyncSection = try extractFunction( named: "func triggerSyncIfNeededForAll()", @@ -271,10 +271,10 @@ final class SettingsManagerSyncTests: XCTestCase { "Full sync should evaluate all supported sync keys." ) // swiftlint:disable:next prefer_nimble - XCTAssertGreaterThan( + XCTAssertEqual( objectWillChangeCount, 0, - "Full sync should emit a consolidated objectWillChange notification when merged state changes." + "Observation-based sync should not depend on the legacy objectWillChange publisher." ) } } |