diff --git a/IceCubesApp.xcodeproj/project.pbxproj b/IceCubesApp.xcodeproj/project.pbxproj index b58d1026..aa2e603f 100644 --- a/IceCubesApp.xcodeproj/project.pbxproj +++ b/IceCubesApp.xcodeproj/project.pbxproj @@ -975,13 +975,13 @@ INFOPLIST_FILE = IceCubesNotifications/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = IceCubesNotifications; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 16.1; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.7.9; + MARKETING_VERSION = 1.8.0; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesNotifications"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -1005,13 +1005,13 @@ INFOPLIST_FILE = IceCubesNotifications/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = IceCubesNotifications; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 16.1; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.7.9; + MARKETING_VERSION = 1.8.0; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesNotifications"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -1036,13 +1036,13 @@ INFOPLIST_FILE = IceCubesShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Ice Cubes"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 16.1; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.7.9; + MARKETING_VERSION = 1.8.0; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesShareExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -1066,13 +1066,13 @@ INFOPLIST_FILE = IceCubesShareExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Ice Cubes"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 16.1; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.7.9; + MARKETING_VERSION = 1.8.0; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesShareExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -1240,11 +1240,11 @@ "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 16.1; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.7.9; + MARKETING_VERSION = 1.8.0; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; @@ -1293,11 +1293,11 @@ "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - IPHONEOS_DEPLOYMENT_TARGET = 16.1; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.7.9; + MARKETING_VERSION = 1.8.0; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; @@ -1325,13 +1325,13 @@ INFOPLIST_FILE = IceCubesActionExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Open in Ice Cube"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 16.1; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.7.9; + MARKETING_VERSION = 1.8.0; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesActionExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -1356,13 +1356,13 @@ INFOPLIST_FILE = IceCubesActionExtension/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Open in Ice Cube"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 16.1; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.7.9; + MARKETING_VERSION = 1.8.0; PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_ID_PREFIX).IceCubesApp.IceCubesActionExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; diff --git a/IceCubesApp/App/AppRouter.swift b/IceCubesApp/App/AppRouter.swift index b96506c0..bd8c1c0e 100644 --- a/IceCubesApp/App/AppRouter.swift +++ b/IceCubesApp/App/AppRouter.swift @@ -112,13 +112,13 @@ extension View { } func withEnvironments() -> some View { - environmentObject(CurrentAccount.shared) + environment(CurrentAccount.shared) .environmentObject(UserPreferences.shared) - .environmentObject(CurrentInstance.shared) + .environment(CurrentInstance.shared) .environmentObject(Theme.shared) - .environmentObject(AppAccountsManager.shared) - .environmentObject(PushNotificationsService.shared) - .environmentObject(AppAccountsManager.shared.currentClient) + .environment(AppAccountsManager.shared) + .environment(PushNotificationsService.shared) + .environment(AppAccountsManager.shared.currentClient) } } diff --git a/IceCubesApp/App/IceCubesApp.swift b/IceCubesApp/App/IceCubesApp.swift index d18a7e4d..4a59375f 100644 --- a/IceCubesApp/App/IceCubesApp.swift +++ b/IceCubesApp/App/IceCubesApp.swift @@ -15,15 +15,15 @@ struct IceCubesApp: App { @Environment(\.scenePhase) private var scenePhase - @StateObject private var appAccountsManager = AppAccountsManager.shared - @StateObject private var currentInstance = CurrentInstance.shared - @StateObject private var currentAccount = CurrentAccount.shared + @State private var appAccountsManager = AppAccountsManager.shared + @State private var currentInstance = CurrentInstance.shared + @State private var currentAccount = CurrentAccount.shared @StateObject private var userPreferences = UserPreferences.shared - @StateObject private var pushNotificationsService = PushNotificationsService.shared - @StateObject private var watcher = StreamWatcher() - @StateObject private var quickLook = QuickLook() + @State private var pushNotificationsService = PushNotificationsService.shared + @State private var watcher = StreamWatcher() + @State private var quickLook = QuickLook() @StateObject private var theme = Theme.shared - @StateObject private var sidebarRouterPath = RouterPath() + @State private var sidebarRouterPath = RouterPath() @State private var selectedTab: Tab = .timeline @State private var popToRootTab: Tab = .other @@ -43,32 +43,32 @@ struct IceCubesApp: App { setupRevenueCat() refreshPushSubs() } - .environmentObject(appAccountsManager) - .environmentObject(appAccountsManager.currentClient) - .environmentObject(quickLook) - .environmentObject(currentAccount) - .environmentObject(currentInstance) + .environment(appAccountsManager) + .environment(appAccountsManager.currentClient) + .environment(quickLook) + .environment(currentAccount) + .environment(currentInstance) .environmentObject(userPreferences) .environmentObject(theme) - .environmentObject(watcher) - .environmentObject(pushNotificationsService) + .environment(watcher) + .environment(pushNotificationsService) .environment(\.isSupporter, isSupporter) .fullScreenCover(item: $quickLook.url, content: { url in QuickLookPreview(selectedURL: url, urls: quickLook.urls) .edgesIgnoringSafeArea(.bottom) .background(TransparentBackground()) }) - .onChange(of: pushNotificationsService.handledNotification) { notification in - if notification != nil { + .onChange(of: pushNotificationsService.handledNotification) { _, newValue in + if newValue != nil { pushNotificationsService.handledNotification = nil - if appAccountsManager.currentAccount.oauthToken?.accessToken != notification?.account.token.accessToken, + if appAccountsManager.currentAccount.oauthToken?.accessToken != newValue?.account.token.accessToken, let account = appAccountsManager.availableAccounts.first(where: - { $0.oauthToken?.accessToken == notification?.account.token.accessToken }) + { $0.oauthToken?.accessToken == newValue?.account.token.accessToken }) { appAccountsManager.currentAccount = account DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { selectedTab = .notifications - pushNotificationsService.handledNotification = notification + pushNotificationsService.handledNotification = newValue } } else { selectedTab = .notifications @@ -79,12 +79,12 @@ struct IceCubesApp: App { .commands { appMenu } - .onChange(of: scenePhase) { scenePhase in - handleScenePhase(scenePhase: scenePhase) + .onChange(of: scenePhase) { _, newValue in + handleScenePhase(scenePhase: newValue) } - .onChange(of: appAccountsManager.currentClient) { newClient in - setNewClientsInEnv(client: newClient) - if newClient.isAuth { + .onChange(of: appAccountsManager.currentClient) { _, newValue in + setNewClientsInEnv(client: newValue) + if newValue.isAuth { watcher.watch(streams: [.user, .direct]) } } @@ -111,8 +111,7 @@ struct IceCubesApp: App { private var sidebarView: some View { SideBarView(selectedTab: $selectedTab, popToRootTab: $popToRootTab, - tabs: availableTabs, - routerPath: sidebarRouterPath) + tabs: availableTabs) { GeometryReader { _ in HStack(spacing: 0) { @@ -143,9 +142,10 @@ struct IceCubesApp: App { } } } - }.onChange(of: $appAccountsManager.currentAccount.id) { _ in + }.onChange(of: $appAccountsManager.currentAccount.id) { sideBarLoadedTabs.removeAll() } + .environment(sidebarRouterPath) } private var notificationsSecondaryColumn: some View { @@ -218,7 +218,7 @@ struct IceCubesApp: App { watcher.stopWatching() case .active: watcher.watch(streams: [.user, .direct]) - UIApplication.shared.applicationIconBadgeNumber = 0 + UNUserNotificationCenter.current().setBadgeCount(0) Task { await userPreferences.refreshServerPreferences() } diff --git a/IceCubesApp/App/Report/ReportView.swift b/IceCubesApp/App/Report/ReportView.swift index c70b905a..30a360d3 100644 --- a/IceCubesApp/App/Report/ReportView.swift +++ b/IceCubesApp/App/Report/ReportView.swift @@ -9,7 +9,7 @@ public struct ReportView: View { @Environment(\.dismiss) private var dismiss @EnvironmentObject private var theme: Theme - @EnvironmentObject private var client: Client + @Environment(Client.self) private var client let status: Status @State private var commentText: String = "" diff --git a/IceCubesApp/App/SafariRouter.swift b/IceCubesApp/App/SafariRouter.swift index c3d40784..acefaa9a 100644 --- a/IceCubesApp/App/SafariRouter.swift +++ b/IceCubesApp/App/SafariRouter.swift @@ -1,5 +1,6 @@ import DesignSystem import Env +import Observation import SafariServices import SwiftUI @@ -13,9 +14,9 @@ extension View { private struct SafariRouter: ViewModifier { @EnvironmentObject private var theme: Theme @EnvironmentObject private var preferences: UserPreferences - @EnvironmentObject private var routerPath: RouterPath + @Environment(RouterPath.self) private var routerPath - @StateObject private var safariManager = InAppSafariManager() + @State private var safariManager = InAppSafariManager() func body(content: Content) -> some View { content @@ -58,7 +59,7 @@ private struct SafariRouter: ViewModifier { } @MainActor -private class InAppSafariManager: NSObject, ObservableObject, SFSafariViewControllerDelegate { +@Observable private class InAppSafariManager: NSObject, SFSafariViewControllerDelegate { var windowScene: UIWindowScene? let viewController: UIViewController = .init() var window: UIWindow? diff --git a/IceCubesApp/App/SideBarView.swift b/IceCubesApp/App/SideBarView.swift index 6bbf7e71..ddd6e032 100644 --- a/IceCubesApp/App/SideBarView.swift +++ b/IceCubesApp/App/SideBarView.swift @@ -6,16 +6,16 @@ import Models import SwiftUI struct SideBarView: View { - @EnvironmentObject private var appAccounts: AppAccountsManager - @EnvironmentObject private var currentAccount: CurrentAccount + @Environment(AppAccountsManager.self) private var appAccounts + @Environment(CurrentAccount.self) private var currentAccount @EnvironmentObject private var theme: Theme - @EnvironmentObject private var watcher: StreamWatcher + @Environment(StreamWatcher.self) private var watcher @EnvironmentObject private var userPreferences: UserPreferences + @Environment(RouterPath.self) private var routerPath @Binding var selectedTab: Tab @Binding var popToRootTab: Tab var tabs: [Tab] - @ObservedObject var routerPath = RouterPath() @ViewBuilder var content: () -> Content private func badgeFor(tab: Tab) -> Int { @@ -122,6 +122,7 @@ struct SideBarView: View { } var body: some View { + @Bindable var routerPath = routerPath HStack(spacing: 0) { ScrollView { VStack(alignment: .center) { diff --git a/IceCubesApp/App/Tabs/ExploreTab.swift b/IceCubesApp/App/Tabs/ExploreTab.swift index 4459c35c..4449e917 100644 --- a/IceCubesApp/App/Tabs/ExploreTab.swift +++ b/IceCubesApp/App/Tabs/ExploreTab.swift @@ -10,9 +10,9 @@ import SwiftUI struct ExploreTab: View { @EnvironmentObject private var theme: Theme @EnvironmentObject private var preferences: UserPreferences - @EnvironmentObject private var currentAccount: CurrentAccount - @EnvironmentObject private var client: Client - @StateObject private var routerPath = RouterPath() + @Environment(CurrentAccount.self) private var currentAccount + @Environment(Client.self) private var client + @State private var routerPath = RouterPath() @Binding var popToRootTab: Tab var body: some View { @@ -35,13 +35,13 @@ struct ExploreTab: View { } } .withSafariRouter() - .environmentObject(routerPath) - .onChange(of: $popToRootTab.wrappedValue) { popToRootTab in - if popToRootTab == .explore { + .environment(routerPath) + .onChange(of: $popToRootTab.wrappedValue) { _, newValue in + if newValue == .explore { routerPath.path = [] } } - .onChange(of: client.id) { _ in + .onChange(of: client.id) { routerPath.path = [] } .onAppear { diff --git a/IceCubesApp/App/Tabs/MessagesTab.swift b/IceCubesApp/App/Tabs/MessagesTab.swift index 70efee17..1a2198bc 100644 --- a/IceCubesApp/App/Tabs/MessagesTab.swift +++ b/IceCubesApp/App/Tabs/MessagesTab.swift @@ -10,11 +10,11 @@ import SwiftUI struct MessagesTab: View { @EnvironmentObject private var theme: Theme - @EnvironmentObject private var watcher: StreamWatcher - @EnvironmentObject private var client: Client - @EnvironmentObject private var currentAccount: CurrentAccount - @EnvironmentObject private var appAccount: AppAccountsManager - @StateObject private var routerPath = RouterPath() + @Environment(StreamWatcher.self) private var watcher + @Environment(Client.self) private var client + @Environment(CurrentAccount.self) private var currentAccount + @Environment(AppAccountsManager.self) private var appAccount + @State private var routerPath = RouterPath() @Binding var popToRootTab: Tab var body: some View { @@ -32,18 +32,18 @@ struct MessagesTab: View { .toolbarBackground(theme.primaryBackgroundColor.opacity(0.50), for: .navigationBar) .id(client.id) } - .onChange(of: $popToRootTab.wrappedValue) { popToRootTab in - if popToRootTab == .messages { + .onChange(of: $popToRootTab.wrappedValue) { _, newValue in + if newValue == .messages { routerPath.path = [] } } - .onChange(of: client.id) { _ in + .onChange(of: client.id) { routerPath.path = [] } .onAppear { routerPath.client = client } .withSafariRouter() - .environmentObject(routerPath) + .environment(routerPath) } } diff --git a/IceCubesApp/App/Tabs/NotificationTab.swift b/IceCubesApp/App/Tabs/NotificationTab.swift index db09edf8..c86546b9 100644 --- a/IceCubesApp/App/Tabs/NotificationTab.swift +++ b/IceCubesApp/App/Tabs/NotificationTab.swift @@ -12,13 +12,13 @@ struct NotificationsTab: View { @Environment(\.scenePhase) private var scenePhase @EnvironmentObject private var theme: Theme - @EnvironmentObject private var client: Client - @EnvironmentObject private var watcher: StreamWatcher - @EnvironmentObject private var appAccount: AppAccountsManager - @EnvironmentObject private var currentAccount: CurrentAccount + @Environment(Client.self) private var client + @Environment(StreamWatcher.self) private var watcher + @Environment(AppAccountsManager.self) private var appAccount + @Environment(CurrentAccount.self) private var currentAccount @EnvironmentObject private var userPreferences: UserPreferences - @EnvironmentObject private var pushNotificationsService: PushNotificationsService - @StateObject private var routerPath = RouterPath() + @Environment(PushNotificationsService.self) private var pushNotificationsService + @State private var routerPath = RouterPath() @Binding var popToRootTab: Tab let lockedType: Models.Notification.NotificationType? @@ -54,35 +54,35 @@ struct NotificationsTab: View { } } .withSafariRouter() - .environmentObject(routerPath) - .onChange(of: $popToRootTab.wrappedValue) { popToRootTab in - if popToRootTab == .notifications { + .environment(routerPath) + .onChange(of: $popToRootTab.wrappedValue) { _, newValue in + if newValue == .notifications { routerPath.path = [] } } - .onChange(of: pushNotificationsService.handledNotification) { notification in - if let notification, let type = notification.notification.supportedType { + .onChange(of: pushNotificationsService.handledNotification) { _, newValue in + if let newValue, let type = newValue.notification.supportedType { DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { switch type { case .follow, .follow_request: - routerPath.navigate(to: .accountDetailWithAccount(account: notification.notification.account)) + routerPath.navigate(to: .accountDetailWithAccount(account: newValue.notification.account)) default: - if let status = notification.notification.status { + if let status = newValue.notification.status { routerPath.navigate(to: .statusDetailWithStatus(status: status)) } } } } } - .onChange(of: scenePhase, perform: { scenePhase in - switch scenePhase { + .onChange(of: scenePhase) { _, newValue in + switch newValue { case .active: clearNotifications() default: break } - }) - .onChange(of: client.id) { _ in + } + .onChange(of: client.id) { routerPath.path = [] } } diff --git a/IceCubesApp/App/Tabs/ProfileTab.swift b/IceCubesApp/App/Tabs/ProfileTab.swift index 4f72b2b2..e5978a13 100644 --- a/IceCubesApp/App/Tabs/ProfileTab.swift +++ b/IceCubesApp/App/Tabs/ProfileTab.swift @@ -9,11 +9,11 @@ import Shimmer import SwiftUI struct ProfileTab: View { - @EnvironmentObject private var appAccount: AppAccountsManager + @Environment(AppAccountsManager.self) private var appAccount @EnvironmentObject private var theme: Theme - @EnvironmentObject private var client: Client - @EnvironmentObject private var currentAccount: CurrentAccount - @StateObject private var routerPath = RouterPath() + @Environment(Client.self) private var client + @Environment(CurrentAccount.self) private var currentAccount + @State private var routerPath = RouterPath() @Binding var popToRootTab: Tab var body: some View { @@ -29,18 +29,18 @@ struct ProfileTab: View { .redacted(reason: .placeholder) } } - .onChange(of: $popToRootTab.wrappedValue) { popToRootTab in - if popToRootTab == .profile { + .onChange(of: $popToRootTab.wrappedValue) { _, newValue in + if newValue == .profile { routerPath.path = [] } } - .onChange(of: client.id) { _ in + .onChange(of: client.id) { routerPath.path = [] } .onAppear { routerPath.client = client } .withSafariRouter() - .environmentObject(routerPath) + .environment(routerPath) } } diff --git a/IceCubesApp/App/Tabs/Settings/AboutView.swift b/IceCubesApp/App/Tabs/Settings/AboutView.swift index 2daa1da5..fe18d78c 100644 --- a/IceCubesApp/App/Tabs/Settings/AboutView.swift +++ b/IceCubesApp/App/Tabs/Settings/AboutView.swift @@ -3,7 +3,7 @@ import Env import SwiftUI struct AboutView: View { - @EnvironmentObject private var routerPath: RouterPath + @Environment(RouterPath.self) private var routerPath @EnvironmentObject private var theme: Theme let versionNumber: String diff --git a/IceCubesApp/App/Tabs/Settings/AccountSettingView.swift b/IceCubesApp/App/Tabs/Settings/AccountSettingView.swift index 84ec34f4..a1610a88 100644 --- a/IceCubesApp/App/Tabs/Settings/AccountSettingView.swift +++ b/IceCubesApp/App/Tabs/Settings/AccountSettingView.swift @@ -11,12 +11,12 @@ struct AccountSettingsView: View { @Environment(\.dismiss) private var dismiss @Environment(\.openURL) private var openURL - @EnvironmentObject private var pushNotifications: PushNotificationsService - @EnvironmentObject private var currentAccount: CurrentAccount - @EnvironmentObject private var currentInstance: CurrentInstance + @Environment(PushNotificationsService.self) private var pushNotifications + @Environment(CurrentAccount.self) private var currentAccount + @Environment(CurrentInstance.self) private var currentInstance @EnvironmentObject private var theme: Theme - @EnvironmentObject private var appAccountsManager: AppAccountsManager - @EnvironmentObject private var client: Client + @Environment(AppAccountsManager.self) private var appAccountsManager + @Environment(Client.self) private var client @State private var isEditingAccount: Bool = false @State private var isEditingFilters: Bool = false diff --git a/IceCubesApp/App/Tabs/Settings/AddAccountsView.swift b/IceCubesApp/App/Tabs/Settings/AddAccountsView.swift index 722a752d..efb825a8 100644 --- a/IceCubesApp/App/Tabs/Settings/AddAccountsView.swift +++ b/IceCubesApp/App/Tabs/Settings/AddAccountsView.swift @@ -13,10 +13,10 @@ struct AddAccountView: View { @Environment(\.dismiss) private var dismiss @Environment(\.scenePhase) private var scenePhase - @EnvironmentObject private var appAccountsManager: AppAccountsManager - @EnvironmentObject private var currentAccount: CurrentAccount - @EnvironmentObject private var currentInstance: CurrentInstance - @EnvironmentObject private var pushNotifications: PushNotificationsService + @Environment(AppAccountsManager.self) private var appAccountsManager + @Environment(CurrentAccount.self) private var currentAccount + @Environment(CurrentInstance.self) private var currentInstance + @Environment(PushNotificationsService.self) private var pushNotifications @EnvironmentObject private var theme: Theme @State private var instanceName: String = "" @@ -89,7 +89,7 @@ struct AddAccountView: View { } isSigninIn = false } - .onChange(of: instanceName) { newValue in + .onChange(of: instanceName) { _, newValue in instanceNamePublisher.send(newValue) } .onReceive(instanceNamePublisher.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)) { _ in @@ -119,24 +119,24 @@ struct AddAccountView: View { } } } - .onChange(of: scenePhase, perform: { scenePhase in - switch scenePhase { + .onChange(of: scenePhase) { _, newValue in + switch newValue { case .active: isSigninIn = false default: break } - }) + } .onOpenURL(perform: { url in Task { await continueSignIn(url: url) } }) - .onChange(of: oauthURL, perform: { newValue in + .onChange(of: oauthURL) { _, newValue in if newValue == nil { isSigninIn = false } - }) + } .sheet(item: $oauthURL, content: { url in SafariView(url: url) }) diff --git a/IceCubesApp/App/Tabs/Settings/ContentSettingsView.swift b/IceCubesApp/App/Tabs/Settings/ContentSettingsView.swift index 32d1620b..40808b19 100644 --- a/IceCubesApp/App/Tabs/Settings/ContentSettingsView.swift +++ b/IceCubesApp/App/Tabs/Settings/ContentSettingsView.swift @@ -44,7 +44,7 @@ struct ContentSettingsView: View { } } .listRowBackground(theme.primaryBackgroundColor) - .onChange(of: userPreferences.useInstanceContentSettings) { newVal in + .onChange(of: userPreferences.useInstanceContentSettings) { _, newVal in if newVal { userPreferences.appAutoExpandSpoilers = userPreferences.autoExpandSpoilers userPreferences.appAutoExpandMedia = userPreferences.autoExpandMedia @@ -93,7 +93,7 @@ struct ContentSettingsView: View { } } } - .onChange(of: userPreferences.postVisibility) { _ in + .onChange(of: userPreferences.postVisibility) { userPreferences.conformReplyVisibilityConstraints() } diff --git a/IceCubesApp/App/Tabs/Settings/DisplaySettingsView.swift b/IceCubesApp/App/Tabs/Settings/DisplaySettingsView.swift index fd817676..77d73067 100644 --- a/IceCubesApp/App/Tabs/Settings/DisplaySettingsView.swift +++ b/IceCubesApp/App/Tabs/Settings/DisplaySettingsView.swift @@ -3,47 +3,20 @@ import DesignSystem import Env import Models import Network +import Observation import Status import SwiftUI -class DisplaySettingsLocalValues: ObservableObject { - @Published var tintColor = Theme.shared.tintColor - @Published var primaryBackgroundColor = Theme.shared.primaryBackgroundColor - @Published var secondaryBackgroundColor = Theme.shared.secondaryBackgroundColor - @Published var labelColor = Theme.shared.labelColor - @Published var lineSpacing = Theme.shared.lineSpacing - @Published var fontSizeScale = Theme.shared.fontSizeScale +@Observable class DisplaySettingsLocalValues { + var tintColor = Theme.shared.tintColor + var primaryBackgroundColor = Theme.shared.primaryBackgroundColor + var secondaryBackgroundColor = Theme.shared.secondaryBackgroundColor + var labelColor = Theme.shared.labelColor + var lineSpacing = Theme.shared.lineSpacing + var fontSizeScale = Theme.shared.fontSizeScale - private let debouncesDelay: DispatchQueue.SchedulerTimeType.Stride = .seconds(0.5) - private var subscriptions = Set() - - init() { - $tintColor - .debounce(for: debouncesDelay, scheduler: DispatchQueue.main) - .sink(receiveValue: { newColor in Theme.shared.tintColor = newColor }) - .store(in: &subscriptions) - $primaryBackgroundColor - .debounce(for: debouncesDelay, scheduler: DispatchQueue.main) - .sink(receiveValue: { newColor in Theme.shared.primaryBackgroundColor = newColor }) - .store(in: &subscriptions) - $secondaryBackgroundColor - .debounce(for: debouncesDelay, scheduler: DispatchQueue.main) - .sink(receiveValue: { newColor in Theme.shared.secondaryBackgroundColor = newColor }) - .store(in: &subscriptions) - $labelColor - .debounce(for: debouncesDelay, scheduler: DispatchQueue.main) - .sink(receiveValue: { newColor in Theme.shared.labelColor = newColor }) - .store(in: &subscriptions) - $lineSpacing - .debounce(for: debouncesDelay, scheduler: DispatchQueue.main) - .sink(receiveValue: { newSpacing in Theme.shared.lineSpacing = newSpacing }) - .store(in: &subscriptions) - $fontSizeScale - .debounce(for: debouncesDelay, scheduler: DispatchQueue.main) - .sink(receiveValue: { newScale in Theme.shared.fontSizeScale = newScale }) - .store(in: &subscriptions) - } + init() { } } struct DisplaySettingsView: View { @@ -53,7 +26,7 @@ struct DisplaySettingsView: View { @EnvironmentObject private var theme: Theme @EnvironmentObject private var userPreferences: UserPreferences - @StateObject private var localValues = DisplaySettingsLocalValues() + @State private var localValues = DisplaySettingsLocalValues() @State private var isFontSelectorPresented = false @@ -64,7 +37,7 @@ struct DisplaySettingsView: View { var body: some View { ZStack(alignment: .top) { Form { - StatusRowView(viewModel: { previewStatusViewModel }) + StatusRowView(viewModel: previewStatusViewModel) .allowsHitTesting(false) .opacity(0) .hidden() @@ -77,13 +50,37 @@ struct DisplaySettingsView: View { .navigationTitle("settings.display.navigation-title") .scrollContentBackground(.hidden) .background(theme.secondaryBackgroundColor) + .task(id: localValues.tintColor) { + do { try await Task.sleep(for: .microseconds(500)) } catch { } + Theme.shared.tintColor = localValues.tintColor + } + .task(id: localValues.primaryBackgroundColor) { + do { try await Task.sleep(for: .microseconds(500)) } catch { } + Theme.shared.primaryBackgroundColor = localValues.primaryBackgroundColor + } + .task(id: localValues.secondaryBackgroundColor) { + do { try await Task.sleep(for: .microseconds(500)) } catch { } + Theme.shared.secondaryBackgroundColor = localValues.secondaryBackgroundColor + } + .task(id: localValues.labelColor) { + do { try await Task.sleep(for: .microseconds(500)) } catch { } + Theme.shared.labelColor = localValues.labelColor + } + .task(id: localValues.lineSpacing) { + do { try await Task.sleep(for: .microseconds(500)) } catch { } + Theme.shared.lineSpacing = localValues.lineSpacing + } + .task(id: localValues.fontSizeScale) { + do { try await Task.sleep(for: .microseconds(500)) } catch { } + Theme.shared.fontSizeScale = localValues.fontSizeScale + } examplePost } } private var examplePost: some View { VStack(spacing: 0) { - StatusRowView(viewModel: { previewStatusViewModel }) + StatusRowView(viewModel: previewStatusViewModel) .allowsHitTesting(false) .padding(.layoutPadding) .background(theme.primaryBackgroundColor) @@ -111,7 +108,7 @@ struct DisplaySettingsView: View { } .disabled(theme.followSystemColorScheme) .opacity(theme.followSystemColorScheme ? 0.5 : 1.0) - .onChange(of: theme.selectedSet) { _ in + .onChange(of: theme.selectedSet) { localValues.tintColor = theme.tintColor localValues.primaryBackgroundColor = theme.primaryBackgroundColor localValues.secondaryBackgroundColor = theme.secondaryBackgroundColor diff --git a/IceCubesApp/App/Tabs/Settings/PushNotificationsView.swift b/IceCubesApp/App/Tabs/Settings/PushNotificationsView.swift index d9ea66ae..c37eb556 100644 --- a/IceCubesApp/App/Tabs/Settings/PushNotificationsView.swift +++ b/IceCubesApp/App/Tabs/Settings/PushNotificationsView.swift @@ -9,10 +9,10 @@ import UserNotifications struct PushNotificationsView: View { @EnvironmentObject private var theme: Theme - @EnvironmentObject private var appAccountsManager: AppAccountsManager - @EnvironmentObject private var pushNotifications: PushNotificationsService + @Environment(AppAccountsManager.self) private var appAccountsManager + @Environment(PushNotificationsService.self) private var pushNotifications - @StateObject public var subscription: PushNotificationSubscriptionSettings + @State public var subscription: PushNotificationSubscriptionSettings var body: some View { Form { diff --git a/IceCubesApp/App/Tabs/Settings/SettingsTab.swift b/IceCubesApp/App/Tabs/Settings/SettingsTab.swift index 9be59060..730343d4 100644 --- a/IceCubesApp/App/Tabs/Settings/SettingsTab.swift +++ b/IceCubesApp/App/Tabs/Settings/SettingsTab.swift @@ -12,15 +12,14 @@ import Timeline struct SettingsTabs: View { @Environment(\.dismiss) private var dismiss - @EnvironmentObject private var pushNotifications: PushNotificationsService + @Environment(PushNotificationsService.self) private var pushNotifications @EnvironmentObject private var preferences: UserPreferences - @EnvironmentObject private var client: Client - @EnvironmentObject private var currentInstance: CurrentInstance - @EnvironmentObject private var appAccountsManager: AppAccountsManager + @Environment(Client.self) private var client + @Environment(CurrentInstance.self) private var currentInstance + @Environment(AppAccountsManager.self) private var appAccountsManager @EnvironmentObject private var theme: Theme - @StateObject private var routerPath = RouterPath() - + @State private var routerPath = RouterPath() @State private var addAccountSheetPresented = false @State private var isEditingAccount = false @State private var cachedRemoved = false @@ -67,9 +66,9 @@ struct SettingsTabs: View { } } .withSafariRouter() - .environmentObject(routerPath) - .onChange(of: $popToRootTab.wrappedValue) { popToRootTab in - if popToRootTab == .notifications { + .environment(routerPath) + .onChange(of: $popToRootTab.wrappedValue) { _, newValue in + if newValue == .notifications { routerPath.path = [] } } diff --git a/IceCubesApp/App/Tabs/Settings/SwipeActionsSettingsView.swift b/IceCubesApp/App/Tabs/Settings/SwipeActionsSettingsView.swift index 9e23eeb9..f5395177 100644 --- a/IceCubesApp/App/Tabs/Settings/SwipeActionsSettingsView.swift +++ b/IceCubesApp/App/Tabs/Settings/SwipeActionsSettingsView.swift @@ -14,7 +14,7 @@ struct SwipeActionsSettingsView: View { createStatusActionPicker(selection: $userPreferences.swipeActionsStatusLeadingLeft, label: "settings.swipeactions.primary") - .onChange(of: userPreferences.swipeActionsStatusLeadingLeft) { action in + .onChange(of: userPreferences.swipeActionsStatusLeadingLeft) { _, action in if action == .none { userPreferences.swipeActionsStatusLeadingRight = .none } @@ -29,7 +29,7 @@ struct SwipeActionsSettingsView: View { createStatusActionPicker(selection: $userPreferences.swipeActionsStatusTrailingRight, label: "settings.swipeactions.primary") - .onChange(of: userPreferences.swipeActionsStatusTrailingRight) { action in + .onChange(of: userPreferences.swipeActionsStatusTrailingRight) { _, action in if action == .none { userPreferences.swipeActionsStatusTrailingLeft = .none } diff --git a/IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift b/IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift index 7b31bb92..5de53e2e 100644 --- a/IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift +++ b/IceCubesApp/App/Tabs/Settings/TranslationSettingsView.swift @@ -52,7 +52,9 @@ struct TranslationSettingsView: View { .navigationTitle("settings.translation.navigation-title") .scrollContentBackground(.hidden) .background(theme.secondaryBackgroundColor) - .onChange(of: apiKey, perform: writeNewValue) + .onChange(of: apiKey) { + writeNewValue() + } .onAppear(perform: updatePrefs) } diff --git a/IceCubesApp/App/Tabs/Timeline/AddRemoteTimelineView.swift b/IceCubesApp/App/Tabs/Timeline/AddRemoteTimelineView.swift index bba68ad4..228e9fe6 100644 --- a/IceCubesApp/App/Tabs/Timeline/AddRemoteTimelineView.swift +++ b/IceCubesApp/App/Tabs/Timeline/AddRemoteTimelineView.swift @@ -58,7 +58,7 @@ struct AddRemoteTimelineView: View { Button("action.cancel", action: { dismiss() }) } } - .onChange(of: instanceName) { newValue in + .onChange(of: instanceName) { _, newValue in instanceNamePublisher.send(newValue) } .onReceive(instanceNamePublisher.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)) { newValue in diff --git a/IceCubesApp/App/Tabs/Timeline/EditTagGroupView.swift b/IceCubesApp/App/Tabs/Timeline/EditTagGroupView.swift index 91626021..99095801 100644 --- a/IceCubesApp/App/Tabs/Timeline/EditTagGroupView.swift +++ b/IceCubesApp/App/Tabs/Timeline/EditTagGroupView.swift @@ -93,7 +93,7 @@ struct EditTagGroupView: View { .onSubmit { focusedField = Focus.new } - .onChange(of: sfSymbolName) { _ in + .onChange(of: sfSymbolName) { popupTagsPresented = true } diff --git a/IceCubesApp/App/Tabs/Timeline/TimelineTab.swift b/IceCubesApp/App/Tabs/Timeline/TimelineTab.swift index 7e44753c..8a99c10c 100644 --- a/IceCubesApp/App/Tabs/Timeline/TimelineTab.swift +++ b/IceCubesApp/App/Tabs/Timeline/TimelineTab.swift @@ -8,12 +8,12 @@ import SwiftUI import Timeline struct TimelineTab: View { - @EnvironmentObject private var appAccount: AppAccountsManager + @Environment(AppAccountsManager.self) private var appAccount @EnvironmentObject private var theme: Theme - @EnvironmentObject private var currentAccount: CurrentAccount + @Environment(CurrentAccount.self) private var currentAccount @EnvironmentObject private var preferences: UserPreferences - @EnvironmentObject private var client: Client - @StateObject private var routerPath = RouterPath() + @Environment(Client.self) private var client + @State private var routerPath = RouterPath() @Binding var popToRootTab: Tab @State private var didAppear: Bool = false @@ -58,22 +58,22 @@ struct TimelineTab: View { routerPath.presentedSheet = .addAccount } } - .onChange(of: client.isAuth, perform: { _ in + .onChange(of: client.isAuth) { if client.isAuth { timeline = lastTimelineFilter } else { timeline = .federated } - }) - .onChange(of: currentAccount.account?.id, perform: { _ in + } + .onChange(of: currentAccount.account?.id) { if client.isAuth, canFilterTimeline { timeline = lastTimelineFilter } else { timeline = .federated } - }) - .onChange(of: $popToRootTab.wrappedValue) { popToRootTab in - if popToRootTab == .timeline { + } + .onChange(of: $popToRootTab.wrappedValue) { _, newValue in + if newValue == .timeline { if routerPath.path.isEmpty { scrollToTopSignal += 1 } else { @@ -81,16 +81,16 @@ struct TimelineTab: View { } } } - .onChange(of: client.id) { _ in + .onChange(of: client.id) { routerPath.path = [] } - .onChange(of: timeline) { timeline in - if timeline == .home || timeline == .federated || timeline == .local { - lastTimelineFilter = timeline + .onChange(of: timeline) { _, newValue in + if client.isAuth, newValue == .home || newValue == .federated || newValue == .local { + lastTimelineFilter = newValue } } .withSafariRouter() - .environmentObject(routerPath) + .environment(routerPath) } @ViewBuilder diff --git a/IceCubesShareExtension/ShareViewController.swift b/IceCubesShareExtension/ShareViewController.swift index 15780371..b9a8ed8c 100644 --- a/IceCubesShareExtension/ShareViewController.swift +++ b/IceCubesShareExtension/ShareViewController.swift @@ -28,11 +28,11 @@ class ShareViewController: UIViewController { if let attachments = item.attachments { let view = StatusEditorView(mode: .shareExtension(items: attachments)) .environmentObject(UserPreferences.shared) - .environmentObject(appAccountsManager) - .environmentObject(client) - .environmentObject(account) + .environment(appAccountsManager) + .environment(client) + .environment(account) .environmentObject(theme) - .environmentObject(instance) + .environment(instance) .tint(theme.tintColor) .preferredColorScheme(colorScheme == .light ? .light : .dark) let childView = UIHostingController(rootView: view) diff --git a/Packages/Account/Package.swift b/Packages/Account/Package.swift index 64e50a98..ada63506 100644 --- a/Packages/Account/Package.swift +++ b/Packages/Account/Package.swift @@ -7,7 +7,7 @@ let package = Package( name: "Account", defaultLocalization: "en", platforms: [ - .iOS(.v16), + .iOS(.v17), ], products: [ .library( diff --git a/Packages/Account/Sources/Account/AccountDetailContextMenu.swift b/Packages/Account/Sources/Account/AccountDetailContextMenu.swift index 86bb6b3b..ce11c899 100644 --- a/Packages/Account/Sources/Account/AccountDetailContextMenu.swift +++ b/Packages/Account/Sources/Account/AccountDetailContextMenu.swift @@ -3,12 +3,12 @@ import Network import SwiftUI public struct AccountDetailContextMenu: View { - @EnvironmentObject private var client: Client - @EnvironmentObject private var routerPath: RouterPath - @EnvironmentObject private var currentInstance: CurrentInstance + @Environment(Client.self) private var client + @Environment(RouterPath.self) private var routerPath + @Environment(CurrentInstance.self) private var currentInstance @EnvironmentObject private var preferences: UserPreferences - @ObservedObject var viewModel: AccountDetailViewModel + var viewModel: AccountDetailViewModel public var body: some View { if let account = viewModel.account { diff --git a/Packages/Account/Sources/Account/AccountDetailHeaderView.swift b/Packages/Account/Sources/Account/AccountDetailHeaderView.swift index 7eccd4fc..5180a853 100644 --- a/Packages/Account/Sources/Account/AccountDetailHeaderView.swift +++ b/Packages/Account/Sources/Account/AccountDetailHeaderView.swift @@ -12,13 +12,13 @@ struct AccountDetailHeaderView: View { } @EnvironmentObject private var theme: Theme - @EnvironmentObject private var quickLook: QuickLook - @EnvironmentObject private var routerPath: RouterPath - @EnvironmentObject private var currentAccount: CurrentAccount + @Environment(QuickLook.self) private var quickLook + @Environment(RouterPath.self) private var routerPath + @Environment(CurrentAccount.self) private var currentAccount @Environment(\.redactionReasons) private var reasons @Environment(\.isSupporter) private var isSupporter: Bool - @ObservedObject var viewModel: AccountDetailViewModel + var viewModel: AccountDetailViewModel let account: Account let scrollViewProxy: ScrollViewProxy? diff --git a/Packages/Account/Sources/Account/AccountDetailView.swift b/Packages/Account/Sources/Account/AccountDetailView.swift index 0e094e68..895f24bf 100644 --- a/Packages/Account/Sources/Account/AccountDetailView.swift +++ b/Packages/Account/Sources/Account/AccountDetailView.swift @@ -11,15 +11,15 @@ public struct AccountDetailView: View { @Environment(\.openURL) private var openURL @Environment(\.redactionReasons) private var reasons - @EnvironmentObject private var watcher: StreamWatcher - @EnvironmentObject private var currentAccount: CurrentAccount - @EnvironmentObject private var currentInstance: CurrentInstance + @Environment(StreamWatcher.self) private var watcher + @Environment(CurrentAccount.self) private var currentAccount + @Environment(CurrentInstance.self) private var currentInstance @EnvironmentObject private var preferences: UserPreferences @EnvironmentObject private var theme: Theme - @EnvironmentObject private var client: Client - @EnvironmentObject private var routerPath: RouterPath + @Environment(Client.self) private var client + @Environment(RouterPath.self) private var routerPath - @StateObject private var viewModel: AccountDetailViewModel + @State private var viewModel: AccountDetailViewModel @State private var isCurrentUser: Bool = false @State private var isCreateListAlertPresented: Bool = false @State private var createListTitle: String = "" @@ -30,12 +30,12 @@ public struct AccountDetailView: View { /// When coming from a URL like a mention tap in a status. public init(accountId: String) { - _viewModel = StateObject(wrappedValue: .init(accountId: accountId)) + _viewModel = .init(initialValue: .init(accountId: accountId)) } /// When the account is already fetched by the parent caller. public init(account: Account) { - _viewModel = StateObject(wrappedValue: .init(account: account)) + _viewModel = .init(initialValue: .init(account: account)) } public var body: some View { @@ -114,21 +114,21 @@ public struct AccountDetailView: View { SoundEffectManager.shared.playSound(of: .refresh) } } - .onChange(of: watcher.latestEvent?.id) { _ in + .onChange(of: watcher.latestEvent?.id) { if let latestEvent = watcher.latestEvent, viewModel.accountId == currentAccount.account?.id { viewModel.handleEvent(event: latestEvent, currentAccount: currentAccount) } } - .onChange(of: isEditingAccount, perform: { isEditing in - if !isEditing { + .onChange(of: isEditingAccount) { _, newValue in + if !newValue { Task { await viewModel.fetchAccount() await preferences.refreshServerPreferences() } } - }) + } .sheet(isPresented: $isEditingAccount, content: { EditAccountView() }) @@ -292,7 +292,7 @@ public struct AccountDetailView: View { .listRowSeparator(.hidden) .listRowBackground(theme.primaryBackgroundColor) ForEach(viewModel.pinned) { status in - StatusRowView(viewModel: { .init(status: status, client: client, routerPath: routerPath) }) + StatusRowView(viewModel: .init(status: status, client: client, routerPath: routerPath)) } Rectangle() .fill(theme.secondaryBackgroundColor) diff --git a/Packages/Account/Sources/Account/AccountDetailViewModel.swift b/Packages/Account/Sources/Account/AccountDetailViewModel.swift index 23ba94ab..52d2a932 100644 --- a/Packages/Account/Sources/Account/AccountDetailViewModel.swift +++ b/Packages/Account/Sources/Account/AccountDetailViewModel.swift @@ -1,11 +1,12 @@ import Env import Models import Network +import Observation import Status import SwiftUI @MainActor -class AccountDetailViewModel: ObservableObject, StatusesFetcher { +@Observable class AccountDetailViewModel: StatusesFetcher { let accountId: String var client: Client? var isCurrentUser: Bool = false @@ -56,8 +57,8 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher { case lists } - @Published var accountState: AccountState = .loading - @Published var tabState: TabState = .statuses(statusesState: .loading) { + var accountState: AccountState = .loading + var tabState: TabState = .statuses(statusesState: .loading) { didSet { /// Forward viewModel tabState related to statusesState to statusesState property /// for `StatusesFetcher` conformance as we wrap StatusesState in TabState @@ -70,18 +71,18 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher { } } - @Published var statusesState: StatusesState = .loading + var statusesState: StatusesState = .loading - @Published var relationship: Relationship? - @Published var pinned: [Status] = [] - @Published var favorites: [Status] = [] - @Published var bookmarks: [Status] = [] + var relationship: Relationship? + var pinned: [Status] = [] + var favorites: [Status] = [] + var bookmarks: [Status] = [] private var favoritesNextPage: LinkHandler? private var bookmarksNextPage: LinkHandler? - @Published var featuredTags: [FeaturedTag] = [] - @Published var fields: [Account.Field] = [] - @Published var familiarFollowers: [Account] = [] - @Published var selectedTab = Tab.statuses { + var featuredTags: [FeaturedTag] = [] + var fields: [Account.Field] = [] + var familiarFollowers: [Account] = [] + var selectedTab = Tab.statuses { didSet { switch selectedTab { case .statuses, .postsAndReplies, .media: @@ -95,8 +96,8 @@ class AccountDetailViewModel: ObservableObject, StatusesFetcher { } } - @Published var translation: Translation? - @Published var isLoadingTranslation = false + var translation: Translation? + var isLoadingTranslation = false private(set) var account: Account? private var tabTask: Task? diff --git a/Packages/Account/Sources/Account/AccountsList/AccountsListRow.swift b/Packages/Account/Sources/Account/AccountsList/AccountsListRow.swift index f507c877..e9272c0f 100644 --- a/Packages/Account/Sources/Account/AccountsList/AccountsListRow.swift +++ b/Packages/Account/Sources/Account/AccountsList/AccountsListRow.swift @@ -4,14 +4,15 @@ import EmojiText import Env import Models import Network +import Observation import SwiftUI @MainActor -public class AccountsListRowViewModel: ObservableObject { +@Observable public class AccountsListRowViewModel { var client: Client? - @Published var account: Account - @Published var relationShip: Relationship? + var account: Account + var relationShip: Relationship? public init(account: Account, relationShip: Relationship? = nil) { self.account = account @@ -21,11 +22,11 @@ public class AccountsListRowViewModel: ObservableObject { public struct AccountsListRow: View { @EnvironmentObject private var theme: Theme - @EnvironmentObject private var currentAccount: CurrentAccount - @EnvironmentObject private var routerPath: RouterPath - @EnvironmentObject private var client: Client + @Environment(CurrentAccount.self) private var currentAccount + @Environment(RouterPath.self) private var routerPath + @Environment(Client.self) private var client - @StateObject var viewModel: AccountsListRowViewModel + @State var viewModel: AccountsListRowViewModel @State private var isEditingRelationshipNote: Bool = false @@ -33,7 +34,7 @@ public struct AccountsListRow: View { let requestUpdated: (() -> Void)? public init(viewModel: AccountsListRowViewModel, isFollowRequest: Bool = false, requestUpdated: (() -> Void)? = nil) { - _viewModel = StateObject(wrappedValue: viewModel) + self.viewModel = viewModel self.isFollowRequest = isFollowRequest self.requestUpdated = requestUpdated } @@ -117,8 +118,8 @@ public struct AccountsListRow: View { .scrollContentBackground(.hidden) .background(theme.primaryBackgroundColor) .environmentObject(theme) - .environmentObject(currentAccount) - .environmentObject(client) + .environment(currentAccount) + .environment(client) } } } diff --git a/Packages/Account/Sources/Account/AccountsList/AccountsListView.swift b/Packages/Account/Sources/Account/AccountsList/AccountsListView.swift index 233b3a8a..dd1df8df 100644 --- a/Packages/Account/Sources/Account/AccountsList/AccountsListView.swift +++ b/Packages/Account/Sources/Account/AccountsList/AccountsListView.swift @@ -7,13 +7,13 @@ import SwiftUI public struct AccountsListView: View { @EnvironmentObject private var theme: Theme - @EnvironmentObject private var client: Client - @EnvironmentObject private var currentAccount: CurrentAccount - @StateObject private var viewModel: AccountsListViewModel + @Environment(Client.self) private var client + @Environment(CurrentAccount.self) private var currentAccount + @State private var viewModel: AccountsListViewModel @State private var didAppear: Bool = false public init(mode: AccountsListMode) { - _viewModel = StateObject(wrappedValue: .init(mode: mode)) + _viewModel = .init(initialValue: .init(mode: mode)) } public var body: some View { diff --git a/Packages/Account/Sources/Account/AccountsList/AccountsListViewModel.swift b/Packages/Account/Sources/Account/AccountsList/AccountsListViewModel.swift index 31a163f2..4204519e 100644 --- a/Packages/Account/Sources/Account/AccountsList/AccountsListViewModel.swift +++ b/Packages/Account/Sources/Account/AccountsList/AccountsListViewModel.swift @@ -1,5 +1,6 @@ import Models import Network +import Observation import SwiftUI public enum AccountsListMode { @@ -24,7 +25,7 @@ public enum AccountsListMode { } @MainActor -class AccountsListViewModel: ObservableObject { +@Observable class AccountsListViewModel { var client: Client? let mode: AccountsListMode @@ -44,7 +45,7 @@ class AccountsListViewModel: ObservableObject { private var accounts: [Account] = [] private var relationships: [Relationship] = [] - @Published var state = State.loading + var state = State.loading private var nextPageId: String? diff --git a/Packages/Account/Sources/Account/Edit/EditAccountView.swift b/Packages/Account/Sources/Account/Edit/EditAccountView.swift index 4c1b7ac3..76551951 100644 --- a/Packages/Account/Sources/Account/Edit/EditAccountView.swift +++ b/Packages/Account/Sources/Account/Edit/EditAccountView.swift @@ -5,10 +5,10 @@ import SwiftUI public struct EditAccountView: View { @Environment(\.dismiss) private var dismiss - @EnvironmentObject private var client: Client + @Environment(Client.self) private var client @EnvironmentObject private var theme: Theme - @StateObject private var viewModel = EditAccountViewModel() + @State private var viewModel = EditAccountViewModel() public init() {} diff --git a/Packages/Account/Sources/Account/Edit/EditAccountViewModel.swift b/Packages/Account/Sources/Account/Edit/EditAccountViewModel.swift index 775af22f..d52b6ef8 100644 --- a/Packages/Account/Sources/Account/Edit/EditAccountViewModel.swift +++ b/Packages/Account/Sources/Account/Edit/EditAccountViewModel.swift @@ -1,13 +1,14 @@ import Models import Network +import Observation import SwiftUI @MainActor -class EditAccountViewModel: ObservableObject { - class FieldEditViewModel: ObservableObject, Identifiable { +@Observable class EditAccountViewModel { + @Observable class FieldEditViewModel: Identifiable { let id = UUID().uuidString - @Published var name: String = "" - @Published var value: String = "" + var name: String = "" + var value: String = "" init(name: String, value: String) { self.name = name @@ -17,18 +18,18 @@ class EditAccountViewModel: ObservableObject { public var client: Client? - @Published var displayName: String = "" - @Published var note: String = "" - @Published var postPrivacy = Models.Visibility.pub - @Published var isSensitive: Bool = false - @Published var isBot: Bool = false - @Published var isLocked: Bool = false - @Published var isDiscoverable: Bool = false - @Published var fields: [FieldEditViewModel] = [] + var displayName: String = "" + var note: String = "" + var postPrivacy = Models.Visibility.pub + var isSensitive: Bool = false + var isBot: Bool = false + var isLocked: Bool = false + var isDiscoverable: Bool = false + var fields: [FieldEditViewModel] = [] - @Published var isLoading: Bool = true - @Published var isSaving: Bool = false - @Published var saveError: Bool = false + var isLoading: Bool = true + var isSaving: Bool = false + var saveError: Bool = false init() {} diff --git a/Packages/Account/Sources/Account/Edit/EditRelationshipNoteView.swift b/Packages/Account/Sources/Account/Edit/EditRelationshipNoteView.swift index f5efa04f..53926b40 100644 --- a/Packages/Account/Sources/Account/Edit/EditRelationshipNoteView.swift +++ b/Packages/Account/Sources/Account/Edit/EditRelationshipNoteView.swift @@ -5,12 +5,10 @@ import SwiftUI public struct EditRelationshipNoteView: View { @Environment(\.dismiss) private var dismiss @EnvironmentObject private var theme: Theme - @EnvironmentObject private var client: Client + @Environment(Client.self) private var client - // need this model to refresh after storing the new note on mastodon - var accountDetailViewModel: AccountDetailViewModel - - @StateObject private var viewModel = EditRelationshipNoteViewModel() + @State var accountDetailViewModel: AccountDetailViewModel + @State private var viewModel = EditRelationshipNoteViewModel() public var body: some View { NavigationStack { diff --git a/Packages/Account/Sources/Account/Edit/EditRelationshipNoteViewModel.swift b/Packages/Account/Sources/Account/Edit/EditRelationshipNoteViewModel.swift index 8b10f599..1f0efbf5 100644 --- a/Packages/Account/Sources/Account/Edit/EditRelationshipNoteViewModel.swift +++ b/Packages/Account/Sources/Account/Edit/EditRelationshipNoteViewModel.swift @@ -1,14 +1,15 @@ import Network +import Observation import SwiftUI @MainActor -class EditRelationshipNoteViewModel: ObservableObject { +@Observable class EditRelationshipNoteViewModel { public var note: String = "" public var relatedAccountId: String? public var client: Client? - @Published var isSaving: Bool = false - @Published var saveError: Bool = false + var isSaving: Bool = false + var saveError: Bool = false init() {} @@ -18,7 +19,7 @@ class EditRelationshipNoteViewModel: ObservableObject { { isSaving = true do { - let _ = try await client!.post(endpoint: Accounts.relationshipNote(id: relatedAccountId!, json: RelationshipNoteData(note: note))) + _ = try await client!.post(endpoint: Accounts.relationshipNote(id: relatedAccountId!, json: RelationshipNoteData(note: note))) } catch { isSaving = false saveError = true diff --git a/Packages/Account/Sources/Account/Filters/EditFilterView.swift b/Packages/Account/Sources/Account/Filters/EditFilterView.swift index d93454d9..9384cd56 100644 --- a/Packages/Account/Sources/Account/Filters/EditFilterView.swift +++ b/Packages/Account/Sources/Account/Filters/EditFilterView.swift @@ -8,8 +8,8 @@ struct EditFilterView: View { @Environment(\.dismiss) private var dismiss @EnvironmentObject private var theme: Theme - @EnvironmentObject private var account: CurrentAccount - @EnvironmentObject private var client: Client + @Environment(CurrentAccount.self) private var account + @Environment(Client.self) private var client @State private var isSavingFilter: Bool = false @State private var filter: ServerFilter? @@ -91,9 +91,9 @@ struct EditFilterView: View { Text(duration.description).tag(duration) } } - .onChange(of: expirySelection) { duration in - if duration != .custom { - expiresAt = Date(timeIntervalSinceNow: TimeInterval(duration.rawValue)) + .onChange(of: expirySelection) { _, newValue in + if newValue != .custom { + expiresAt = Date(timeIntervalSinceNow: TimeInterval(newValue.rawValue)) } } if expirySelection != .infinite { @@ -227,7 +227,7 @@ struct EditFilterView: View { } label: { EmptyView() } - .onChange(of: filterAction) { _ in + .onChange(of: filterAction) { Task { await saveFilter() } diff --git a/Packages/Account/Sources/Account/Filters/FiltersListView.swift b/Packages/Account/Sources/Account/Filters/FiltersListView.swift index ca56f2a8..fd1ed815 100644 --- a/Packages/Account/Sources/Account/Filters/FiltersListView.swift +++ b/Packages/Account/Sources/Account/Filters/FiltersListView.swift @@ -8,8 +8,8 @@ public struct FiltersListView: View { @Environment(\.dismiss) private var dismiss @EnvironmentObject private var theme: Theme - @EnvironmentObject private var account: CurrentAccount - @EnvironmentObject private var client: Client + @Environment(CurrentAccount.self) private var account + @Environment(Client.self) private var client @State private var isLoading: Bool = true @State private var filters: [ServerFilter] = [] diff --git a/Packages/Account/Sources/Account/Follow/FollowButton.swift b/Packages/Account/Sources/Account/Follow/FollowButton.swift index 9795f43f..8695d067 100644 --- a/Packages/Account/Sources/Account/Follow/FollowButton.swift +++ b/Packages/Account/Sources/Account/Follow/FollowButton.swift @@ -2,17 +2,18 @@ import Combine import Foundation import Models import Network +import Observation import SwiftUI @MainActor -public class FollowButtonViewModel: ObservableObject { +@Observable public class FollowButtonViewModel { var client: Client? public let accountId: String public let shouldDisplayNotify: Bool public let relationshipUpdated: (Relationship) -> Void - @Published public private(set) var relationship: Relationship - @Published public private(set) var isUpdating: Bool = false + public private(set) var relationship: Relationship + public private(set) var isUpdating: Bool = false public init(accountId: String, relationship: Relationship, @@ -75,11 +76,11 @@ public class FollowButtonViewModel: ObservableObject { } public struct FollowButton: View { - @EnvironmentObject private var client: Client - @StateObject private var viewModel: FollowButtonViewModel + @Environment(Client.self) private var client + @State private var viewModel: FollowButtonViewModel public init(viewModel: FollowButtonViewModel) { - _viewModel = StateObject(wrappedValue: viewModel) + _viewModel = .init(initialValue: viewModel) } public var body: some View { diff --git a/Packages/AppAccount/Package.swift b/Packages/AppAccount/Package.swift index d2a0cb0d..a2e4e44d 100644 --- a/Packages/AppAccount/Package.swift +++ b/Packages/AppAccount/Package.swift @@ -7,7 +7,7 @@ let package = Package( name: "AppAccount", defaultLocalization: "en", platforms: [ - .iOS(.v16), + .iOS(.v17), ], products: [ .library( diff --git a/Packages/AppAccount/Sources/AppAccount/AppAccountView.swift b/Packages/AppAccount/Sources/AppAccount/AppAccountView.swift index 13b8bce1..112fd452 100644 --- a/Packages/AppAccount/Sources/AppAccount/AppAccountView.swift +++ b/Packages/AppAccount/Sources/AppAccount/AppAccountView.swift @@ -5,14 +5,14 @@ import SwiftUI public struct AppAccountView: View { @EnvironmentObject private var theme: Theme - @EnvironmentObject private var routerPath: RouterPath - @EnvironmentObject private var appAccounts: AppAccountsManager + @Environment(RouterPath.self) private var routerPath + @Environment(AppAccountsManager.self) private var appAccounts @EnvironmentObject private var preferences: UserPreferences - @StateObject var viewModel: AppAccountViewModel + @State var viewModel: AppAccountViewModel public init(viewModel: AppAccountViewModel) { - _viewModel = .init(wrappedValue: viewModel) + self.viewModel = viewModel } public var body: some View { diff --git a/Packages/AppAccount/Sources/AppAccount/AppAccountViewModel.swift b/Packages/AppAccount/Sources/AppAccount/AppAccountViewModel.swift index 0f53294e..eb65f4c0 100644 --- a/Packages/AppAccount/Sources/AppAccount/AppAccountViewModel.swift +++ b/Packages/AppAccount/Sources/AppAccount/AppAccountViewModel.swift @@ -2,10 +2,11 @@ import Combine import DesignSystem import Models import Network +import Observation import SwiftUI @MainActor -public class AppAccountViewModel: ObservableObject { +@Observable public class AppAccountViewModel { private static var avatarsCache: [String: UIImage] = [:] private static var accountsCache: [String: Account] = [:] @@ -15,7 +16,7 @@ public class AppAccountViewModel: ObservableObject { let isInNavigation: Bool let showBadge: Bool - @Published var account: Account? { + var account: Account? { didSet { if let account { refreshAcct(account: account) diff --git a/Packages/AppAccount/Sources/AppAccount/AppAccountsManager.swift b/Packages/AppAccount/Sources/AppAccount/AppAccountsManager.swift index e1f90a0c..d0094448 100644 --- a/Packages/AppAccount/Sources/AppAccount/AppAccountsManager.swift +++ b/Packages/AppAccount/Sources/AppAccount/AppAccountsManager.swift @@ -2,14 +2,15 @@ import Combine import Env import Models import Network +import Observation import SwiftUI @MainActor -public class AppAccountsManager: ObservableObject { +@Observable public class AppAccountsManager { @AppStorage("latestCurrentAccountKey", store: UserPreferences.sharedDefault) public static var latestCurrentAccountKey: String = "" - @Published public var currentAccount: AppAccount { + public var currentAccount: AppAccount { didSet { Self.latestCurrentAccountKey = currentAccount.id currentClient = .init(server: currentAccount.server, @@ -17,8 +18,8 @@ public class AppAccountsManager: ObservableObject { } } - @Published public var availableAccounts: [AppAccount] - @Published public var currentClient: Client + public var availableAccounts: [AppAccount] + public var currentClient: Client public var pushAccounts: [PushAccount] { availableAccounts.filter { $0.oauthToken != nil } diff --git a/Packages/AppAccount/Sources/AppAccount/AppAccountsSelectorView.swift b/Packages/AppAccount/Sources/AppAccount/AppAccountsSelectorView.swift index 355a19dd..b26abe11 100644 --- a/Packages/AppAccount/Sources/AppAccount/AppAccountsSelectorView.swift +++ b/Packages/AppAccount/Sources/AppAccount/AppAccountsSelectorView.swift @@ -4,11 +4,11 @@ import SwiftUI public struct AppAccountsSelectorView: View { @EnvironmentObject private var preferences: UserPreferences - @EnvironmentObject private var currentAccount: CurrentAccount - @EnvironmentObject private var appAccounts: AppAccountsManager + @Environment(CurrentAccount.self) private var currentAccount + @Environment(AppAccountsManager.self) private var appAccounts @EnvironmentObject private var theme: Theme - @ObservedObject var routerPath: RouterPath + var routerPath: RouterPath @State private var accountsViewModel: [AppAccountViewModel] = [] @State private var isPresented: Bool = false @@ -61,7 +61,7 @@ public struct AppAccountsSelectorView: View { } } }) - .onChange(of: currentAccount.account?.id) { _ in + .onChange(of: currentAccount.account?.id) { refreshAccounts() } .onAppear { diff --git a/Packages/Conversations/Package.swift b/Packages/Conversations/Package.swift index cd769c5f..e113bb99 100644 --- a/Packages/Conversations/Package.swift +++ b/Packages/Conversations/Package.swift @@ -7,7 +7,7 @@ let package = Package( name: "Conversations", defaultLocalization: "en", platforms: [ - .iOS(.v16), + .iOS(.v17), ], products: [ .library( diff --git a/Packages/Conversations/Sources/Conversations/Detail/ConversationDetailView.swift b/Packages/Conversations/Sources/Conversations/Detail/ConversationDetailView.swift index cdb5b75b..b21f655c 100644 --- a/Packages/Conversations/Sources/Conversations/Detail/ConversationDetailView.swift +++ b/Packages/Conversations/Sources/Conversations/Detail/ConversationDetailView.swift @@ -10,14 +10,14 @@ public struct ConversationDetailView: View { static let bottomAnchor = "bottom" } - @EnvironmentObject private var quickLook: QuickLook - @EnvironmentObject private var routerPath: RouterPath - @EnvironmentObject private var currentAccount: CurrentAccount - @EnvironmentObject private var client: Client + @Environment(QuickLook.self) private var quickLook + @Environment(RouterPath.self) private var routerPath + @Environment(CurrentAccount.self) private var currentAccount + @Environment(Client.self) private var client @EnvironmentObject private var theme: Theme - @EnvironmentObject private var watcher: StreamWatcher + @Environment(StreamWatcher.self) private var watcher - @StateObject private var viewModel: ConversationDetailViewModel + @State private var viewModel: ConversationDetailViewModel @FocusState private var isMessageFieldFocused: Bool @@ -25,7 +25,7 @@ public struct ConversationDetailView: View { @State private var didAppear: Bool = false public init(conversation: Conversation) { - _viewModel = StateObject(wrappedValue: .init(conversation: conversation)) + _viewModel = .init(initialValue: .init(conversation: conversation)) } public var body: some View { @@ -85,7 +85,7 @@ public struct ConversationDetailView: View { } } } - .onChange(of: watcher.latestEvent?.id) { _ in + .onChange(of: watcher.latestEvent?.id) { if let latestEvent = watcher.latestEvent { viewModel.handleEvent(event: latestEvent) DispatchQueue.main.async { diff --git a/Packages/Conversations/Sources/Conversations/Detail/ConversationDetailViewModel.swift b/Packages/Conversations/Sources/Conversations/Detail/ConversationDetailViewModel.swift index 662f7b9e..3c6c4ab5 100644 --- a/Packages/Conversations/Sources/Conversations/Detail/ConversationDetailViewModel.swift +++ b/Packages/Conversations/Sources/Conversations/Detail/ConversationDetailViewModel.swift @@ -4,16 +4,16 @@ import Network import SwiftUI @MainActor -class ConversationDetailViewModel: ObservableObject { +@Observable class ConversationDetailViewModel { var client: Client? var conversation: Conversation - @Published var isLoadingMessages: Bool = true - @Published var messages: [Status] = [] + var isLoadingMessages: Bool = true + var messages: [Status] = [] - @Published var isSendingMessage: Bool = false - @Published var newMessageText: String = "" + var isSendingMessage: Bool = false + var newMessageText: String = "" init(conversation: Conversation) { self.conversation = conversation diff --git a/Packages/Conversations/Sources/Conversations/Detail/ConversationMessageView.swift b/Packages/Conversations/Sources/Conversations/Detail/ConversationMessageView.swift index bc95ebea..6d718784 100644 --- a/Packages/Conversations/Sources/Conversations/Detail/ConversationMessageView.swift +++ b/Packages/Conversations/Sources/Conversations/Detail/ConversationMessageView.swift @@ -6,10 +6,10 @@ import NukeUI import SwiftUI struct ConversationMessageView: View { - @EnvironmentObject private var quickLook: QuickLook - @EnvironmentObject private var routerPath: RouterPath - @EnvironmentObject private var currentAccount: CurrentAccount - @EnvironmentObject private var client: Client + @Environment(QuickLook.self) private var quickLook + @Environment(RouterPath.self) private var routerPath + @Environment(CurrentAccount.self) private var currentAccount + @Environment(Client.self) private var client @EnvironmentObject private var theme: Theme let message: Status diff --git a/Packages/Conversations/Sources/Conversations/List/ConversationsListRow.swift b/Packages/Conversations/Sources/Conversations/List/ConversationsListRow.swift index d2fae12e..581ff694 100644 --- a/Packages/Conversations/Sources/Conversations/List/ConversationsListRow.swift +++ b/Packages/Conversations/Sources/Conversations/List/ConversationsListRow.swift @@ -6,13 +6,13 @@ import Network import SwiftUI struct ConversationsListRow: View { - @EnvironmentObject private var client: Client - @EnvironmentObject private var routerPath: RouterPath + @Environment(Client.self) private var client + @Environment(RouterPath.self) private var routerPath @EnvironmentObject private var theme: Theme - @EnvironmentObject private var currentAccount: CurrentAccount + @Environment(CurrentAccount.self) private var currentAccount @Binding var conversation: Conversation - @ObservedObject var viewModel: ConversationsListViewModel + var viewModel: ConversationsListViewModel var body: some View { Button { diff --git a/Packages/Conversations/Sources/Conversations/List/ConversationsListView.swift b/Packages/Conversations/Sources/Conversations/List/ConversationsListView.swift index da34ae03..0d1a7c49 100644 --- a/Packages/Conversations/Sources/Conversations/List/ConversationsListView.swift +++ b/Packages/Conversations/Sources/Conversations/List/ConversationsListView.swift @@ -7,12 +7,12 @@ import SwiftUI public struct ConversationsListView: View { @EnvironmentObject private var preferences: UserPreferences - @EnvironmentObject private var routerPath: RouterPath - @EnvironmentObject private var watcher: StreamWatcher - @EnvironmentObject private var client: Client + @Environment(RouterPath.self) private var routerPath + @Environment(StreamWatcher.self) private var watcher + @Environment(Client.self) private var client @EnvironmentObject private var theme: Theme - @StateObject private var viewModel = ConversationsListViewModel() + @State private var viewModel = ConversationsListViewModel() public init() {} @@ -83,7 +83,7 @@ public struct ConversationsListView: View { SecondaryColumnToolbarItem() } } - .onChange(of: watcher.latestEvent?.id) { _ in + .onChange(of: watcher.latestEvent?.id) { if let latestEvent = watcher.latestEvent { viewModel.handleEvent(event: latestEvent) } diff --git a/Packages/Conversations/Sources/Conversations/List/ConversationsListViewModel.swift b/Packages/Conversations/Sources/Conversations/List/ConversationsListViewModel.swift index 805655d3..6f02f623 100644 --- a/Packages/Conversations/Sources/Conversations/List/ConversationsListViewModel.swift +++ b/Packages/Conversations/Sources/Conversations/List/ConversationsListViewModel.swift @@ -3,13 +3,13 @@ import Network import SwiftUI @MainActor -class ConversationsListViewModel: ObservableObject { +@Observable class ConversationsListViewModel { var client: Client? - @Published var isLoadingFirstPage: Bool = true - @Published var isLoadingNextPage: Bool = false - @Published var conversations: [Conversation] = [] - @Published var isError: Bool = false + var isLoadingFirstPage: Bool = true + var isLoadingNextPage: Bool = false + var conversations: [Conversation] = [] + var isError: Bool = false var nextPage: LinkHandler? diff --git a/Packages/DesignSystem/Package.swift b/Packages/DesignSystem/Package.swift index 2747103f..2e14ae6f 100644 --- a/Packages/DesignSystem/Package.swift +++ b/Packages/DesignSystem/Package.swift @@ -7,7 +7,7 @@ let package = Package( name: "DesignSystem", defaultLocalization: "en", platforms: [ - .iOS(.v16), + .iOS(.v17), ], products: [ .library( diff --git a/Packages/DesignSystem/Sources/DesignSystem/SceneDelegate.swift b/Packages/DesignSystem/Sources/DesignSystem/SceneDelegate.swift index bd5d84c4..ff71431a 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/SceneDelegate.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/SceneDelegate.swift @@ -1,7 +1,7 @@ import Combine import UIKit -public class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate { +@Observable public class SceneDelegate: NSObject, UIWindowSceneDelegate { public var window: UIWindow? public var windowWidth: CGFloat { diff --git a/Packages/DesignSystem/Sources/DesignSystem/ThemeApplier.swift b/Packages/DesignSystem/Sources/DesignSystem/ThemeApplier.swift index bc953e98..e8f6c202 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/ThemeApplier.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/ThemeApplier.swift @@ -41,16 +41,16 @@ struct ThemeApplier: ViewModifier { setWindowUserInterfaceStyle(from: theme.selectedScheme) setBarsColor(theme.primaryBackgroundColor) } - .onChange(of: theme.tintColor) { newValue in + .onChange(of: theme.tintColor) { _, newValue in setWindowTint(newValue) } - .onChange(of: theme.primaryBackgroundColor) { newValue in + .onChange(of: theme.primaryBackgroundColor) { _, newValue in setBarsColor(newValue) } - .onChange(of: theme.selectedScheme) { newValue in + .onChange(of: theme.selectedScheme) { _, newValue in setWindowUserInterfaceStyle(from: newValue) } - .onChange(of: colorScheme) { newColorScheme in + .onChange(of: colorScheme) { _, newColorScheme in if theme.followSystemColorScheme, let sets = availableColorsSets .first(where: { $0.light.name == theme.selectedSet || $0.dark.name == theme.selectedSet }) diff --git a/Packages/DesignSystem/Sources/DesignSystem/Views/AvatarView.swift b/Packages/DesignSystem/Sources/DesignSystem/Views/AvatarView.swift index b487ac43..1622abeb 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/Views/AvatarView.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/Views/AvatarView.swift @@ -3,6 +3,7 @@ import NukeUI import Shimmer import SwiftUI +@MainActor public struct AvatarView: View { @Environment(\.redactionReasons) private var reasons @EnvironmentObject private var theme: Theme diff --git a/Packages/DesignSystem/Sources/DesignSystem/Views/FollowRequestButtons.swift b/Packages/DesignSystem/Sources/DesignSystem/Views/FollowRequestButtons.swift index 6de2e53c..043cb5b0 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/Views/FollowRequestButtons.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/Views/FollowRequestButtons.swift @@ -3,7 +3,7 @@ import Models import SwiftUI public struct FollowRequestButtons: View { - @EnvironmentObject private var currentAccount: CurrentAccount + @Environment(CurrentAccount.self) private var currentAccount let account: Account let requestUpdated: (() -> Void)? diff --git a/Packages/DesignSystem/Sources/DesignSystem/Views/StatusEditorToolbarItem.swift b/Packages/DesignSystem/Sources/DesignSystem/Views/StatusEditorToolbarItem.swift index d9fe8586..d0d5fd54 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/Views/StatusEditorToolbarItem.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/Views/StatusEditorToolbarItem.swift @@ -24,7 +24,7 @@ public extension View { @MainActor public struct StatusEditorToolbarItem: ToolbarContent { - @EnvironmentObject private var routerPath: RouterPath + @Environment(RouterPath.self) private var routerPath let visibility: Models.Visibility diff --git a/Packages/DesignSystem/Sources/DesignSystem/Views/TagRowView.swift b/Packages/DesignSystem/Sources/DesignSystem/Views/TagRowView.swift index 0a09a6b9..6eed3c5b 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/Views/TagRowView.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/Views/TagRowView.swift @@ -3,7 +3,7 @@ import Models import SwiftUI public struct TagRowView: View { - @EnvironmentObject private var routerPath: RouterPath + @Environment(RouterPath.self) private var routerPath let tag: Tag diff --git a/Packages/DesignSystem/Sources/DesignSystem/Views/ThemePreviewView.swift b/Packages/DesignSystem/Sources/DesignSystem/Views/ThemePreviewView.swift index 61433928..54941829 100644 --- a/Packages/DesignSystem/Sources/DesignSystem/Views/ThemePreviewView.swift +++ b/Packages/DesignSystem/Sources/DesignSystem/Views/ThemePreviewView.swift @@ -81,7 +81,7 @@ struct ThemeBoxView: View { .onAppear { isSelected = theme.selectedSet.rawValue == color.name.rawValue } - .onChange(of: theme.selectedSet) { newValue in + .onChange(of: theme.selectedSet) { _, newValue in isSelected = newValue.rawValue == color.name.rawValue } .onTapGesture { diff --git a/Packages/Env/Package.swift b/Packages/Env/Package.swift index 390c934d..cfa53709 100644 --- a/Packages/Env/Package.swift +++ b/Packages/Env/Package.swift @@ -7,7 +7,7 @@ let package = Package( name: "Env", defaultLocalization: "en", platforms: [ - .iOS(.v16), + .iOS(.v17), ], products: [ .library( diff --git a/Packages/Env/Sources/Env/CurrentAccount.swift b/Packages/Env/Sources/Env/CurrentAccount.swift index b3db9321..15ea373b 100644 --- a/Packages/Env/Sources/Env/CurrentAccount.swift +++ b/Packages/Env/Sources/Env/CurrentAccount.swift @@ -2,18 +2,19 @@ import Combine import Foundation import Models import Network +import Observation @MainActor -public class CurrentAccount: ObservableObject { +@Observable public class CurrentAccount { private static var accountsCache: [String: Account] = [:] - @Published public private(set) var account: Account? - @Published public private(set) var lists: [List] = [] - @Published public private(set) var tags: [Tag] = [] - @Published public private(set) var followRequests: [Account] = [] - @Published public private(set) var isUpdating: Bool = false - @Published public private(set) var updatingFollowRequestAccountIds = Set() - @Published public private(set) var isLoadingAccount: Bool = false + public private(set) var account: Account? + public private(set) var lists: [List] = [] + public private(set) var tags: [Tag] = [] + public private(set) var followRequests: [Account] = [] + public private(set) var isUpdating: Bool = false + public private(set) var updatingFollowRequestAccountIds = Set() + public private(set) var isLoadingAccount: Bool = false private var client: Client? diff --git a/Packages/Env/Sources/Env/CurrentInstance.swift b/Packages/Env/Sources/Env/CurrentInstance.swift index 8535eb7e..41766cbe 100644 --- a/Packages/Env/Sources/Env/CurrentInstance.swift +++ b/Packages/Env/Sources/Env/CurrentInstance.swift @@ -2,10 +2,11 @@ import Combine import Foundation import Models import Network +import Observation @MainActor -public class CurrentInstance: ObservableObject { - @Published public private(set) var instance: Instance? +@Observable public class CurrentInstance { + public private(set) var instance: Instance? private var client: Client? diff --git a/Packages/Env/Sources/Env/CustomEnvValues.swift b/Packages/Env/Sources/Env/CustomEnvValues.swift index 6369cf38..9b298ce5 100644 --- a/Packages/Env/Sources/Env/CustomEnvValues.swift +++ b/Packages/Env/Sources/Env/CustomEnvValues.swift @@ -21,11 +21,11 @@ private struct IsSupporter: EnvironmentKey { static let defaultValue: Bool = false } -private struct IsStatusDetailLoaded: EnvironmentKey { +private struct IsStatusFocused: EnvironmentKey { static let defaultValue: Bool = false } -private struct IsStatusFocused: EnvironmentKey { +private struct IsStatusReplyToPrevious: EnvironmentKey { static let defaultValue: Bool = false } @@ -54,14 +54,14 @@ public extension EnvironmentValues { get { self[IsSupporter.self] } set { self[IsSupporter.self] = newValue } } - - var isStatusDetailLoaded: Bool { - get { self[IsStatusDetailLoaded.self] } - set { self[IsStatusDetailLoaded.self] = newValue } - } - + var isStatusFocused: Bool { get { self[IsStatusFocused.self] } set { self[IsStatusFocused.self] = newValue } } + + var isStatusReplyToPrevious: Bool { + get { self[IsStatusReplyToPrevious.self] } + set { self[IsStatusReplyToPrevious.self] = newValue } + } } diff --git a/Packages/Env/Sources/Env/PushNotificationsService.swift b/Packages/Env/Sources/Env/PushNotificationsService.swift index 636d01ca..17bb27a9 100644 --- a/Packages/Env/Sources/Env/PushNotificationsService.swift +++ b/Packages/Env/Sources/Env/PushNotificationsService.swift @@ -4,6 +4,7 @@ import Foundation import KeychainSwift import Models import Network +import Observation import SwiftUI import UserNotifications @@ -28,7 +29,7 @@ public struct HandledNotification: Equatable { } @MainActor -public class PushNotificationsService: NSObject, ObservableObject { +@Observable public class PushNotificationsService: NSObject { enum Constants { static let endpoint = "https://icecubesrelay.fly.dev" static let keychainAuthKey = "notifications_auth_key" @@ -39,9 +40,9 @@ public class PushNotificationsService: NSObject, ObservableObject { public private(set) var subscriptions: [PushNotificationSubscriptionSettings] = [] - @Published public var pushToken: Data? + public var pushToken: Data? - @Published public var handledNotification: HandledNotification? + public var handledNotification: HandledNotification? override init() { super.init() @@ -162,14 +163,14 @@ extension Data { } @MainActor -public class PushNotificationSubscriptionSettings: ObservableObject { - @Published public var isEnabled: Bool = true - @Published public var isFollowNotificationEnabled: Bool = true - @Published public var isFavoriteNotificationEnabled: Bool = true - @Published public var isReblogNotificationEnabled: Bool = true - @Published public var isMentionNotificationEnabled: Bool = true - @Published public var isPollNotificationEnabled: Bool = true - @Published public var isNewPostsNotificationEnabled: Bool = true +@Observable public class PushNotificationSubscriptionSettings { + public var isEnabled: Bool = true + public var isFollowNotificationEnabled: Bool = true + public var isFavoriteNotificationEnabled: Bool = true + public var isReblogNotificationEnabled: Bool = true + public var isMentionNotificationEnabled: Bool = true + public var isPollNotificationEnabled: Bool = true + public var isNewPostsNotificationEnabled: Bool = true public let account: PushAccount diff --git a/Packages/Env/Sources/Env/QuickLook.swift b/Packages/Env/Sources/Env/QuickLook.swift index b9b34ad7..ec5a90ac 100644 --- a/Packages/Env/Sources/Env/QuickLook.swift +++ b/Packages/Env/Sources/Env/QuickLook.swift @@ -3,8 +3,8 @@ import Combine import SwiftUI @MainActor -public class QuickLook: ObservableObject { - @Published public var url: URL? { +@Observable public class QuickLook { + public var url: URL? { didSet { if url == nil { cleanup(urls: urls) @@ -12,9 +12,9 @@ public class QuickLook: ObservableObject { } } - @Published public private(set) var urls: [URL] = [] - @Published public private(set) var isPreparing: Bool = false - @Published public private(set) var latestError: Error? + public private(set) var urls: [URL] = [] + public private(set) var isPreparing: Bool = false + public private(set) var latestError: Error? public init() {} diff --git a/Packages/Env/Sources/Env/Router.swift b/Packages/Env/Sources/Env/Router.swift index 57507d24..313e3b2e 100644 --- a/Packages/Env/Sources/Env/Router.swift +++ b/Packages/Env/Sources/Env/Router.swift @@ -2,6 +2,7 @@ import Combine import Foundation import Models import Network +import Observation import SwiftUI public enum RouterDestination: Hashable { @@ -69,12 +70,12 @@ public enum SheetDestination: Identifiable { } @MainActor -public class RouterPath: ObservableObject { +@Observable public class RouterPath { public var client: Client? public var urlHandler: ((URL) -> OpenURLAction.Result)? - @Published public var path: [RouterDestination] = [] - @Published public var presentedSheet: SheetDestination? + public var path: [RouterDestination] = [] + public var presentedSheet: SheetDestination? public init() {} diff --git a/Packages/Env/Sources/Env/StatusDataController.swift b/Packages/Env/Sources/Env/StatusDataController.swift index c221ea82..97c574b9 100644 --- a/Packages/Env/Sources/Env/StatusDataController.swift +++ b/Packages/Env/Sources/Env/StatusDataController.swift @@ -1,10 +1,11 @@ import Foundation import Models import Network +import Observation import SwiftUI @MainActor -public protocol StatusDataControlling: ObservableObject { +public protocol StatusDataControlling { var isReblogged: Bool { get set } var isBookmarked: Bool { get set } var isFavorited: Bool { get set } @@ -43,13 +44,13 @@ public final class StatusDataControllerProvider { for status in statuses { let realStatus: AnyStatus = status.reblog ?? status let controller = dataController(for: realStatus, client: client) - controller.updateFrom(status: realStatus, publishUpdate: false) + controller.updateFrom(status: realStatus) } } } @MainActor -public final class StatusDataController: StatusDataControlling { +@Observable public final class StatusDataController: StatusDataControlling { private let status: AnyStatus private let client: Client @@ -74,7 +75,7 @@ public final class StatusDataController: StatusDataControlling { favoritesCount = status.favouritesCount } - public func updateFrom(status: AnyStatus, publishUpdate: Bool) { + public func updateFrom(status: AnyStatus) { isReblogged = status.reblogged == true isBookmarked = status.bookmarked == true isFavorited = status.favourited == true @@ -82,10 +83,6 @@ public final class StatusDataController: StatusDataControlling { reblogsCount = status.reblogsCount repliesCount = status.repliesCount favoritesCount = status.favouritesCount - - if publishUpdate { - objectWillChange.send() - } } public func toggleFavorite(remoteStatus: String?) async { @@ -94,14 +91,12 @@ public final class StatusDataController: StatusDataControlling { let id = remoteStatus ?? status.id let endpoint = isFavorited ? Statuses.favorite(id: id) : Statuses.unfavorite(id: id) favoritesCount += isFavorited ? 1 : -1 - objectWillChange.send() do { let status: Status = try await client.post(endpoint: endpoint) - updateFrom(status: status, publishUpdate: true) + updateFrom(status: status) } catch { isFavorited.toggle() favoritesCount += isFavorited ? -1 : 1 - objectWillChange.send() } } @@ -111,14 +106,12 @@ public final class StatusDataController: StatusDataControlling { let id = remoteStatus ?? status.id let endpoint = isReblogged ? Statuses.reblog(id: id) : Statuses.unreblog(id: id) reblogsCount += isReblogged ? 1 : -1 - objectWillChange.send() do { let status: Status = try await client.post(endpoint: endpoint) - updateFrom(status: status.reblog ?? status, publishUpdate: true) + updateFrom(status: status.reblog ?? status) } catch { isReblogged.toggle() reblogsCount += isReblogged ? -1 : 1 - objectWillChange.send() } } @@ -127,13 +120,11 @@ public final class StatusDataController: StatusDataControlling { isBookmarked.toggle() let id = remoteStatus ?? status.id let endpoint = isBookmarked ? Statuses.bookmark(id: id) : Statuses.unbookmark(id: id) - objectWillChange.send() do { let status: Status = try await client.post(endpoint: endpoint) - updateFrom(status: status, publishUpdate: true) + updateFrom(status: status) } catch { isBookmarked.toggle() - objectWillChange.send() } } } diff --git a/Packages/Env/Sources/Env/StreamWatcher.swift b/Packages/Env/Sources/Env/StreamWatcher.swift index cf3dc1b4..0881fc2a 100644 --- a/Packages/Env/Sources/Env/StreamWatcher.swift +++ b/Packages/Env/Sources/Env/StreamWatcher.swift @@ -2,9 +2,10 @@ import Combine import Foundation import Models import Network +import Observation @MainActor -public class StreamWatcher: ObservableObject { +@Observable public class StreamWatcher { private var client: Client? private var task: URLSessionWebSocketTask? private var watchedStreams: [Stream] = [] @@ -21,9 +22,9 @@ public class StreamWatcher: ObservableObject { case direct } - @Published public var events: [any StreamEvent] = [] - @Published public var unreadNotificationsCount: Int = 0 - @Published public var latestEvent: (any StreamEvent)? + public var events: [any StreamEvent] = [] + public var unreadNotificationsCount: Int = 0 + public var latestEvent: (any StreamEvent)? public init() { decoder.keyDecodingStrategy = .convertFromSnakeCase diff --git a/Packages/Explore/Package.swift b/Packages/Explore/Package.swift index 08dd9b92..999db826 100644 --- a/Packages/Explore/Package.swift +++ b/Packages/Explore/Package.swift @@ -7,7 +7,7 @@ let package = Package( name: "Explore", defaultLocalization: "en", platforms: [ - .iOS(.v16), + .iOS(.v17), ], products: [ .library( diff --git a/Packages/Explore/Sources/Explore/ExploreView.swift b/Packages/Explore/Sources/Explore/ExploreView.swift index f8f84adc..eb811f72 100644 --- a/Packages/Explore/Sources/Explore/ExploreView.swift +++ b/Packages/Explore/Sources/Explore/ExploreView.swift @@ -9,10 +9,10 @@ import SwiftUI public struct ExploreView: View { @EnvironmentObject private var theme: Theme - @EnvironmentObject private var client: Client - @EnvironmentObject private var routerPath: RouterPath + @Environment(Client.self) private var client + @Environment(RouterPath.self) private var routerPath - @StateObject private var viewModel = ExploreViewModel() + @State private var viewModel = ExploreViewModel() public init() {} @@ -89,6 +89,12 @@ public struct ExploreView: View { Text(scope.localizedString) } } + .task(id: viewModel.searchQuery) { + do { + try await Task.sleep(for: .milliseconds(150)) + await viewModel.search() + } catch {} + } } private var quickAccessView: some View { @@ -117,7 +123,7 @@ public struct ExploreView: View { private var loadingView: some View { ForEach(Status.placeholders()) { status in - StatusRowView(viewModel: { .init(status: status, client: client, routerPath: routerPath) }) + StatusRowView(viewModel: .init(status: status, client: client, routerPath: routerPath)) .padding(.vertical, 8) .redacted(reason: .placeholder) .listRowBackground(theme.primaryBackgroundColor) @@ -148,7 +154,7 @@ public struct ExploreView: View { if !results.statuses.isEmpty, viewModel.searchScope == .all || viewModel.searchScope == .posts { Section("explore.section.posts") { ForEach(results.statuses) { status in - StatusRowView(viewModel: { .init(status: status, client: client, routerPath: routerPath) }) + StatusRowView(viewModel: .init(status: status, client: client, routerPath: routerPath)) .listRowBackground(theme.primaryBackgroundColor) .padding(.vertical, 8) } @@ -196,7 +202,7 @@ public struct ExploreView: View { ForEach(viewModel.trendingStatuses .prefix(upTo: viewModel.trendingStatuses.count > 3 ? 3 : viewModel.trendingStatuses.count)) { status in - StatusRowView(viewModel: { .init(status: status, client: client, routerPath: routerPath) }) + StatusRowView(viewModel: .init(status: status, client: client, routerPath: routerPath)) .listRowBackground(theme.primaryBackgroundColor) .padding(.vertical, 8) } diff --git a/Packages/Explore/Sources/Explore/ExploreViewModel.swift b/Packages/Explore/Sources/Explore/ExploreViewModel.swift index af19b021..adce23f4 100644 --- a/Packages/Explore/Sources/Explore/ExploreViewModel.swift +++ b/Packages/Explore/Sources/Explore/ExploreViewModel.swift @@ -1,10 +1,10 @@ -import Combine import Models import Network +import Observation import SwiftUI @MainActor -class ExploreViewModel: ObservableObject { +@Observable class ExploreViewModel { enum SearchScope: String, CaseIterable { case all, people, hashtags, posts @@ -39,34 +39,23 @@ class ExploreViewModel: ObservableObject { trendingLinks.isEmpty && trendingTags.isEmpty && trendingStatuses.isEmpty && suggestedAccounts.isEmpty } - @Published var searchQuery = "" { + var searchQuery = "" { didSet { isSearching = true } } - @Published var results: [String: SearchResults] = [:] - @Published var isLoaded = false - @Published var isSearching = false - @Published var suggestedAccounts: [Account] = [] - @Published var suggestedAccountsRelationShips: [Relationship] = [] - @Published var trendingTags: [Tag] = [] - @Published var trendingStatuses: [Status] = [] - @Published var trendingLinks: [Card] = [] - @Published var searchScope: SearchScope = .all + var results: [String: SearchResults] = [:] + var isLoaded = false + var isSearching = false + var suggestedAccounts: [Account] = [] + var suggestedAccountsRelationShips: [Relationship] = [] + var trendingTags: [Tag] = [] + var trendingStatuses: [Status] = [] + var trendingLinks: [Card] = [] + var searchScope: SearchScope = .all - private var searchTask: Task? - private var cancellables = Set() - - init() { - $searchQuery - .removeDuplicates() - .debounce(for: .milliseconds(250), scheduler: DispatchQueue.main) - .sink(receiveValue: { [weak self] _ in - self?.search() - }) - .store(in: &cancellables) - } + init() {} func fetchTrending() async { guard let client else { return } @@ -104,29 +93,24 @@ class ExploreViewModel: ObservableObject { trendingLinks: trendingLinks) } - func search() { - guard !searchQuery.isEmpty else { return } - isSearching = true - searchTask?.cancel() - searchTask = nil - searchTask = Task { - guard let client else { return } - do { - var results: SearchResults = try await client.get(endpoint: Search.search(query: searchQuery, - type: nil, - offset: nil, - following: nil), - forceVersion: .v2) - let relationships: [Relationship] = - try await client.get(endpoint: Accounts.relationships(ids: results.accounts.map(\.id))) - results.relationships = relationships - withAnimation { - self.results[searchQuery] = results - isSearching = false - } - } catch { + func search() async { + guard let client else { return } + do { + try await Task.sleep(for: .milliseconds(250)) + var results: SearchResults = try await client.get(endpoint: Search.search(query: searchQuery, + type: nil, + offset: nil, + following: nil), + forceVersion: .v2) + let relationships: [Relationship] = + try await client.get(endpoint: Accounts.relationships(ids: results.accounts.map(\.id))) + results.relationships = relationships + withAnimation { + self.results[searchQuery] = results isSearching = false } + } catch { + isSearching = false } } } diff --git a/Packages/Lists/Package.swift b/Packages/Lists/Package.swift index 50c43eae..8a4d27de 100644 --- a/Packages/Lists/Package.swift +++ b/Packages/Lists/Package.swift @@ -7,7 +7,7 @@ let package = Package( name: "Lists", defaultLocalization: "en", platforms: [ - .iOS(.v16), + .iOS(.v17), ], products: [ .library( diff --git a/Packages/Lists/Sources/Lists/AddAccounts/ListAddAccountView.swift b/Packages/Lists/Sources/Lists/AddAccounts/ListAddAccountView.swift index 956049d0..670da7ad 100644 --- a/Packages/Lists/Sources/Lists/AddAccounts/ListAddAccountView.swift +++ b/Packages/Lists/Sources/Lists/AddAccounts/ListAddAccountView.swift @@ -6,16 +6,16 @@ import SwiftUI public struct ListAddAccountView: View { @Environment(\.dismiss) private var dismiss - @EnvironmentObject private var client: Client + @Environment(Client.self) private var client @EnvironmentObject private var theme: Theme - @EnvironmentObject private var currentAccount: CurrentAccount - @StateObject private var viewModel: ListAddAccountViewModel + @Environment(CurrentAccount.self) private var currentAccount + @State private var viewModel: ListAddAccountViewModel @State private var isCreateListAlertPresented: Bool = false @State private var createListTitle: String = "" public init(account: Account) { - _viewModel = StateObject(wrappedValue: .init(account: account)) + _viewModel = .init(initialValue: .init(account: account)) } public var body: some View { diff --git a/Packages/Lists/Sources/Lists/AddAccounts/ListAddAccountViewModel.swift b/Packages/Lists/Sources/Lists/AddAccounts/ListAddAccountViewModel.swift index 910ae16a..e104f3b2 100644 --- a/Packages/Lists/Sources/Lists/AddAccounts/ListAddAccountViewModel.swift +++ b/Packages/Lists/Sources/Lists/AddAccounts/ListAddAccountViewModel.swift @@ -1,13 +1,14 @@ import Models import Network +import Observation import SwiftUI @MainActor -class ListAddAccountViewModel: ObservableObject { +@Observable class ListAddAccountViewModel { let account: Account - @Published var inLists: [Models.List] = [] - @Published var isLoadingInfo: Bool = true + var inLists: [Models.List] = [] + var isLoadingInfo: Bool = true var client: Client? diff --git a/Packages/Lists/Sources/Lists/Edit/ListEditView.swift b/Packages/Lists/Sources/Lists/Edit/ListEditView.swift index e4bbcf6d..5c7dc8db 100644 --- a/Packages/Lists/Sources/Lists/Edit/ListEditView.swift +++ b/Packages/Lists/Sources/Lists/Edit/ListEditView.swift @@ -7,12 +7,12 @@ import SwiftUI public struct ListEditView: View { @Environment(\.dismiss) private var dismiss @EnvironmentObject private var theme: Theme - @EnvironmentObject private var client: Client + @Environment(Client.self) private var client - @StateObject private var viewModel: ListEditViewModel + @State private var viewModel: ListEditViewModel public init(list: Models.List) { - _viewModel = StateObject(wrappedValue: .init(list: list)) + _viewModel = .init(initialValue: .init(list: list)) } public var body: some View { diff --git a/Packages/Lists/Sources/Lists/Edit/ListEditViewModel.swift b/Packages/Lists/Sources/Lists/Edit/ListEditViewModel.swift index 84598cbc..6198a2d3 100644 --- a/Packages/Lists/Sources/Lists/Edit/ListEditViewModel.swift +++ b/Packages/Lists/Sources/Lists/Edit/ListEditViewModel.swift @@ -1,16 +1,17 @@ import Combine import Models import Network +import Observation import SwiftUI @MainActor -public class ListEditViewModel: ObservableObject { +@Observable public class ListEditViewModel { let list: Models.List var client: Client? - @Published var isLoadingAccounts: Bool = true - @Published var accounts: [Account] = [] + var isLoadingAccounts: Bool = true + var accounts: [Account] = [] init(list: Models.List) { self.list = list diff --git a/Packages/Models/Package.swift b/Packages/Models/Package.swift index dd90f005..3de3587e 100644 --- a/Packages/Models/Package.swift +++ b/Packages/Models/Package.swift @@ -7,7 +7,7 @@ let package = Package( name: "Models", defaultLocalization: "en", platforms: [ - .iOS(.v16), + .iOS(.v17), ], products: [ .library( diff --git a/Packages/Network/Package.swift b/Packages/Network/Package.swift index abf2405f..b4702e64 100644 --- a/Packages/Network/Package.swift +++ b/Packages/Network/Package.swift @@ -7,7 +7,7 @@ let package = Package( name: "Network", defaultLocalization: "en", platforms: [ - .iOS(.v16), + .iOS(.v17), ], products: [ .library( diff --git a/Packages/Network/Sources/Network/Client.swift b/Packages/Network/Sources/Network/Client.swift index cc3a3dc8..f014e9c2 100644 --- a/Packages/Network/Sources/Network/Client.swift +++ b/Packages/Network/Sources/Network/Client.swift @@ -1,10 +1,11 @@ import Combine import Foundation import Models +import Observation import os import SwiftUI -public final class Client: ObservableObject, Equatable, Identifiable, Hashable { +@Observable public final class Client: Equatable, Identifiable, Hashable { public static func == (lhs: Client, rhs: Client) -> Bool { let lhsToken = lhs.critical.withLock { $0.oauthToken } let rhsToken = rhs.critical.withLock { $0.oauthToken } diff --git a/Packages/Notifications/Package.swift b/Packages/Notifications/Package.swift index 28f95a38..03dae33f 100644 --- a/Packages/Notifications/Package.swift +++ b/Packages/Notifications/Package.swift @@ -7,7 +7,7 @@ let package = Package( name: "Notifications", defaultLocalization: "en", platforms: [ - .iOS(.v16), + .iOS(.v17), ], products: [ .library( diff --git a/Packages/Notifications/Sources/Notifications/NotificationRowView.swift b/Packages/Notifications/Sources/Notifications/NotificationRowView.swift index 1c76a4ed..f0c8cde4 100644 --- a/Packages/Notifications/Sources/Notifications/NotificationRowView.swift +++ b/Packages/Notifications/Sources/Notifications/NotificationRowView.swift @@ -149,16 +149,16 @@ struct NotificationRowView: View { if let status = notification.status { HStack { if type == .mention { - StatusRowView(viewModel: { .init(status: status, - client: client, - routerPath: routerPath, - showActions: true) }) + StatusRowView(viewModel: .init(status: status, + client: client, + routerPath: routerPath, + showActions: true)) } else { - StatusRowView(viewModel: { .init(status: status, - client: client, - routerPath: routerPath, - showActions: false, - textDisabled: true) }) + StatusRowView(viewModel: .init(status: status, + client: client, + routerPath: routerPath, + showActions: false, + textDisabled: true)) .lineLimit(4) } Spacer() diff --git a/Packages/Notifications/Sources/Notifications/NotificationsListView.swift b/Packages/Notifications/Sources/Notifications/NotificationsListView.swift index e6ecf75e..a91d331b 100644 --- a/Packages/Notifications/Sources/Notifications/NotificationsListView.swift +++ b/Packages/Notifications/Sources/Notifications/NotificationsListView.swift @@ -8,11 +8,11 @@ import SwiftUI public struct NotificationsListView: View { @Environment(\.scenePhase) private var scenePhase @EnvironmentObject private var theme: Theme - @EnvironmentObject private var watcher: StreamWatcher - @EnvironmentObject private var client: Client - @EnvironmentObject private var routerPath: RouterPath - @EnvironmentObject private var account: CurrentAccount - @StateObject private var viewModel = NotificationsViewModel() + @Environment(StreamWatcher.self) private var watcher + @Environment(Client.self) private var client + @Environment(RouterPath.self) private var routerPath + @Environment(CurrentAccount.self) private var account + @State private var viewModel = NotificationsViewModel() let lockedType: Models.Notification.NotificationType? @@ -88,13 +88,13 @@ public struct NotificationsListView: View { HapticManager.shared.fireHaptic(of: .dataRefresh(intensity: 0.7)) SoundEffectManager.shared.playSound(of: .refresh) } - .onChange(of: watcher.latestEvent?.id, perform: { _ in + .onChange(of: watcher.latestEvent?.id) { if let latestEvent = watcher.latestEvent { viewModel.handleEvent(event: latestEvent) } - }) - .onChange(of: scenePhase, perform: { scenePhase in - switch scenePhase { + } + .onChange(of: scenePhase) { _, newValue in + switch newValue { case .active: Task { await viewModel.fetchNotifications() @@ -102,7 +102,7 @@ public struct NotificationsListView: View { default: break } - }) + } } @ViewBuilder diff --git a/Packages/Notifications/Sources/Notifications/NotificationsViewModel.swift b/Packages/Notifications/Sources/Notifications/NotificationsViewModel.swift index f6fcca88..31c7ae1d 100644 --- a/Packages/Notifications/Sources/Notifications/NotificationsViewModel.swift +++ b/Packages/Notifications/Sources/Notifications/NotificationsViewModel.swift @@ -2,10 +2,11 @@ import Env import Foundation import Models import Network +import Observation import SwiftUI @MainActor -class NotificationsViewModel: ObservableObject { +@Observable class NotificationsViewModel { public enum State { public enum PagingState { case none, hasNextPage, loadingNextPage @@ -35,8 +36,8 @@ class NotificationsViewModel: ObservableObject { var currentAccount: CurrentAccount? - @Published var state: State = .loading - @Published var selectedType: Models.Notification.NotificationType? { + var state: State = .loading + var selectedType: Models.Notification.NotificationType? { didSet { if oldValue != selectedType { consolidatedNotifications = [] diff --git a/Packages/Status/Package.swift b/Packages/Status/Package.swift index 0673ce45..bfe05744 100644 --- a/Packages/Status/Package.swift +++ b/Packages/Status/Package.swift @@ -7,7 +7,7 @@ let package = Package( name: "Status", defaultLocalization: "en", platforms: [ - .iOS(.v16), + .iOS(.v17), ], products: [ .library( diff --git a/Packages/Status/Sources/Status/Detail/StatusDetailView.swift b/Packages/Status/Sources/Status/Detail/StatusDetailView.swift index dc26bd19..3eb6b3bd 100644 --- a/Packages/Status/Sources/Status/Detail/StatusDetailView.swift +++ b/Packages/Status/Sources/Status/Detail/StatusDetailView.swift @@ -7,12 +7,12 @@ import SwiftUI public struct StatusDetailView: View { @EnvironmentObject private var theme: Theme - @EnvironmentObject private var currentAccount: CurrentAccount - @EnvironmentObject private var watcher: StreamWatcher - @EnvironmentObject private var client: Client - @EnvironmentObject private var routerPath: RouterPath + @Environment(CurrentAccount.self) private var currentAccount + @Environment(StreamWatcher.self) private var watcher + @Environment(Client.self) private var client + @Environment(RouterPath.self) private var routerPath - @StateObject private var viewModel: StatusDetailViewModel + @State private var viewModel: StatusDetailViewModel @State private var isLoaded: Bool = false @State private var statusHeight: CGFloat = 0 @@ -22,15 +22,15 @@ public struct StatusDetailView: View { @AccessibilityFocusState private var initialFocusBugWorkaround: Bool public init(statusId: String) { - _viewModel = StateObject(wrappedValue: { .init(statusId: statusId) }()) + _viewModel = .init(wrappedValue: .init(statusId: statusId)) } public init(status: Status) { - _viewModel = StateObject(wrappedValue: { .init(status: status) }()) + _viewModel = .init(wrappedValue: .init(status: status)) } public init(remoteStatusURL: URL) { - _viewModel = StateObject(wrappedValue: { .init(remoteStatusURL: remoteStatusURL) }()) + _viewModel = .init(wrappedValue: .init(remoteStatusURL: remoteStatusURL)) } public var body: some View { @@ -45,9 +45,8 @@ public struct StatusDetailView: View { case .loading: loadingDetailView - case let .display(statuses, date): - makeStatusesListView(statuses: statuses, date: date) - .id(date) + case let .display(statuses): + makeStatusesListView(statuses: statuses) if !isLoaded { loadingContextView @@ -69,12 +68,12 @@ public struct StatusDetailView: View { .listStyle(.plain) .scrollContentBackground(.hidden) .background(theme.primaryBackgroundColor) - .onChange(of: viewModel.scrollToId, perform: { scrollToId in - if let scrollToId { + .onChange(of: viewModel.scrollToId) { _, newValue in + if let newValue { viewModel.scrollToId = nil - proxy.scrollTo(scrollToId, anchor: .top) + proxy.scrollTo(newValue, anchor: .top) } - }) + } .task { guard !isLoaded else { return } viewModel.client = client @@ -92,7 +91,7 @@ public struct StatusDetailView: View { } } } - .onChange(of: watcher.latestEvent?.id) { _ in + .onChange(of: watcher.latestEvent?.id) { guard let lastEvent = watcher.latestEvent else { return } viewModel.handleEvent(event: lastEvent, currentAccount: currentAccount.account) } @@ -101,69 +100,35 @@ public struct StatusDetailView: View { .navigationBarTitleDisplayMode(.inline) } - private func makeStatusesListView(statuses: [Status], date _: Date) -> some View { + private func makeStatusesListView(statuses: [Status]) -> some View { ForEach(statuses) { status in - var isReplyToPrevious: Bool = false - if let index = statuses.firstIndex(where: { $0.id == status.id }), - index > 0, - statuses[index - 1].id == status.inReplyToId - { - if index == 1, statuses.count > 2 { - let nextStatus = statuses[2] - isReplyToPrevious = nextStatus.inReplyToId == status.id - } else if statuses.count == 2 { - isReplyToPrevious = false - } else { - isReplyToPrevious = true - } - } + let isReplyToPrevious = viewModel.isReplyToPreviousCache[status.id] ?? false let viewModel: StatusRowViewModel = .init(status: status, client: client, routerPath: routerPath) - return HStack(spacing: 0) { - if isReplyToPrevious { - Rectangle() - .fill(theme.tintColor) - .frame(width: 2) - .accessibilityHidden(true) - Spacer(minLength: 8) - } - if self.viewModel.statusId == status.id { - makeCurrentStatusView(status: status) - .environment(\.extraLeadingInset, isReplyToPrevious ? 10 : 0) - } else { - StatusRowView(viewModel: { viewModel }) - .environment(\.extraLeadingInset, isReplyToPrevious ? 10 : 0) - } - } - .listRowBackground(viewModel.highlightRowColor) - .listRowInsets(.init(top: 12, - leading: .layoutPadding, - bottom: 12, - trailing: .layoutPadding)) - } - } + let isFocused = self.viewModel.statusId == status.id - private func makeCurrentStatusView(status: Status) -> some View { - StatusRowView(viewModel: { .init(status: status, - client: client, - routerPath: routerPath) }) - .environment(\.isStatusFocused, true) - .environment(\.isStatusDetailLoaded, !viewModel.isLoadingContext) - .accessibilityFocused($initialFocusBugWorkaround, equals: true) - .overlay { - GeometryReader { reader in - VStack {} - .onAppear { - statusHeight = reader.size.height + StatusRowView(viewModel: viewModel) + .environment(\.extraLeadingInset, isReplyToPrevious ? 10 : 0) + .environment(\.isStatusReplyToPrevious, isReplyToPrevious) + .environment(\.isStatusFocused, isFocused) + .overlay { + if isFocused { + GeometryReader { reader in + VStack {} + .onAppear { + statusHeight = reader.size.height + } } + } } - } - .id(status.id) - // VoiceOver / Switch Control focus workaround - .onAppear { - initialFocusBugWorkaround = true - } + .id(status.id) + .listRowBackground(viewModel.highlightRowColor) + .listRowInsets(.init(top: 12, + leading: .layoutPadding, + bottom: 12, + trailing: .layoutPadding)) + } } private var errorView: some View { @@ -181,7 +146,7 @@ public struct StatusDetailView: View { private var loadingDetailView: some View { ForEach(Status.placeholders()) { status in - StatusRowView(viewModel: { .init(status: status, client: client, routerPath: routerPath) }) + StatusRowView(viewModel: .init(status: status, client: client, routerPath: routerPath)) .redacted(reason: .placeholder) } } diff --git a/Packages/Status/Sources/Status/Detail/StatusDetailViewModel.swift b/Packages/Status/Sources/Status/Detail/StatusDetailViewModel.swift index 627c9fd0..d7e9c0e2 100644 --- a/Packages/Status/Sources/Status/Detail/StatusDetailViewModel.swift +++ b/Packages/Status/Sources/Status/Detail/StatusDetailViewModel.swift @@ -5,7 +5,7 @@ import Network import SwiftUI @MainActor -class StatusDetailViewModel: ObservableObject { +@Observable class StatusDetailViewModel { public var statusId: String? public var remoteStatusURL: URL? @@ -13,13 +13,15 @@ class StatusDetailViewModel: ObservableObject { var routerPath: RouterPath? enum State { - case loading, display(statuses: [Status], date: Date), error(error: Error) + case loading, display(statuses: [Status]), error(error: Error) } - @Published var state: State = .loading - @Published var isLoadingContext = true - @Published var title: LocalizedStringKey = "" - @Published var scrollToId: String? + var state: State = .loading + var title: LocalizedStringKey = "" + var scrollToId: String? + + @ObservationIgnored + var isReplyToPreviousCache: [String: Bool] = [:] init(statusId: String) { state = .loading @@ -28,7 +30,7 @@ class StatusDetailViewModel: ObservableObject { } init(status: Status) { - state = .display(statuses: [status], date: Date()) + state = .display(statuses: [status]) title = "status.post-from-\(status.account.displayNameWithoutEmojis)" statusId = status.id remoteStatusURL = nil @@ -74,23 +76,20 @@ class StatusDetailViewModel: ObservableObject { private func fetchStatusDetail(animate: Bool) async { guard let client, let statusId else { return } do { - isLoadingContext = true let data = try await fetchContextData(client: client, statusId: statusId) title = "status.post-from-\(data.status.account.displayNameWithoutEmojis)" var statuses = data.context.ancestors statuses.append(data.status) statuses.append(contentsOf: data.context.descendants) - + cacheReplyTopPrevious(statuses: statuses) StatusDataControllerProvider.shared.updateDataControllers(for: statuses, client: client) if animate { withAnimation { - isLoadingContext = false - state = .display(statuses: statuses, date: Date()) + state = .display(statuses: statuses) } } else { - isLoadingContext = false - state = .display(statuses: statuses, date: Date()) + state = .display(statuses: statuses) scrollToId = statusId } } catch { @@ -108,6 +107,27 @@ class StatusDetailViewModel: ObservableObject { return try await .init(status: status, context: context) } + private func cacheReplyTopPrevious(statuses: [Status]) { + isReplyToPreviousCache = [:] + for status in statuses { + var isReplyToPrevious: Bool = false + if let index = statuses.firstIndex(where: { $0.id == status.id }), + index > 0, + statuses[index - 1].id == status.inReplyToId + { + if index == 1, statuses.count > 2 { + let nextStatus = statuses[2] + isReplyToPrevious = nextStatus.inReplyToId == status.id + } else if statuses.count == 2 { + isReplyToPrevious = false + } else { + isReplyToPrevious = true + } + } + isReplyToPreviousCache[status.id] = isReplyToPrevious + } + } + func handleEvent(event: any StreamEvent, currentAccount: Account?) { Task { if let event = event as? StreamEventUpdate, diff --git a/Packages/Status/Sources/Status/Editor/Components/StatusEditorAccessoryView.swift b/Packages/Status/Sources/Status/Editor/Components/StatusEditorAccessoryView.swift index b7e97738..9c66f514 100644 --- a/Packages/Status/Sources/Status/Editor/Components/StatusEditorAccessoryView.swift +++ b/Packages/Status/Sources/Status/Editor/Components/StatusEditorAccessoryView.swift @@ -8,11 +8,11 @@ import SwiftUI struct StatusEditorAccessoryView: View { @EnvironmentObject private var preferences: UserPreferences @EnvironmentObject private var theme: Theme - @EnvironmentObject private var currentInstance: CurrentInstance + @Environment(CurrentInstance.self) private var currentInstance @Environment(\.colorScheme) private var colorScheme @FocusState.Binding var isSpoilerTextFocused: Bool - @ObservedObject var viewModel: StatusEditorViewModel + var viewModel: StatusEditorViewModel @State private var isDraftsSheetDisplayed: Bool = false @State private var isLanguageSheetDisplayed: Bool = false @@ -24,6 +24,7 @@ struct StatusEditorAccessoryView: View { @State private var isCameraPickerPresented: Bool = false var body: some View { + @Bindable var viewModel = viewModel VStack(spacing: 0) { Divider() HStack { diff --git a/Packages/Status/Sources/Status/Editor/Components/StatusEditorAutoCompleteView.swift b/Packages/Status/Sources/Status/Editor/Components/StatusEditorAutoCompleteView.swift index d6546bf8..6db70d9f 100644 --- a/Packages/Status/Sources/Status/Editor/Components/StatusEditorAutoCompleteView.swift +++ b/Packages/Status/Sources/Status/Editor/Components/StatusEditorAutoCompleteView.swift @@ -5,7 +5,7 @@ import SwiftUI struct StatusEditorAutoCompleteView: View { @EnvironmentObject private var theme: Theme - @ObservedObject var viewModel: StatusEditorViewModel + var viewModel: StatusEditorViewModel var body: some View { if !viewModel.mentionsSuggestions.isEmpty || !viewModel.tagsSuggestions.isEmpty { diff --git a/Packages/Status/Sources/Status/Editor/Components/StatusEditorMediaEditView.swift b/Packages/Status/Sources/Status/Editor/Components/StatusEditorMediaEditView.swift index 5e626f13..7da7cce6 100644 --- a/Packages/Status/Sources/Status/Editor/Components/StatusEditorMediaEditView.swift +++ b/Packages/Status/Sources/Status/Editor/Components/StatusEditorMediaEditView.swift @@ -7,8 +7,8 @@ import SwiftUI struct StatusEditorMediaEditView: View { @Environment(\.dismiss) private var dismiss @EnvironmentObject private var theme: Theme - @EnvironmentObject private var currentInstance: CurrentInstance - @ObservedObject var viewModel: StatusEditorViewModel + @Environment(CurrentInstance.self) private var currentInstance + var viewModel: StatusEditorViewModel let container: StatusEditorMediaContainer @State private var imageDescription: String = "" diff --git a/Packages/Status/Sources/Status/Editor/Components/StatusEditorMediaView.swift b/Packages/Status/Sources/Status/Editor/Components/StatusEditorMediaView.swift index 89315f40..de37cc92 100644 --- a/Packages/Status/Sources/Status/Editor/Components/StatusEditorMediaView.swift +++ b/Packages/Status/Sources/Status/Editor/Components/StatusEditorMediaView.swift @@ -7,8 +7,8 @@ import SwiftUI struct StatusEditorMediaView: View { @EnvironmentObject private var theme: Theme - @EnvironmentObject private var currentInstance: CurrentInstance - @ObservedObject var viewModel: StatusEditorViewModel + @Environment(CurrentInstance.self) private var currentInstance + var viewModel: StatusEditorViewModel @State private var editingContainer: StatusEditorMediaContainer? @State private var isErrorDisplayed: Bool = false diff --git a/Packages/Status/Sources/Status/Editor/Components/StatusEditorPollView.swift b/Packages/Status/Sources/Status/Editor/Components/StatusEditorPollView.swift index 224f1199..c3dbd108 100644 --- a/Packages/Status/Sources/Status/Editor/Components/StatusEditorPollView.swift +++ b/Packages/Status/Sources/Status/Editor/Components/StatusEditorPollView.swift @@ -12,15 +12,15 @@ struct StatusEditorPollView: View { @State private var currentFocusIndex: Int = 0 @EnvironmentObject private var theme: Theme - @EnvironmentObject private var currentInstance: CurrentInstance + @Environment(CurrentInstance.self) private var currentInstance - @ObservedObject var viewModel: StatusEditorViewModel + var viewModel: StatusEditorViewModel @Binding var showPoll: Bool var body: some View { + @Bindable var viewModel = viewModel let count = viewModel.pollOptions.count - VStack { ForEach(0 ..< count, id: \.self) { index in VStack { @@ -38,10 +38,6 @@ struct StatusEditorPollView: View { addChoice(at: index) } } - .onChange(of: viewModel.pollOptions[index]) { - let maxCharacters: Int = currentInstance.instance?.configuration?.polls.maxCharactersPerOption ?? 50 - viewModel.pollOptions[index] = String($0.prefix(maxCharacters)) - } if canAddMoreAt(index) { Button { diff --git a/Packages/Status/Sources/Status/Editor/StatusEditorView.swift b/Packages/Status/Sources/Status/Editor/StatusEditorView.swift index bfdfe0c3..b3d176b5 100644 --- a/Packages/Status/Sources/Status/Editor/StatusEditorView.swift +++ b/Packages/Status/Sources/Status/Editor/StatusEditorView.swift @@ -12,22 +12,22 @@ import SwiftUI import UIKit public struct StatusEditorView: View { - @EnvironmentObject private var appAccounts: AppAccountsManager + @Environment(AppAccountsManager.self) private var appAccounts @EnvironmentObject private var preferences: UserPreferences @EnvironmentObject private var theme: Theme - @EnvironmentObject private var client: Client - @EnvironmentObject private var currentAccount: CurrentAccount - @EnvironmentObject private var routerPath: RouterPath + @Environment(Client.self) private var client + @Environment(CurrentAccount.self) private var currentAccount + @Environment(RouterPath.self) private var routerPath @Environment(\.dismiss) private var dismiss - @StateObject private var viewModel: StatusEditorViewModel + @State private var viewModel: StatusEditorViewModel @FocusState private var isSpoilerTextFocused: Bool @State private var isDismissAlertPresented: Bool = false @State private var isLanguageConfirmPresented = false public init(mode: StatusEditorViewModel.Mode) { - _viewModel = StateObject(wrappedValue: .init(mode: mode)) + _viewModel = .init(initialValue: .init(mode: mode)) } public var body: some View { @@ -93,9 +93,9 @@ public struct StatusEditorView: View { await viewModel.fetchCustomEmojis() } } - .onChange(of: currentAccount.account?.id, perform: { _ in + .onChange(of: currentAccount.account?.id) { viewModel.currentAccount = currentAccount.account - }) + } .background(theme.primaryBackgroundColor) .navigationTitle(viewModel.mode.title) .navigationBarTitleDisplayMode(.inline) @@ -164,10 +164,10 @@ public struct StatusEditorView: View { } } .interactiveDismissDisabled(viewModel.shouldDisplayDismissWarning) - .onChange(of: appAccounts.currentClient) { newClient in + .onChange(of: appAccounts.currentClient) { _, newValue in if viewModel.mode.isInShareExtension { - currentAccount.setClient(client: newClient) - viewModel.client = newClient + currentAccount.setClient(client: newValue) + viewModel.client = newValue } } } diff --git a/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift b/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift index de87f289..6f36c45f 100644 --- a/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift +++ b/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift @@ -8,7 +8,7 @@ import PhotosUI import SwiftUI @MainActor -public class StatusEditorViewModel: NSObject, ObservableObject { +@Observable public class StatusEditorViewModel: NSObject { var mode: Mode var client: Client? @@ -50,7 +50,7 @@ public class StatusEditorViewModel: NSObject, ObservableObject { return textView.markedTextRange } - @Published var statusText = NSMutableAttributedString(string: "") { + var statusText = NSMutableAttributedString(string: "") { didSet { let range = selectedRange processText() @@ -73,18 +73,18 @@ public class StatusEditorViewModel: NSObject, ObservableObject { private var itemsProvider: [NSItemProvider]? - @Published var backupStatusText: NSAttributedString? + var backupStatusText: NSAttributedString? - @Published var showPoll: Bool = false - @Published var pollVotingFrequency = PollVotingFrequency.oneVote - @Published var pollDuration = Duration.oneDay - @Published var pollOptions: [String] = ["", ""] + var showPoll: Bool = false + var pollVotingFrequency = PollVotingFrequency.oneVote + var pollDuration = Duration.oneDay + var pollOptions: [String] = ["", ""] - @Published var spoilerOn: Bool = false - @Published var spoilerText: String = "" + var spoilerOn: Bool = false + var spoilerText: String = "" - @Published var isPosting: Bool = false - @Published var selectedMedias: [PhotosPickerItem] = [] { + var isPosting: Bool = false + var selectedMedias: [PhotosPickerItem] = [] { didSet { if selectedMedias.count > 4 { selectedMedias = selectedMedias.prefix(4).map { $0 } @@ -94,16 +94,16 @@ public class StatusEditorViewModel: NSObject, ObservableObject { } } - @Published var isMediasLoading: Bool = false + var isMediasLoading: Bool = false - @Published var mediasImages: [StatusEditorMediaContainer] = [] - @Published var replyToStatus: Status? - @Published var embeddedStatus: Status? + var mediasImages: [StatusEditorMediaContainer] = [] + var replyToStatus: Status? + var embeddedStatus: Status? - @Published var customEmojis: [Emoji] = [] + var customEmojis: [Emoji] = [] - @Published var postingError: String? - @Published var showPostingErrorAlert: Bool = false + var postingError: String? + var showPostingErrorAlert: Bool = false var canPost: Bool { statusText.length > 0 || !mediasImages.isEmpty @@ -123,11 +123,11 @@ public class StatusEditorViewModel: NSObject, ObservableObject { return !modifiedStatusText.isEmpty && !mode.isInShareExtension } - @Published var visibility: Models.Visibility = .pub + var visibility: Models.Visibility = .pub - @Published var mentionsSuggestions: [Account] = [] - @Published var tagsSuggestions: [Tag] = [] - @Published var selectedLanguage: String? + var mentionsSuggestions: [Account] = [] + var tagsSuggestions: [Tag] = [] + var selectedLanguage: String? var hasExplicitlySelectedLanguage: Bool = false private var currentSuggestionRange: NSRange? diff --git a/Packages/Status/Sources/Status/Embed/StatusEmbeddedView.swift b/Packages/Status/Sources/Status/Embed/StatusEmbeddedView.swift index ba1c8a58..f35507d5 100644 --- a/Packages/Status/Sources/Status/Embed/StatusEmbeddedView.swift +++ b/Packages/Status/Sources/Status/Embed/StatusEmbeddedView.swift @@ -23,10 +23,10 @@ public struct StatusEmbeddedView: View { HStack { VStack(alignment: .leading) { makeAccountView(account: status.reblog?.account ?? status.account) - StatusRowView(viewModel: { .init(status: status, - client: client, - routerPath: routerPath, - showActions: false) }) + StatusRowView(viewModel: .init(status: status, + client: client, + routerPath: routerPath, + showActions: false)) .accessibilityLabel(status.content.asRawText) .environment(\.isCompact, true) } diff --git a/Packages/Status/Sources/Status/History/StatusEditHistoryView.swift b/Packages/Status/Sources/Status/History/StatusEditHistoryView.swift index fa7eedfe..40777ccd 100644 --- a/Packages/Status/Sources/Status/History/StatusEditHistoryView.swift +++ b/Packages/Status/Sources/Status/History/StatusEditHistoryView.swift @@ -6,7 +6,7 @@ import SwiftUI public struct StatusEditHistoryView: View { @Environment(\.dismiss) private var dismiss - @EnvironmentObject private var client: Client + @Environment(Client.self) private var client @EnvironmentObject private var theme: Theme private let statusId: String diff --git a/Packages/Status/Sources/Status/List/StatusesFetcher.swift b/Packages/Status/Sources/Status/List/StatusesFetcher.swift index fdf4ad4e..ed8d0d6b 100644 --- a/Packages/Status/Sources/Status/List/StatusesFetcher.swift +++ b/Packages/Status/Sources/Status/List/StatusesFetcher.swift @@ -1,5 +1,6 @@ import Combine import Models +import Observation import SwiftUI public enum StatusesState { @@ -13,7 +14,7 @@ public enum StatusesState { } @MainActor -public protocol StatusesFetcher: ObservableObject { +public protocol StatusesFetcher { var statusesState: StatusesState { get } func fetchNewestStatuses() async func fetchNextPage() async diff --git a/Packages/Status/Sources/Status/List/StatusesListView.swift b/Packages/Status/Sources/Status/List/StatusesListView.swift index 9eebdb4a..b924475a 100644 --- a/Packages/Status/Sources/Status/List/StatusesListView.swift +++ b/Packages/Status/Sources/Status/List/StatusesListView.swift @@ -8,7 +8,7 @@ import SwiftUI public struct StatusesListView: View where Fetcher: StatusesFetcher { @EnvironmentObject private var theme: Theme - @ObservedObject private var fetcher: Fetcher + @State private var fetcher: Fetcher // Whether this status is on a remote local timeline (many actions are unavailable if so) private let isRemote: Bool private let routerPath: RouterPath @@ -19,7 +19,7 @@ public struct StatusesListView: View where Fetcher: StatusesFetcher { routerPath: RouterPath, isRemote: Bool = false) { - self.fetcher = fetcher + _fetcher = .init(initialValue: fetcher) self.isRemote = isRemote self.client = client self.routerPath = routerPath @@ -29,7 +29,7 @@ public struct StatusesListView: View where Fetcher: StatusesFetcher { switch fetcher.statusesState { case .loading: ForEach(Status.placeholders()) { status in - StatusRowView(viewModel: { .init(status: status, client: client, routerPath: routerPath) }) + StatusRowView(viewModel: .init(status: status, client: client, routerPath: routerPath)) .redacted(reason: .placeholder) } case .error: @@ -46,12 +46,10 @@ public struct StatusesListView: View where Fetcher: StatusesFetcher { case let .display(statuses, nextPageState): ForEach(statuses, id: \.viewId) { status in - StatusRowView(viewModel: { StatusRowViewModel(status: status, - client: client, - routerPath: routerPath, - isRemote: isRemote) - - }) + StatusRowView(viewModel: StatusRowViewModel(status: status, + client: client, + routerPath: routerPath, + isRemote: isRemote)) .id(status.id) .onAppear { fetcher.statusDidAppear(status: status) diff --git a/Packages/Status/Sources/Status/Media/VideoPlayerView.swift b/Packages/Status/Sources/Status/Media/VideoPlayerView.swift index e8943440..7e6e86fb 100644 --- a/Packages/Status/Sources/Status/Media/VideoPlayerView.swift +++ b/Packages/Status/Sources/Status/Media/VideoPlayerView.swift @@ -1,11 +1,12 @@ import AVKit import DesignSystem import Env +import Observation import SwiftUI @MainActor -class VideoPlayerViewModel: ObservableObject { - @Published var player: AVPlayer? +@Observable class VideoPlayerViewModel { + var player: AVPlayer? private let url: URL init(url: URL) { @@ -53,7 +54,7 @@ struct VideoPlayerView: View { @EnvironmentObject private var preferences: UserPreferences @EnvironmentObject private var theme: Theme - @StateObject var viewModel: VideoPlayerViewModel + @State var viewModel: VideoPlayerViewModel var body: some View { ZStack { @@ -75,8 +76,8 @@ struct VideoPlayerView: View { viewModel.pause() } .cornerRadius(4) - .onChange(of: scenePhase, perform: { scenePhase in - switch scenePhase { + .onChange(of: scenePhase) { _, newValue in + switch newValue { case .background, .inactive: viewModel.pause() case .active: @@ -86,6 +87,6 @@ struct VideoPlayerView: View { default: break } - }) + } } } diff --git a/Packages/Status/Sources/Status/Poll/StatusPollView.swift b/Packages/Status/Sources/Status/Poll/StatusPollView.swift index 3c85f492..c43ce1bf 100644 --- a/Packages/Status/Sources/Status/Poll/StatusPollView.swift +++ b/Packages/Status/Sources/Status/Poll/StatusPollView.swift @@ -6,15 +6,16 @@ import SwiftUI public struct StatusPollView: View { @EnvironmentObject private var theme: Theme - @EnvironmentObject private var client: Client - @EnvironmentObject private var currentInstance: CurrentInstance - @EnvironmentObject private var currentAccount: CurrentAccount - @StateObject private var viewModel: StatusPollViewModel + @Environment(Client.self) private var client + @Environment(CurrentInstance.self) private var currentInstance + @Environment(CurrentAccount.self) private var currentAccount + + @State private var viewModel: StatusPollViewModel private var status: AnyStatus public init(poll: Poll, status: AnyStatus) { - _viewModel = StateObject(wrappedValue: .init(poll: poll)) + _viewModel = .init(initialValue: .init(poll: poll)) self.status = status } diff --git a/Packages/Status/Sources/Status/Poll/StatusPollViewModel.swift b/Packages/Status/Sources/Status/Poll/StatusPollViewModel.swift index ff22d07b..4a8bfe8f 100644 --- a/Packages/Status/Sources/Status/Poll/StatusPollViewModel.swift +++ b/Packages/Status/Sources/Status/Poll/StatusPollViewModel.swift @@ -1,15 +1,16 @@ import Combine import Models import Network +import Observation import SwiftUI @MainActor -public class StatusPollViewModel: ObservableObject { +@Observable public class StatusPollViewModel { public var client: Client? public var instance: Instance? - @Published var poll: Poll - @Published var votes: [Int] = [] + var poll: Poll + var votes: [Int] = [] var showResults: Bool { poll.ownVotes?.isEmpty == false || poll.expired diff --git a/Packages/Status/Sources/Status/Row/StatusActionButtonStyle.swift b/Packages/Status/Sources/Status/Row/StatusActionButtonStyle.swift index 003c0b18..7cc4d2f8 100644 --- a/Packages/Status/Sources/Status/Row/StatusActionButtonStyle.swift +++ b/Packages/Status/Sources/Status/Row/StatusActionButtonStyle.swift @@ -28,8 +28,8 @@ struct StatusActionButtonStyle: ButtonStyle { SparklesView(counter: sparklesCounter, tint: tint, size: 5, velocity: 30) } } - .onChange(of: configuration.isPressed) { isPressed in - guard tintColor != nil, !isPressed, !isOn else { return } + .onChange(of: configuration.isPressed) { _, newValue in + guard tintColor != nil, !newValue, !isOn else { return } withAnimation(.spring(response: 1, dampingFraction: 1)) { sparklesCounter += 1 @@ -88,8 +88,8 @@ struct StatusActionButtonStyle: ButtonStyle { .onAppear { cells = Self.generateCells() } - .onChange(of: counter) { [counter] newCounter in - if floor(counter) != floor(newCounter) { + .onChange(of: counter) { oldValue, newValue in + if floor(oldValue) != floor(newValue) { cells = Self.generateCells() } } diff --git a/Packages/Status/Sources/Status/Row/StatusRowView.swift b/Packages/Status/Sources/Status/Row/StatusRowView.swift index d6427e3b..11510830 100644 --- a/Packages/Status/Sources/Status/Row/StatusRowView.swift +++ b/Packages/Status/Sources/Status/Row/StatusRowView.swift @@ -13,15 +13,15 @@ public struct StatusRowView: View { @Environment(\.isCompact) private var isCompact: Bool @Environment(\.accessibilityVoiceOverEnabled) private var accessibilityVoiceOverEnabled @Environment(\.isStatusFocused) private var isFocused + @Environment(\.isStatusReplyToPrevious) private var isStatusReplyToPrevious - @EnvironmentObject private var quickLook: QuickLook + @Environment(QuickLook.self) private var quickLook @EnvironmentObject private var theme: Theme - @StateObject var viewModel: StatusRowViewModel + @State private var viewModel: StatusRowViewModel - // StateObject accepts an @autoclosure which only allocates the view model once when the view gets on screen. - public init(viewModel: @escaping () -> StatusRowViewModel) { - _viewModel = StateObject(wrappedValue: viewModel()) + public init(viewModel: StatusRowViewModel) { + _viewModel = .init(initialValue: viewModel) } var contextMenu: some View { @@ -29,62 +29,77 @@ public struct StatusRowView: View { } public var body: some View { - VStack(alignment: .leading) { - if viewModel.isFiltered, let filter = viewModel.filter { - switch filter.filter.filterAction { - case .warn: - makeFilterView(filter: filter.filter) - case .hide: - EmptyView() - } - } else { - if !isCompact, theme.avatarPosition == .leading { - Group { - StatusRowReblogView(viewModel: viewModel) - StatusRowReplyView(viewModel: viewModel) + HStack(spacing: 0) { + if isStatusReplyToPrevious { + Rectangle() + .fill(theme.tintColor) + .frame(width: 2) + .accessibilityHidden(true) + Spacer(minLength: 8) + } + VStack(alignment: .leading) { + if viewModel.isFiltered, let filter = viewModel.filter { + switch filter.filter.filterAction { + case .warn: + makeFilterView(filter: filter.filter) + case .hide: + EmptyView() } - .padding(.leading, AvatarView.Size.status.size.width + .statusColumnsSpacing) - } - HStack(alignment: .top, spacing: .statusColumnsSpacing) { - if !isCompact, - theme.avatarPosition == .leading - { - Button { - viewModel.navigateToAccountDetail(account: viewModel.finalStatus.account) - } label: { - AvatarView(url: viewModel.finalStatus.account.avatar, size: .status) - } - } - VStack(alignment: .leading) { - if !isCompact, theme.avatarPosition == .top { + } else { + if !isCompact, theme.avatarPosition == .leading { + Group { StatusRowReblogView(viewModel: viewModel) StatusRowReplyView(viewModel: viewModel) } - VStack(alignment: .leading, spacing: 8) { - if !isCompact { - StatusRowHeaderView(viewModel: viewModel) + .padding(.leading, AvatarView.Size.status.size.width + .statusColumnsSpacing) + } + HStack(alignment: .top, spacing: .statusColumnsSpacing) { + if !isCompact, + theme.avatarPosition == .leading + { + Button { + viewModel.navigateToAccountDetail(account: viewModel.finalStatus.account) + } label: { + AvatarView(url: viewModel.finalStatus.account.avatar, size: .status) } - StatusRowContentView(viewModel: viewModel) - .contentShape(Rectangle()) - .onTapGesture { - guard !isFocused else { return } - viewModel.navigateToDetail() - } - .accessibilityActions { - if isFocused, viewModel.showActions { - accessibilityActions - } - } } - if viewModel.showActions, isFocused || theme.statusActionsDisplay != .none, !isInCaptureMode { - StatusRowActionsView(viewModel: viewModel) - .padding(.top, 8) - .tint(isFocused ? theme.tintColor : .gray) - .contentShape(Rectangle()) - .onTapGesture { - guard !isFocused else { return } - viewModel.navigateToDetail() + VStack(alignment: .leading) { + if !isCompact, theme.avatarPosition == .top { + StatusRowReblogView(viewModel: viewModel) + StatusRowReplyView(viewModel: viewModel) + } + VStack(alignment: .leading, spacing: 8) { + if !isCompact { + StatusRowHeaderView(viewModel: viewModel) } + StatusRowContentView(viewModel: viewModel) + .contentShape(Rectangle()) + .onTapGesture { + guard !isFocused else { return } + viewModel.navigateToDetail() + } + .accessibilityActions { + if isFocused, viewModel.showActions { + accessibilityActions + } + } + } + VStack(alignment: .leading, spacing: 12) { + if viewModel.showActions, isFocused || theme.statusActionsDisplay != .none, !isInCaptureMode { + StatusRowActionsView(viewModel: viewModel) + .padding(.top, 8) + .tint(isFocused ? theme.tintColor : .gray) + .contentShape(Rectangle()) + .onTapGesture { + guard !isFocused else { return } + viewModel.navigateToDetail() + } + } + + if isFocused { + StatusRowDetailView(viewModel: viewModel) + } + } } } } @@ -168,7 +183,7 @@ public struct StatusRowView: View { .alignmentGuide(.listRowSeparatorLeading) { _ in -100 } - .environmentObject( + .environment( StatusDataControllerProvider.shared.dataController(for: viewModel.finalStatus, client: viewModel.client) ) diff --git a/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift b/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift index f22a60fb..dc940540 100644 --- a/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift +++ b/Packages/Status/Sources/Status/Row/StatusRowViewModel.swift @@ -4,37 +4,44 @@ import Env import Models import NaturalLanguage import Network +import Observation import SwiftUI @MainActor -public class StatusRowViewModel: ObservableObject { +@Observable public class StatusRowViewModel { let status: Status // Whether this status is on a remote local timeline (many actions are unavailable if so) let isRemote: Bool let showActions: Bool let textDisabled: Bool let finalStatus: AnyStatus + + let client: Client + let routerPath: RouterPath + + private let theme = Theme.shared + private let userMentionned: Bool - @Published var isPinned: Bool - @Published var embeddedStatus: Status? - @Published var displaySpoiler: Bool = false - @Published var isEmbedLoading: Bool = false - @Published var isFiltered: Bool = false + var isPinned: Bool + var embeddedStatus: Status? + var displaySpoiler: Bool = false + var isEmbedLoading: Bool = false + var isFiltered: Bool = false - @Published var translation: Translation? - @Published var isLoadingTranslation: Bool = false - @Published var showDeleteAlert: Bool = false + var translation: Translation? + var isLoadingTranslation: Bool = false + var showDeleteAlert: Bool = false - private var actionsAccountsFetched: Bool = false - @Published var favoriters: [Account] = [] - @Published var rebloggers: [Account] = [] + private(set) var actionsAccountsFetched: Bool = false + var favoriters: [Account] = [] + var rebloggers: [Account] = [] - @Published var isLoadingRemoteContent: Bool = false - @Published var localStatusId: String? - @Published var localStatus: Status? + var isLoadingRemoteContent: Bool = false + var localStatusId: String? + var localStatus: Status? // The relationship our user has to the author of this post, if available - @Published var authorRelationship: Relationship? { + var authorRelationship: Relationship? { didSet { // if we are newly blocking or muting the author, force collapse post so it goes away if let relationship = authorRelationship, @@ -46,19 +53,22 @@ public class StatusRowViewModel: ObservableObject { } // used by the button to expand a collapsed post - @Published var isCollapsed: Bool = true { + var isCollapsed: Bool = true { didSet { recalcCollapse() } } // number of lines to show, nil means show the whole post - @Published var lineLimit: Int? = nil + var lineLimit: Int? // post length determining if the post should be collapsed + @ObservationIgnored let collapseThresholdLength: Int = 750 // number of text lines to show on a collpased post + @ObservationIgnored let collapsedLines: Int = 8 // user preference, set in init + @ObservationIgnored var collapseLongPosts: Bool = false private func recalcCollapse() { @@ -71,9 +81,7 @@ public class StatusRowViewModel: ObservableObject { } } - private let theme = Theme.shared - private let userMentionned: Bool - + @ObservationIgnored private var seen = false var filter: Filtered? { @@ -95,9 +103,6 @@ public class StatusRowViewModel: ObservableObject { } } - let client: Client - let routerPath: RouterPath - public init(status: Status, client: Client, routerPath: RouterPath, @@ -283,9 +288,15 @@ public class StatusRowViewModel: ObservableObject { func fetchActionsAccounts() async { guard !actionsAccountsFetched else { return } do { - favoriters = try await client.get(endpoint: Statuses.favoritedBy(id: status.id, maxId: nil)) - rebloggers = try await client.get(endpoint: Statuses.rebloggedBy(id: status.id, maxId: nil)) - actionsAccountsFetched = true + withAnimation(.smooth) { + actionsAccountsFetched = true + } + let favoriters: [Account] = try await client.get(endpoint: Statuses.favoritedBy(id: status.id, maxId: nil)) + let rebloggers: [Account] = try await client.get(endpoint: Statuses.rebloggedBy(id: status.id, maxId: nil)) + withAnimation(.smooth) { + self.favoriters = favoriters + self.rebloggers = rebloggers + } } catch {} } diff --git a/Packages/Status/Sources/Status/Row/Subviews/StatusRowActionsView.swift b/Packages/Status/Sources/Status/Row/Subviews/StatusRowActionsView.swift index bc950edd..948b602a 100644 --- a/Packages/Status/Sources/Status/Row/Subviews/StatusRowActionsView.swift +++ b/Packages/Status/Sources/Status/Row/Subviews/StatusRowActionsView.swift @@ -6,16 +6,14 @@ import SwiftUI struct StatusRowActionsView: View { @EnvironmentObject private var theme: Theme - @EnvironmentObject private var currentAccount: CurrentAccount - @EnvironmentObject private var statusDataController: StatusDataController + @Environment(CurrentAccount.self) private var currentAccount + @Environment(StatusDataController.self) private var statusDataController @EnvironmentObject private var userPreferences: UserPreferences - - @Environment(\.isStatusFocused) private var isFocused - @Environment(\.isStatusDetailLoaded) private var isStatusDetailLoaded - - @ObservedObject var viewModel: StatusRowViewModel - + @Environment(\.isStatusFocused) private var isFocused + + var viewModel: StatusRowViewModel + func privateBoost() -> Bool { viewModel.status.visibility == .priv && viewModel.status.account.id == currentAccount.account?.id } @@ -153,12 +151,6 @@ struct StatusRowActionsView: View { } } } - - if isStatusDetailLoaded { - StatusRowDetailView(viewModel: viewModel) - .transition(.move(edge: .bottom)) - .animation(.snappy) - } } } diff --git a/Packages/Status/Sources/Status/Row/Subviews/StatusRowContentView.swift b/Packages/Status/Sources/Status/Row/Subviews/StatusRowContentView.swift index 858ba824..f54a3055 100644 --- a/Packages/Status/Sources/Status/Row/Subviews/StatusRowContentView.swift +++ b/Packages/Status/Sources/Status/Row/Subviews/StatusRowContentView.swift @@ -10,10 +10,11 @@ struct StatusRowContentView: View { @EnvironmentObject private var theme: Theme - @ObservedObject var viewModel: StatusRowViewModel + var viewModel: StatusRowViewModel var body: some View { if !viewModel.finalStatus.spoilerText.asRawText.isEmpty { + @Bindable var viewModel = viewModel StatusRowSpoilerView(status: viewModel.finalStatus, displaySpoiler: $viewModel.displaySpoiler) } diff --git a/Packages/Status/Sources/Status/Row/Subviews/StatusRowContextMenu.swift b/Packages/Status/Sources/Status/Row/Subviews/StatusRowContextMenu.swift index c877fdd8..3711eb6d 100644 --- a/Packages/Status/Sources/Status/Row/Subviews/StatusRowContextMenu.swift +++ b/Packages/Status/Sources/Status/Row/Subviews/StatusRowContextMenu.swift @@ -7,14 +7,14 @@ import SwiftUI struct StatusRowContextMenu: View { @Environment(\.displayScale) var displayScale - @EnvironmentObject private var client: Client - @EnvironmentObject private var sceneDelegate: SceneDelegate + @Environment(Client.self) private var client + @Environment(SceneDelegate.self) private var sceneDelegate @EnvironmentObject private var preferences: UserPreferences - @EnvironmentObject private var account: CurrentAccount - @EnvironmentObject private var currentInstance: CurrentInstance - @EnvironmentObject private var statusDataController: StatusDataController + @Environment(CurrentAccount.self) private var account + @Environment(CurrentInstance.self) private var currentInstance + @Environment(StatusDataController.self) private var statusDataController - @ObservedObject var viewModel: StatusRowViewModel + var viewModel: StatusRowViewModel var boostLabel: some View { if viewModel.status.visibility == .priv, viewModel.status.account.id == account.account?.id { @@ -81,17 +81,17 @@ struct StatusRowContextMenu: View { Button { let view = HStack { - StatusRowView(viewModel: { viewModel }) + StatusRowView(viewModel: viewModel) .padding(16) } .environment(\.isInCaptureMode, true) .environmentObject(Theme.shared) .environmentObject(preferences) - .environmentObject(account) - .environmentObject(currentInstance) - .environmentObject(SceneDelegate()) - .environmentObject(QuickLook()) - .environmentObject(viewModel.client) + .environment(account) + .environment(currentInstance) + .environment(SceneDelegate()) + .environment(QuickLook()) + .environment(viewModel.client) .preferredColorScheme(Theme.shared.selectedScheme == .dark ? .dark : .light) .foregroundColor(Theme.shared.labelColor) .background(Theme.shared.primaryBackgroundColor) diff --git a/Packages/Status/Sources/Status/Row/Subviews/StatusRowDetailView.swift b/Packages/Status/Sources/Status/Row/Subviews/StatusRowDetailView.swift index 8914666c..7e742943 100644 --- a/Packages/Status/Sources/Status/Row/Subviews/StatusRowDetailView.swift +++ b/Packages/Status/Sources/Status/Row/Subviews/StatusRowDetailView.swift @@ -3,12 +3,13 @@ import Env import Models import SwiftUI +@MainActor struct StatusRowDetailView: View { @Environment(\.openURL) private var openURL - @EnvironmentObject private var statusDataController: StatusDataController + @Environment(StatusDataController.self) private var statusDataController - @ObservedObject var viewModel: StatusRowViewModel + var viewModel: StatusRowViewModel var body: some View { Group { @@ -57,7 +58,7 @@ struct StatusRowDetailView: View { .foregroundColor(.gray) } - if statusDataController.favoritesCount > 0 { + if viewModel.actionsAccountsFetched, statusDataController.favoritesCount > 0 { Divider() Button { viewModel.routerPath.navigate(to: .favoritedBy(id: viewModel.status.id)) @@ -72,8 +73,10 @@ struct StatusRowDetailView: View { .frame(height: 20) } .buttonStyle(.borderless) + .transition(.move(edge: .leading)) } - if statusDataController.reblogsCount > 0 { + + if viewModel.actionsAccountsFetched, statusDataController.reblogsCount > 0 { Divider() Button { viewModel.routerPath.navigate(to: .rebloggedBy(id: viewModel.status.id)) @@ -88,6 +91,7 @@ struct StatusRowDetailView: View { .frame(height: 20) } .buttonStyle(.borderless) + .transition(.move(edge: .leading)) } } .task { @@ -102,6 +106,7 @@ struct StatusRowDetailView: View { AvatarView(url: account.avatar, size: .list) .padding(.leading, -4) } + .transition(.opacity) } .padding(.leading, .layoutPadding) } diff --git a/Packages/Status/Sources/Status/Row/Subviews/StatusRowHeaderView.swift b/Packages/Status/Sources/Status/Row/Subviews/StatusRowHeaderView.swift index e0f92b4b..ba41a64e 100644 --- a/Packages/Status/Sources/Status/Row/Subviews/StatusRowHeaderView.swift +++ b/Packages/Status/Sources/Status/Row/Subviews/StatusRowHeaderView.swift @@ -6,7 +6,7 @@ import SwiftUI struct StatusRowHeaderView: View { @Environment(\.isInCaptureMode) private var isInCaptureMode: Bool @Environment(\.isStatusFocused) private var isFocused - + @EnvironmentObject private var theme: Theme let viewModel: StatusRowViewModel diff --git a/Packages/Status/Sources/Status/Row/Subviews/StatusRowMediaPreviewView.swift b/Packages/Status/Sources/Status/Row/Subviews/StatusRowMediaPreviewView.swift index 7b0dacc0..edc20186 100644 --- a/Packages/Status/Sources/Status/Row/Subviews/StatusRowMediaPreviewView.swift +++ b/Packages/Status/Sources/Status/Row/Subviews/StatusRowMediaPreviewView.swift @@ -11,9 +11,9 @@ public struct StatusRowMediaPreviewView: View { @Environment(\.isInCaptureMode) private var isInCaptureMode: Bool @Environment(\.isCompact) private var isCompact: Bool - @EnvironmentObject var sceneDelegate: SceneDelegate + @Environment(SceneDelegate.self) private var sceneDelegate @EnvironmentObject private var preferences: UserPreferences - @EnvironmentObject private var quickLook: QuickLook + @Environment(QuickLook.self) private var quickLook @EnvironmentObject private var theme: Theme public let attachments: [MediaAttachment] diff --git a/Packages/Status/Sources/Status/Row/Subviews/StatusRowSwipeView.swift b/Packages/Status/Sources/Status/Row/Subviews/StatusRowSwipeView.swift index b2185529..dcb38176 100644 --- a/Packages/Status/Sources/Status/Row/Subviews/StatusRowSwipeView.swift +++ b/Packages/Status/Sources/Status/Row/Subviews/StatusRowSwipeView.swift @@ -6,8 +6,8 @@ import SwiftUI struct StatusRowSwipeView: View { @EnvironmentObject private var theme: Theme @EnvironmentObject private var preferences: UserPreferences - @EnvironmentObject private var currentAccount: CurrentAccount - @EnvironmentObject private var statusDataController: StatusDataController + @Environment(CurrentAccount.self) private var currentAccount + @Environment(StatusDataController.self) private var statusDataController enum Mode { case leading, trailing @@ -17,7 +17,7 @@ struct StatusRowSwipeView: View { viewModel.status.visibility == .priv && viewModel.status.account.id == currentAccount.account?.id } - @ObservedObject var viewModel: StatusRowViewModel + var viewModel: StatusRowViewModel let mode: Mode var body: some View { diff --git a/Packages/Status/Sources/Status/Row/Subviews/StatusRowTextView.swift b/Packages/Status/Sources/Status/Row/Subviews/StatusRowTextView.swift index 5bf804a2..06fc4d71 100644 --- a/Packages/Status/Sources/Status/Row/Subviews/StatusRowTextView.swift +++ b/Packages/Status/Sources/Status/Row/Subviews/StatusRowTextView.swift @@ -7,7 +7,7 @@ struct StatusRowTextView: View { @EnvironmentObject private var theme: Theme @Environment(\.isStatusFocused) private var isFocused - @ObservedObject var viewModel: StatusRowViewModel + var viewModel: StatusRowViewModel var body: some View { VStack { diff --git a/Packages/Status/Sources/Status/Row/Subviews/StatusRowTranslateView.swift b/Packages/Status/Sources/Status/Row/Subviews/StatusRowTranslateView.swift index d9ee3a23..b0f90e77 100644 --- a/Packages/Status/Sources/Status/Row/Subviews/StatusRowTranslateView.swift +++ b/Packages/Status/Sources/Status/Row/Subviews/StatusRowTranslateView.swift @@ -9,7 +9,7 @@ struct StatusRowTranslateView: View { @EnvironmentObject private var preferences: UserPreferences - @ObservedObject var viewModel: StatusRowViewModel + var viewModel: StatusRowViewModel private var shouldShowTranslateButton: Bool { let statusLang = viewModel.getStatusLang() diff --git a/Packages/Timeline/Package.swift b/Packages/Timeline/Package.swift index fe24615e..68898599 100644 --- a/Packages/Timeline/Package.swift +++ b/Packages/Timeline/Package.swift @@ -7,7 +7,7 @@ let package = Package( name: "Timeline", defaultLocalization: "en", platforms: [ - .iOS(.v16), + .iOS(.v17), ], products: [ .library( diff --git a/Packages/Timeline/Sources/Timeline/PendingStatusesObserver.swift b/Packages/Timeline/Sources/Timeline/PendingStatusesObserver.swift index 488f2bd8..c358672e 100644 --- a/Packages/Timeline/Sources/Timeline/PendingStatusesObserver.swift +++ b/Packages/Timeline/Sources/Timeline/PendingStatusesObserver.swift @@ -1,11 +1,12 @@ import Env import Foundation import Models +import Observation import SwiftUI @MainActor -class PendingStatusesObserver: ObservableObject { - @Published var pendingStatusesCount: Int = 0 +@Observable class PendingStatusesObserver { + var pendingStatusesCount: Int = 0 var disableUpdate: Bool = false var scrollToIndex: ((Int) -> Void)? @@ -27,7 +28,7 @@ class PendingStatusesObserver: ObservableObject { } struct PendingStatusesObserverView: View { - @ObservedObject var observer: PendingStatusesObserver + @State var observer: PendingStatusesObserver var body: some View { if observer.pendingStatusesCount > 0 { diff --git a/Packages/Timeline/Sources/Timeline/TimelinePrefetcher.swift b/Packages/Timeline/Sources/Timeline/TimelinePrefetcher.swift index 68020c18..af5fa24f 100644 --- a/Packages/Timeline/Sources/Timeline/TimelinePrefetcher.swift +++ b/Packages/Timeline/Sources/Timeline/TimelinePrefetcher.swift @@ -1,9 +1,10 @@ import Models import Nuke +import Observation import SwiftUI import UIKit -final class TimelinePrefetcher: NSObject, ObservableObject, UICollectionViewDataSourcePrefetching { +@Observable final class TimelinePrefetcher: NSObject, UICollectionViewDataSourcePrefetching { private let prefetcher = ImagePrefetcher() weak var viewModel: TimelineViewModel? diff --git a/Packages/Timeline/Sources/Timeline/TimelineView.swift b/Packages/Timeline/Sources/Timeline/TimelineView.swift index 36bfed27..f9838a22 100644 --- a/Packages/Timeline/Sources/Timeline/TimelineView.swift +++ b/Packages/Timeline/Sources/Timeline/TimelineView.swift @@ -14,13 +14,13 @@ public struct TimelineView: View { @Environment(\.scenePhase) private var scenePhase @EnvironmentObject private var theme: Theme - @EnvironmentObject private var account: CurrentAccount - @EnvironmentObject private var watcher: StreamWatcher - @EnvironmentObject private var client: Client - @EnvironmentObject private var routerPath: RouterPath + @Environment(CurrentAccount.self) private var account + @Environment(StreamWatcher.self) private var watcher + @Environment(Client.self) private var client + @Environment(RouterPath.self) private var routerPath - @StateObject private var viewModel = TimelineViewModel() - @StateObject private var prefetcher = TimelinePrefetcher() + @State private var viewModel = TimelineViewModel() + @State private var prefetcher = TimelinePrefetcher() @State private var wasBackgrounded: Bool = false @State private var collectionView: UICollectionView? @@ -58,7 +58,7 @@ public struct TimelineView: View { .listStyle(.plain) .scrollContentBackground(.hidden) .background(theme.primaryBackgroundColor) - .introspect(.list, on: .iOS(.v16, .v17)) { (collectionView: UICollectionView) in + .introspect(.list, on: .iOS(.v17)) { (collectionView: UICollectionView) in DispatchQueue.main.async { self.collectionView = collectionView } @@ -70,24 +70,24 @@ public struct TimelineView: View { PendingStatusesObserverView(observer: viewModel.pendingStatusesObserver) } } - .onChange(of: viewModel.scrollToIndex) { index in + .onChange(of: viewModel.scrollToIndex) { _, newValue in if let collectionView, - let index, + let newValue, let rows = collectionView.dataSource?.collectionView(collectionView, numberOfItemsInSection: 0), - rows > index + rows > newValue { - collectionView.scrollToItem(at: .init(row: index, section: 0), + collectionView.scrollToItem(at: .init(row: newValue, section: 0), at: .top, animated: viewModel.scrollToIndexAnimated) viewModel.scrollToIndexAnimated = false viewModel.scrollToIndex = nil } } - .onChange(of: scrollToTopSignal, perform: { _ in + .onChange(of: scrollToTopSignal) { withAnimation { proxy.scrollTo(Constants.scrollToTop, anchor: .top) } - }) + } } .toolbar { ToolbarItem(placement: .principal) { @@ -145,25 +145,25 @@ public struct TimelineView: View { HapticManager.shared.fireHaptic(of: .dataRefresh(intensity: 0.7)) SoundEffectManager.shared.playSound(of: .refresh) } - .onChange(of: watcher.latestEvent?.id) { _ in + .onChange(of: watcher.latestEvent?.id) { if let latestEvent = watcher.latestEvent { viewModel.handleEvent(event: latestEvent, currentAccount: account) } } - .onChange(of: timeline) { newTimeline in - switch newTimeline { + .onChange(of: timeline) { _, newValue in + switch newValue { case let .remoteLocal(server, _): viewModel.client = Client(server: server) default: viewModel.client = client } - viewModel.timeline = newTimeline + viewModel.timeline = newValue } - .onChange(of: viewModel.timeline, perform: { newValue in + .onChange(of: viewModel.timeline) { _, newValue in timeline = newValue - }) - .onChange(of: scenePhase, perform: { scenePhase in - switch scenePhase { + } + .onChange(of: scenePhase) { _, newValue in + switch newValue { case .active: if wasBackgrounded { wasBackgrounded = false @@ -175,7 +175,7 @@ public struct TimelineView: View { default: break } - }) + } } @ViewBuilder diff --git a/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift b/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift index 9092fda9..0dd226c1 100644 --- a/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift +++ b/Packages/Timeline/Sources/Timeline/TimelineViewModel.swift @@ -1,14 +1,15 @@ import Env import Models import Network +import Observation import Status import SwiftUI @MainActor -class TimelineViewModel: ObservableObject { - @Published var scrollToIndex: Int? - @Published var statusesState: StatusesState = .loading - @Published var timeline: TimelineFilter = .federated { +@Observable class TimelineViewModel { + var scrollToIndex: Int? + var statusesState: StatusesState = .loading + var timeline: TimelineFilter = .federated { didSet { timelineTask?.cancel() timelineTask = Task { @@ -39,7 +40,7 @@ class TimelineViewModel: ObservableObject { private var timelineTask: Task? - @Published var tag: Tag? + var tag: Tag? var tagGroup: TagGroup? { if case let .tagGroup(group) = timeline {