Initial iOS 17 + Observable migration

This commit is contained in:
Thomas Ricouard 2023-09-16 18:40:59 +02:00
parent 98035e8530
commit 54839953cd
85 changed files with 334 additions and 324 deletions

View file

@ -975,7 +975,7 @@
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",
@ -1005,7 +1005,7 @@
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",
@ -1036,7 +1036,7 @@
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",
@ -1066,7 +1066,7 @@
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",
@ -1240,7 +1240,7 @@
"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;
@ -1293,7 +1293,7 @@
"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;
@ -1325,7 +1325,7 @@
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",
@ -1356,7 +1356,7 @@
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",

View file

@ -116,9 +116,9 @@ extension View {
.environmentObject(UserPreferences.shared)
.environmentObject(CurrentInstance.shared)
.environmentObject(Theme.shared)
.environmentObject(AppAccountsManager.shared)
.environment(AppAccountsManager.shared)
.environmentObject(PushNotificationsService.shared)
.environmentObject(AppAccountsManager.shared.currentClient)
.environment(AppAccountsManager.shared.currentClient)
}
}

View file

@ -15,7 +15,7 @@ struct IceCubesApp: App {
@Environment(\.scenePhase) private var scenePhase
@StateObject private var appAccountsManager = AppAccountsManager.shared
@State private var appAccountsManager = AppAccountsManager.shared
@StateObject private var currentInstance = CurrentInstance.shared
@StateObject private var currentAccount = CurrentAccount.shared
@StateObject private var userPreferences = UserPreferences.shared
@ -43,8 +43,8 @@ struct IceCubesApp: App {
setupRevenueCat()
refreshPushSubs()
}
.environmentObject(appAccountsManager)
.environmentObject(appAccountsManager.currentClient)
.environment(appAccountsManager)
.environment(appAccountsManager.currentClient)
.environmentObject(quickLook)
.environmentObject(currentAccount)
.environmentObject(currentInstance)
@ -58,17 +58,17 @@ struct IceCubesApp: App {
.edgesIgnoringSafeArea(.bottom)
.background(TransparentBackground())
})
.onChange(of: pushNotificationsService.handledNotification) { notification in
if notification != nil {
.onChange(of: pushNotificationsService.handledNotification) { oldValue, 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) { oldValue, newValue in
handleScenePhase(scenePhase: newValue)
}
.onChange(of: appAccountsManager.currentClient) { newClient in
setNewClientsInEnv(client: newClient)
if newClient.isAuth {
.onChange(of: appAccountsManager.currentClient) { oldValue, newValue in
setNewClientsInEnv(client: newValue)
if newValue.isAuth {
watcher.watch(streams: [.user, .direct])
}
}
@ -143,7 +143,7 @@ struct IceCubesApp: App {
}
}
}
}.onChange(of: $appAccountsManager.currentAccount.id) { _ in
}.onChange(of: $appAccountsManager.currentAccount.id) {
sideBarLoadedTabs.removeAll()
}
}
@ -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()
}

View file

@ -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 = ""

View file

@ -2,6 +2,7 @@ import DesignSystem
import Env
import SafariServices
import SwiftUI
import Observation
extension View {
@MainActor func withSafariRouter() -> some View {
@ -15,7 +16,7 @@ private struct SafariRouter: ViewModifier {
@EnvironmentObject private var preferences: UserPreferences
@EnvironmentObject private var routerPath: 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?

View file

@ -6,7 +6,7 @@ import Models
import SwiftUI
struct SideBarView<Content: View>: View {
@EnvironmentObject private var appAccounts: AppAccountsManager
@Environment(AppAccountsManager.self) private var appAccounts
@EnvironmentObject private var currentAccount: CurrentAccount
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var watcher: StreamWatcher

View file

@ -11,7 +11,7 @@ struct ExploreTab: View {
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var preferences: UserPreferences
@EnvironmentObject private var currentAccount: CurrentAccount
@EnvironmentObject private var client: Client
@Environment(Client.self) private var client
@StateObject private var routerPath = RouterPath()
@Binding var popToRootTab: Tab
@ -36,12 +36,12 @@ struct ExploreTab: View {
}
.withSafariRouter()
.environmentObject(routerPath)
.onChange(of: $popToRootTab.wrappedValue) { popToRootTab in
if popToRootTab == .explore {
.onChange(of: $popToRootTab.wrappedValue) { oldValue, newValue in
if newValue == .explore {
routerPath.path = []
}
}
.onChange(of: client.id) { _ in
.onChange(of: client.id) {
routerPath.path = []
}
.onAppear {

View file

@ -11,9 +11,9 @@ import SwiftUI
struct MessagesTab: View {
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var watcher: StreamWatcher
@EnvironmentObject private var client: Client
@Environment(Client.self) private var client
@EnvironmentObject private var currentAccount: CurrentAccount
@EnvironmentObject private var appAccount: AppAccountsManager
@Environment(AppAccountsManager.self) private var appAccount
@StateObject private var routerPath = RouterPath()
@Binding var popToRootTab: Tab
@ -32,12 +32,12 @@ 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) { oldValue, newValue in
if newValue == .messages {
routerPath.path = []
}
}
.onChange(of: client.id) { _ in
.onChange(of: client.id) {
routerPath.path = []
}
.onAppear {

View file

@ -12,9 +12,9 @@ struct NotificationsTab: View {
@Environment(\.scenePhase) private var scenePhase
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var client: Client
@Environment(Client.self) private var client
@EnvironmentObject private var watcher: StreamWatcher
@EnvironmentObject private var appAccount: AppAccountsManager
@Environment(AppAccountsManager.self) private var appAccount
@EnvironmentObject private var currentAccount: CurrentAccount
@EnvironmentObject private var userPreferences: UserPreferences
@EnvironmentObject private var pushNotificationsService: PushNotificationsService
@ -55,34 +55,34 @@ struct NotificationsTab: View {
}
.withSafariRouter()
.environmentObject(routerPath)
.onChange(of: $popToRootTab.wrappedValue) { popToRootTab in
if popToRootTab == .notifications {
.onChange(of: $popToRootTab.wrappedValue) { oldValue, 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) { oldValue, 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) { oldValue, newValue in
switch newValue {
case .active:
clearNotifications()
default:
break
}
})
.onChange(of: client.id) { _ in
}
.onChange(of: client.id) {
routerPath.path = []
}
}

View file

@ -9,9 +9,9 @@ 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
@Environment(Client.self) private var client
@EnvironmentObject private var currentAccount: CurrentAccount
@StateObject private var routerPath = RouterPath()
@Binding var popToRootTab: Tab
@ -29,12 +29,12 @@ struct ProfileTab: View {
.redacted(reason: .placeholder)
}
}
.onChange(of: $popToRootTab.wrappedValue) { popToRootTab in
if popToRootTab == .profile {
.onChange(of: $popToRootTab.wrappedValue) { oldValue, newValue in
if newValue == .profile {
routerPath.path = []
}
}
.onChange(of: client.id) { _ in
.onChange(of: client.id) {
routerPath.path = []
}
.onAppear {

View file

@ -15,8 +15,8 @@ struct AccountSettingsView: View {
@EnvironmentObject private var currentAccount: CurrentAccount
@EnvironmentObject private var currentInstance: 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

View file

@ -13,7 +13,7 @@ struct AddAccountView: View {
@Environment(\.dismiss) private var dismiss
@Environment(\.scenePhase) private var scenePhase
@EnvironmentObject private var appAccountsManager: AppAccountsManager
@Environment(AppAccountsManager.self) private var appAccountsManager
@EnvironmentObject private var currentAccount: CurrentAccount
@EnvironmentObject private var currentInstance: CurrentInstance
@EnvironmentObject private var pushNotifications: PushNotificationsService
@ -89,7 +89,7 @@ struct AddAccountView: View {
}
isSigninIn = false
}
.onChange(of: instanceName) { newValue in
.onChange(of: instanceName) { oldValue, 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) { oldValue, 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) { oldValue, newValue in
if newValue == nil {
isSigninIn = false
}
})
}
.sheet(item: $oauthURL, content: { url in
SafariView(url: url)
})

View file

@ -44,7 +44,7 @@ struct ContentSettingsView: View {
}
}
.listRowBackground(theme.primaryBackgroundColor)
.onChange(of: userPreferences.useInstanceContentSettings) { newVal in
.onChange(of: userPreferences.useInstanceContentSettings) { oldValue, 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()
}

View file

@ -5,6 +5,7 @@ import Models
import Network
import Status
import SwiftUI
import Observation
class DisplaySettingsLocalValues: ObservableObject {
@Published var tintColor = Theme.shared.tintColor
@ -64,7 +65,7 @@ struct DisplaySettingsView: View {
var body: some View {
ZStack(alignment: .top) {
Form {
StatusRowView(viewModel: { previewStatusViewModel })
StatusRowView(viewModel: previewStatusViewModel)
.allowsHitTesting(false)
.opacity(0)
.hidden()
@ -83,7 +84,7 @@ struct DisplaySettingsView: View {
private var examplePost: some View {
VStack(spacing: 0) {
StatusRowView(viewModel: { previewStatusViewModel })
StatusRowView(viewModel: previewStatusViewModel)
.allowsHitTesting(false)
.padding(.layoutPadding)
.background(theme.primaryBackgroundColor)
@ -111,7 +112,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

View file

@ -9,7 +9,7 @@ import UserNotifications
struct PushNotificationsView: View {
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var appAccountsManager: AppAccountsManager
@Environment(AppAccountsManager.self) private var appAccountsManager
@EnvironmentObject private var pushNotifications: PushNotificationsService
@StateObject public var subscription: PushNotificationSubscriptionSettings

View file

@ -14,9 +14,9 @@ struct SettingsTabs: View {
@EnvironmentObject private var pushNotifications: PushNotificationsService
@EnvironmentObject private var preferences: UserPreferences
@EnvironmentObject private var client: Client
@Environment(Client.self) private var client
@EnvironmentObject private var currentInstance: CurrentInstance
@EnvironmentObject private var appAccountsManager: AppAccountsManager
@Environment(AppAccountsManager.self) private var appAccountsManager
@EnvironmentObject private var theme: Theme
@StateObject private var routerPath = RouterPath()
@ -68,8 +68,8 @@ struct SettingsTabs: View {
}
.withSafariRouter()
.environmentObject(routerPath)
.onChange(of: $popToRootTab.wrappedValue) { popToRootTab in
if popToRootTab == .notifications {
.onChange(of: $popToRootTab.wrappedValue) { oldValue, newValue in
if newValue == .notifications {
routerPath.path = []
}
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -58,7 +58,7 @@ struct AddRemoteTimelineView: View {
Button("action.cancel", action: { dismiss() })
}
}
.onChange(of: instanceName) { newValue in
.onChange(of: instanceName) { oldValue, newValue in
instanceNamePublisher.send(newValue)
}
.onReceive(instanceNamePublisher.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)) { newValue in

View file

@ -93,7 +93,7 @@ struct EditTagGroupView: View {
.onSubmit {
focusedField = Focus.new
}
.onChange(of: sfSymbolName) { _ in
.onChange(of: sfSymbolName) {
popupTagsPresented = true
}

View file

@ -8,11 +8,11 @@ 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
@EnvironmentObject private var preferences: UserPreferences
@EnvironmentObject private var client: Client
@Environment(Client.self) private var client
@StateObject private var routerPath = RouterPath()
@Binding var popToRootTab: Tab
@ -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) { oldValue, newValue in
if newValue == .timeline {
if routerPath.path.isEmpty {
scrollToTopSignal += 1
} else {
@ -81,12 +81,12 @@ 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) { oldValue, newValue in
if newValue == .home || newValue == .federated || newValue == .local {
lastTimelineFilter = newValue
}
}
.withSafariRouter()

View file

@ -28,8 +28,8 @@ class ShareViewController: UIViewController {
if let attachments = item.attachments {
let view = StatusEditorView(mode: .shareExtension(items: attachments))
.environmentObject(UserPreferences.shared)
.environmentObject(appAccountsManager)
.environmentObject(client)
.environment(appAccountsManager)
.environment(client)
.environmentObject(account)
.environmentObject(theme)
.environmentObject(instance)

View file

@ -7,7 +7,7 @@ let package = Package(
name: "Account",
defaultLocalization: "en",
platforms: [
.iOS(.v16),
.iOS(.v17),
],
products: [
.library(

View file

@ -3,12 +3,12 @@ import Network
import SwiftUI
public struct AccountDetailContextMenu: View {
@EnvironmentObject private var client: Client
@Environment(Client.self) private var client
@EnvironmentObject private var routerPath: RouterPath
@EnvironmentObject private var currentInstance: CurrentInstance
@EnvironmentObject private var preferences: UserPreferences
@ObservedObject var viewModel: AccountDetailViewModel
var viewModel: AccountDetailViewModel
public var body: some View {
if let account = viewModel.account {

View file

@ -18,7 +18,7 @@ struct AccountDetailHeaderView: View {
@Environment(\.redactionReasons) private var reasons
@Environment(\.isSupporter) private var isSupporter: Bool
@ObservedObject var viewModel: AccountDetailViewModel
var viewModel: AccountDetailViewModel
let account: Account
let scrollViewProxy: ScrollViewProxy?

View file

@ -16,10 +16,10 @@ public struct AccountDetailView: View {
@EnvironmentObject private var currentInstance: CurrentInstance
@EnvironmentObject private var preferences: UserPreferences
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var client: Client
@Environment(Client.self) private var client
@EnvironmentObject private var routerPath: 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) { oldValue, 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)

View file

@ -3,9 +3,10 @@ import Models
import Network
import Status
import SwiftUI
import Observation
@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<Void, Never>?

View file

@ -5,13 +5,14 @@ import Env
import Models
import Network
import SwiftUI
import Observation
@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
@ -23,9 +24,9 @@ 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(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
}
@ -118,7 +119,7 @@ public struct AccountsListRow: View {
.background(theme.primaryBackgroundColor)
.environmentObject(theme)
.environmentObject(currentAccount)
.environmentObject(client)
.environment(client)
}
}
}

View file

@ -7,13 +7,13 @@ import SwiftUI
public struct AccountsListView: View {
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var client: Client
@Environment(Client.self) private var client
@EnvironmentObject private var currentAccount: CurrentAccount
@StateObject private var viewModel: AccountsListViewModel
@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 {

View file

@ -1,6 +1,7 @@
import Models
import Network
import SwiftUI
import Observation
public enum AccountsListMode {
case following(accountId: String), followers(accountId: String)
@ -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?

View file

@ -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() {}

View file

@ -1,13 +1,14 @@
import Models
import Network
import SwiftUI
import Observation
@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() {}

View file

@ -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 {

View file

@ -1,14 +1,15 @@
import Network
import SwiftUI
import Observation
@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() {}

View file

@ -9,7 +9,7 @@ struct EditFilterView: View {
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var account: CurrentAccount
@EnvironmentObject private var client: Client
@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) { oldValue, 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()
}

View file

@ -9,7 +9,7 @@ public struct FiltersListView: View {
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var account: CurrentAccount
@EnvironmentObject private var client: Client
@Environment(Client.self) private var client
@State private var isLoading: Bool = true
@State private var filters: [ServerFilter] = []

View file

@ -3,16 +3,17 @@ import Foundation
import Models
import Network
import SwiftUI
import Observation
@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 {

View file

@ -7,7 +7,7 @@ let package = Package(
name: "AppAccount",
defaultLocalization: "en",
platforms: [
.iOS(.v16),
.iOS(.v17),
],
products: [
.library(

View file

@ -6,13 +6,13 @@ import SwiftUI
public struct AppAccountView: View {
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var routerPath: RouterPath
@EnvironmentObject private var appAccounts: AppAccountsManager
@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 {

View file

@ -3,9 +3,10 @@ import DesignSystem
import Models
import Network
import SwiftUI
import Observation
@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)

View file

@ -3,13 +3,14 @@ import Env
import Models
import Network
import SwiftUI
import Observation
@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 }

View file

@ -5,7 +5,7 @@ import SwiftUI
public struct AppAccountsSelectorView: View {
@EnvironmentObject private var preferences: UserPreferences
@EnvironmentObject private var currentAccount: CurrentAccount
@EnvironmentObject private var appAccounts: AppAccountsManager
@Environment(AppAccountsManager.self) private var appAccounts
@EnvironmentObject private var theme: Theme
@ObservedObject var routerPath: RouterPath
@ -61,7 +61,7 @@ public struct AppAccountsSelectorView: View {
}
}
})
.onChange(of: currentAccount.account?.id) { _ in
.onChange(of: currentAccount.account?.id) {
refreshAccounts()
}
.onAppear {

View file

@ -7,7 +7,7 @@ let package = Package(
name: "Conversations",
defaultLocalization: "en",
platforms: [
.iOS(.v16),
.iOS(.v17),
],
products: [
.library(

View file

@ -13,7 +13,7 @@ public struct ConversationDetailView: View {
@EnvironmentObject private var quickLook: QuickLook
@EnvironmentObject private var routerPath: RouterPath
@EnvironmentObject private var currentAccount: CurrentAccount
@EnvironmentObject private var client: Client
@Environment(Client.self) private var client
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var watcher: StreamWatcher
@ -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 {

View file

@ -9,7 +9,7 @@ struct ConversationMessageView: View {
@EnvironmentObject private var quickLook: QuickLook
@EnvironmentObject private var routerPath: RouterPath
@EnvironmentObject private var currentAccount: CurrentAccount
@EnvironmentObject private var client: Client
@Environment(Client.self) private var client
@EnvironmentObject private var theme: Theme
let message: Status

View file

@ -6,7 +6,7 @@ import Network
import SwiftUI
struct ConversationsListRow: View {
@EnvironmentObject private var client: Client
@Environment(Client.self) private var client
@EnvironmentObject private var routerPath: RouterPath
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var currentAccount: CurrentAccount

View file

@ -9,7 +9,7 @@ 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(Client.self) private var client
@EnvironmentObject private var theme: Theme
@StateObject private var viewModel = ConversationsListViewModel()
@ -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)
}

View file

@ -7,7 +7,7 @@ let package = Package(
name: "DesignSystem",
defaultLocalization: "en",
platforms: [
.iOS(.v16),
.iOS(.v17),
],
products: [
.library(

View file

@ -41,16 +41,16 @@ struct ThemeApplier: ViewModifier {
setWindowUserInterfaceStyle(from: theme.selectedScheme)
setBarsColor(theme.primaryBackgroundColor)
}
.onChange(of: theme.tintColor) { newValue in
.onChange(of: theme.tintColor) { oldValue, newValue in
setWindowTint(newValue)
}
.onChange(of: theme.primaryBackgroundColor) { newValue in
.onChange(of: theme.primaryBackgroundColor) { oldValue, newValue in
setBarsColor(newValue)
}
.onChange(of: theme.selectedScheme) { newValue in
.onChange(of: theme.selectedScheme) { oldValue, newValue in
setWindowUserInterfaceStyle(from: newValue)
}
.onChange(of: colorScheme) { newColorScheme in
.onChange(of: colorScheme) { oldValue, newColorScheme in
if theme.followSystemColorScheme,
let sets = availableColorsSets
.first(where: { $0.light.name == theme.selectedSet || $0.dark.name == theme.selectedSet })

View file

@ -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) { oldValue, newValue in
isSelected = newValue.rawValue == color.name.rawValue
}
.onTapGesture {

View file

@ -7,7 +7,7 @@ let package = Package(
name: "Env",
defaultLocalization: "en",
platforms: [
.iOS(.v16),
.iOS(.v17),
],
products: [
.library(

View file

@ -7,7 +7,7 @@ let package = Package(
name: "Explore",
defaultLocalization: "en",
platforms: [
.iOS(.v16),
.iOS(.v17),
],
products: [
.library(

View file

@ -9,7 +9,7 @@ import SwiftUI
public struct ExploreView: View {
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var client: Client
@Environment(Client.self) private var client
@EnvironmentObject private var routerPath: RouterPath
@StateObject private var viewModel = ExploreViewModel()
@ -117,7 +117,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 +148,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 +196,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)
}

View file

@ -7,7 +7,7 @@ let package = Package(
name: "Lists",
defaultLocalization: "en",
platforms: [
.iOS(.v16),
.iOS(.v17),
],
products: [
.library(

View file

@ -6,7 +6,7 @@ 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

View file

@ -7,7 +7,7 @@ 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

View file

@ -7,7 +7,7 @@ let package = Package(
name: "Models",
defaultLocalization: "en",
platforms: [
.iOS(.v16),
.iOS(.v17),
],
products: [
.library(

View file

@ -7,7 +7,7 @@ let package = Package(
name: "Network",
defaultLocalization: "en",
platforms: [
.iOS(.v16),
.iOS(.v17),
],
products: [
.library(

View file

@ -3,8 +3,9 @@ import Foundation
import Models
import os
import SwiftUI
import Observation
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 }

View file

@ -7,7 +7,7 @@ let package = Package(
name: "Notifications",
defaultLocalization: "en",
platforms: [
.iOS(.v16),
.iOS(.v17),
],
products: [
.library(

View file

@ -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()

View file

@ -9,7 +9,7 @@ public struct NotificationsListView: View {
@Environment(\.scenePhase) private var scenePhase
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var watcher: StreamWatcher
@EnvironmentObject private var client: Client
@Environment(Client.self) private var client
@EnvironmentObject private var routerPath: RouterPath
@EnvironmentObject private var account: CurrentAccount
@StateObject private var viewModel = NotificationsViewModel()
@ -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) { oldValue, newValue in
switch newValue {
case .active:
Task {
await viewModel.fetchNotifications()
@ -102,7 +102,7 @@ public struct NotificationsListView: View {
default:
break
}
})
}
}
@ViewBuilder

View file

@ -7,7 +7,7 @@ let package = Package(
name: "Status",
defaultLocalization: "en",
platforms: [
.iOS(.v16),
.iOS(.v17),
],
products: [
.library(

View file

@ -9,7 +9,7 @@ public struct StatusDetailView: View {
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var currentAccount: CurrentAccount
@EnvironmentObject private var watcher: StreamWatcher
@EnvironmentObject private var client: Client
@Environment(Client.self) private var client
@EnvironmentObject private var routerPath: RouterPath
@StateObject private var viewModel: StatusDetailViewModel
@ -69,10 +69,10 @@ 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, { oldValue, newValue in
if let newValue {
viewModel.scrollToId = nil
proxy.scrollTo(scrollToId, anchor: .top)
proxy.scrollTo(newValue, anchor: .top)
}
})
.task {
@ -92,7 +92,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)
}
@ -132,7 +132,7 @@ public struct StatusDetailView: View {
makeCurrentStatusView(status: status)
.environment(\.extraLeadingInset, isReplyToPrevious ? 10 : 0)
} else {
StatusRowView(viewModel: { viewModel })
StatusRowView(viewModel: viewModel)
.environment(\.extraLeadingInset, isReplyToPrevious ? 10 : 0)
}
}
@ -145,9 +145,9 @@ public struct StatusDetailView: View {
}
private func makeCurrentStatusView(status: Status) -> some View {
StatusRowView(viewModel: { .init(status: status,
client: client,
routerPath: routerPath) })
StatusRowView(viewModel: .init(status: status,
client: client,
routerPath: routerPath))
.environment(\.isStatusFocused, true)
.environment(\.isStatusDetailLoaded, !viewModel.isLoadingContext)
.accessibilityFocused($initialFocusBugWorkaround, equals: true)
@ -181,7 +181,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)
}
}

View file

@ -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 {

View file

@ -12,10 +12,10 @@ 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
@Environment(Client.self) private var client
@EnvironmentObject private var currentAccount: CurrentAccount
@EnvironmentObject private var routerPath: RouterPath
@Environment(\.dismiss) private var dismiss
@ -90,9 +90,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)
@ -161,10 +161,10 @@ public struct StatusEditorView: View {
}
}
.interactiveDismissDisabled(viewModel.shouldDisplayDismissWarning)
.onChange(of: appAccounts.currentClient) { newClient in
.onChange(of: appAccounts.currentClient) { oldValue, newValue in
if viewModel.mode.isInShareExtension {
currentAccount.setClient(client: newClient)
viewModel.client = newClient
currentAccount.setClient(client: newValue)
viewModel.client = newValue
}
}
}

View file

@ -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)
}

View file

@ -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

View file

@ -29,7 +29,7 @@ public struct StatusesListView<Fetcher>: 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<Fetcher>: 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)

View file

@ -2,10 +2,11 @@ import AVKit
import DesignSystem
import Env
import SwiftUI
import Observation
@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) { oldValue, newValue in
switch newValue {
case .background, .inactive:
viewModel.pause()
case .active:
@ -86,6 +87,6 @@ struct VideoPlayerView: View {
default:
break
}
})
}
}
}

View file

@ -6,15 +6,16 @@ import SwiftUI
public struct StatusPollView: View {
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var client: Client
@Environment(Client.self) private var client
@EnvironmentObject private var currentInstance: CurrentInstance
@EnvironmentObject private var currentAccount: CurrentAccount
@StateObject private var viewModel: StatusPollViewModel
@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
}

View file

@ -2,14 +2,15 @@ import Combine
import Models
import Network
import SwiftUI
import Observation
@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

View file

@ -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) { oldValue, 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()
}
}

View file

@ -17,11 +17,10 @@ public struct StatusRowView: View {
@EnvironmentObject private var quickLook: QuickLook
@EnvironmentObject private var theme: Theme
@StateObject var viewModel: StatusRowViewModel
@State 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 {

View file

@ -5,9 +5,10 @@ import Models
import NaturalLanguage
import Network
import SwiftUI
import Observation
@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
@ -15,26 +16,26 @@ public class StatusRowViewModel: ObservableObject {
let textDisabled: Bool
let finalStatus: AnyStatus
@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] = []
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,14 +47,14 @@ 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? = nil
// post length determining if the post should be collapsed
let collapseThresholdLength: Int = 750
// number of text lines to show on a collpased post

View file

@ -13,7 +13,7 @@ struct StatusRowActionsView: View {
@Environment(\.isStatusFocused) private var isFocused
@Environment(\.isStatusDetailLoaded) private var isStatusDetailLoaded
@ObservedObject var viewModel: StatusRowViewModel
var viewModel: StatusRowViewModel
func privateBoost() -> Bool {

View file

@ -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)
}

View file

@ -7,14 +7,14 @@ import SwiftUI
struct StatusRowContextMenu: View {
@Environment(\.displayScale) var displayScale
@EnvironmentObject private var client: Client
@Environment(Client.self) private var client
@EnvironmentObject private var sceneDelegate: SceneDelegate
@EnvironmentObject private var preferences: UserPreferences
@EnvironmentObject private var account: CurrentAccount
@EnvironmentObject private var currentInstance: CurrentInstance
@EnvironmentObject private var statusDataController: 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,7 +81,7 @@ struct StatusRowContextMenu: View {
Button {
let view = HStack {
StatusRowView(viewModel: { viewModel })
StatusRowView(viewModel: viewModel)
.padding(16)
}
.environment(\.isInCaptureMode, true)
@ -91,7 +91,7 @@ struct StatusRowContextMenu: View {
.environmentObject(currentInstance)
.environmentObject(SceneDelegate())
.environmentObject(QuickLook())
.environmentObject(viewModel.client)
.environment(viewModel.client)
.preferredColorScheme(Theme.shared.selectedScheme == .dark ? .dark : .light)
.foregroundColor(Theme.shared.labelColor)
.background(Theme.shared.primaryBackgroundColor)

View file

@ -8,7 +8,7 @@ struct StatusRowDetailView: View {
@EnvironmentObject private var statusDataController: StatusDataController
@ObservedObject var viewModel: StatusRowViewModel
var viewModel: StatusRowViewModel
var body: some View {
Group {

View file

@ -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 {

View file

@ -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 {

View file

@ -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()

View file

@ -7,7 +7,7 @@ let package = Package(
name: "Timeline",
defaultLocalization: "en",
platforms: [
.iOS(.v16),
.iOS(.v17),
],
products: [
.library(

View file

@ -2,10 +2,11 @@ import Env
import Foundation
import Models
import SwiftUI
import Observation
@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 {

View file

@ -16,7 +16,7 @@ public struct TimelineView: View {
@EnvironmentObject private var theme: Theme
@EnvironmentObject private var account: CurrentAccount
@EnvironmentObject private var watcher: StreamWatcher
@EnvironmentObject private var client: Client
@Environment(Client.self) private var client
@EnvironmentObject private var routerPath: RouterPath
@StateObject private var viewModel = TimelineViewModel()
@ -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) { oldValue, 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) { oldValue, 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) { oldValue, newValue in
timeline = newValue
})
.onChange(of: scenePhase, perform: { scenePhase in
switch scenePhase {
}
.onChange(of: scenePhase) { oldValue, newValue in
switch newValue {
case .active:
if wasBackgrounded {
wasBackgrounded = false
@ -175,7 +175,7 @@ public struct TimelineView: View {
default:
break
}
})
}
}
@ViewBuilder