From 27d44340e88ecade6910e6670c6d345be39df108 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sat, 27 Jan 2024 15:48:58 -0500 Subject: [PATCH] Even more strict concurrency fixes --- .../Pachyderm/Model/StatusSource.swift | 2 +- .../Utilities/InstanceSelector.swift | 2 +- Tusker/API/MastodonController.swift | 23 ++------- .../MastodonCachePersistentStore.swift | 13 +++-- Tusker/Preferences/StatusSwipeActions.swift | 1 + ...ConversationCollectionViewController.swift | 6 +-- .../Conversation/ConversationTree.swift | 5 ++ .../ConversationViewController.swift | 2 +- .../FeaturedProfileCollectionViewCell.swift | 47 ++++++++++--------- .../SuggestedProfilesViewController.swift | 8 +++- .../TrendingHashtagsViewController.swift | 32 +++++++++---- .../Explore/TrendingLinksViewController.swift | 32 +++++++++---- .../TrendingStatusesViewController.swift | 8 +++- .../Explore/TrendsViewController.swift | 6 +-- .../FastSwitchingAccountView.swift | 21 +++++---- .../LoadingLargeImageViewController.swift | 2 +- .../EditListAccountsViewController.swift | 24 +++++++--- ...otificationsCollectionViewController.swift | 11 +++-- .../InstanceSelectorTableViewController.swift | 2 +- .../Preferences/LocalAccountAvatarView.swift | 19 ++++---- .../Profile/MyProfileViewController.swift | 11 ++--- Tusker/Screens/Utilities/Previewing.swift | 15 ++++-- Tusker/Shortcuts/AppShortcutItems.swift | 1 + Tusker/TimelineLikeController.swift | 5 +- 24 files changed, 179 insertions(+), 119 deletions(-) diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/StatusSource.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/StatusSource.swift index 01f14381..b251e769 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/StatusSource.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/StatusSource.swift @@ -7,7 +7,7 @@ import Foundation -public struct StatusSource: Decodable { +public struct StatusSource: Decodable, Sendable { public let id: String public let text: String public let spoilerText: String diff --git a/Packages/Pachyderm/Sources/Pachyderm/Utilities/InstanceSelector.swift b/Packages/Pachyderm/Sources/Pachyderm/Utilities/InstanceSelector.swift index 6ad7c2ff..5f44fa0f 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Utilities/InstanceSelector.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Utilities/InstanceSelector.swift @@ -49,7 +49,7 @@ public class InstanceSelector { } public extension InstanceSelector { - struct Instance: Codable { + struct Instance: Codable, Sendable { public let domain: String public let description: String public let proxiedThumbnailURL: URL diff --git a/Tusker/API/MastodonController.swift b/Tusker/API/MastodonController.swift index 82ecfe0a..c308ff82 100644 --- a/Tusker/API/MastodonController.swift +++ b/Tusker/API/MastodonController.swift @@ -51,6 +51,7 @@ class MastodonController: ObservableObject { let instanceURL: URL let accountInfo: UserAccountInfo? + @MainActor private(set) var accountPreferences: AccountPreferences! private(set) var client: Client! @@ -271,17 +272,6 @@ class MastodonController: ObservableObject { } } - func getOwnAccount(completion: (@Sendable (Result) -> Void)? = nil) { - Task.detached { - do { - let account = try await self.getOwnAccount() - completion?(.success(account)) - } catch { - completion?(.failure(error)) - } - } - } - @MainActor func getOwnAccount() async throws -> AccountMO { if let account { @@ -307,15 +297,8 @@ class MastodonController: ObservableObject { return MainThreadBox(value: accountMO) } } - } - if let account = account { - return account - } else { - return try await withCheckedThrowingContinuation({ continuation in - self.getOwnAccount { result in - continuation.resume(with: result) - } - }) + fetchOwnAccountTask = task + return try await task.value.value } } diff --git a/Tusker/CoreData/MastodonCachePersistentStore.swift b/Tusker/CoreData/MastodonCachePersistentStore.swift index 17d55be7..7b820a90 100644 --- a/Tusker/CoreData/MastodonCachePersistentStore.swift +++ b/Tusker/CoreData/MastodonCachePersistentStore.swift @@ -550,14 +550,19 @@ class MastodonCachePersistentStore: NSPersistentCloudKitContainer { } } } + // Can't capture vars in concurrently-executing closure + let hashtags = changedHashtags + let instances = changedInstances + let timelinePositions = changedTimelinePositions + let accountPrefs = changedAccountPrefs DispatchQueue.main.async { - if changedHashtags { + if hashtags { NotificationCenter.default.post(name: .savedHashtagsChanged, object: nil) } - if changedInstances { + if instances { NotificationCenter.default.post(name: .savedInstancesChanged, object: nil) } - for id in changedTimelinePositions { + for id in timelinePositions { guard let timelinePosition = try? self.viewContext.existingObject(with: id) as? TimelinePosition else { continue } @@ -565,7 +570,7 @@ class MastodonCachePersistentStore: NSPersistentCloudKitContainer { timelinePosition.changedRemotely() NotificationCenter.default.post(name: .timelinePositionChanged, object: timelinePosition) } - if changedAccountPrefs { + if accountPrefs { NotificationCenter.default.post(name: .accountPreferencesChangedRemotely, object: nil) } } diff --git a/Tusker/Preferences/StatusSwipeActions.swift b/Tusker/Preferences/StatusSwipeActions.swift index 5e2a6eaf..2dd1641e 100644 --- a/Tusker/Preferences/StatusSwipeActions.swift +++ b/Tusker/Preferences/StatusSwipeActions.swift @@ -30,6 +30,7 @@ extension StatusSwipeAction { } } +@MainActor protocol StatusSwipeActionContainer: UIView { var mastodonController: MastodonController! { get } var navigationDelegate: any TuskerNavigationDelegate { get } diff --git a/Tusker/Screens/Conversation/ConversationCollectionViewController.swift b/Tusker/Screens/Conversation/ConversationCollectionViewController.swift index 088c7db0..1712e984 100644 --- a/Tusker/Screens/Conversation/ConversationCollectionViewController.swift +++ b/Tusker/Screens/Conversation/ConversationCollectionViewController.swift @@ -296,7 +296,7 @@ extension ConversationCollectionViewController { case mainStatus case childThread(firstStatusID: String) } - enum Item: Hashable { + enum Item: Hashable, Sendable { case status(id: String, node: ConversationNode, state: CollapseState, prevLink: Bool, nextLink: Bool) case expandThread(childThreads: [ConversationNode], inline: Bool) case loadingIndicator @@ -306,7 +306,7 @@ extension ConversationCollectionViewController { case let (.status(id: a, node: _, state: _, prevLink: aPrev, nextLink: aNext), .status(id: b, node: _, state: _, prevLink: bPrev, nextLink: bNext)): return a == b && aPrev == bPrev && aNext == bNext case let (.expandThread(childThreads: a, inline: aInline), .expandThread(childThreads: b, inline: bInline)): - return a.count == b.count && zip(a, b).allSatisfy { $0.status.id == $1.status.id } && aInline == bInline + return a.count == b.count && zip(a, b).allSatisfy { $0.statusID == $1.statusID } && aInline == bInline case (.loadingIndicator, .loadingIndicator): return true default: @@ -324,7 +324,7 @@ extension ConversationCollectionViewController { case .expandThread(childThreads: let childThreads, inline: let inline): hasher.combine(1) for thread in childThreads { - hasher.combine(thread.status.id) + hasher.combine(thread.statusID) } hasher.combine(inline) case .loadingIndicator: diff --git a/Tusker/Screens/Conversation/ConversationTree.swift b/Tusker/Screens/Conversation/ConversationTree.swift index 19c47ba3..257fba3e 100644 --- a/Tusker/Screens/Conversation/ConversationTree.swift +++ b/Tusker/Screens/Conversation/ConversationTree.swift @@ -9,15 +9,20 @@ import Foundation import Pachyderm +@MainActor class ConversationNode { + let statusID: String let status: StatusMO var children: [ConversationNode] init(status: StatusMO) { + self.statusID = status.id self.status = status self.children = [] } } + +@MainActor struct ConversationTree { let ancestors: [ConversationNode] let mainStatus: ConversationNode diff --git a/Tusker/Screens/Conversation/ConversationViewController.swift b/Tusker/Screens/Conversation/ConversationViewController.swift index 42e6ff1f..43ae6ef7 100644 --- a/Tusker/Screens/Conversation/ConversationViewController.swift +++ b/Tusker/Screens/Conversation/ConversationViewController.swift @@ -194,7 +194,7 @@ class ConversationViewController: UIViewController { if let cached = mastodonController.persistentContainer.status(for: mainStatusID) { // if we have a cached copy, display it immediately but still try to refresh it Task { - await doLoadMainStatus() + _ = await doLoadMainStatus() } mainStatusLoaded(cached) } else { diff --git a/Tusker/Screens/Explore/FeaturedProfileCollectionViewCell.swift b/Tusker/Screens/Explore/FeaturedProfileCollectionViewCell.swift index 2418e631..b253e068 100644 --- a/Tusker/Screens/Explore/FeaturedProfileCollectionViewCell.swift +++ b/Tusker/Screens/Explore/FeaturedProfileCollectionViewCell.swift @@ -20,8 +20,11 @@ class FeaturedProfileCollectionViewCell: UICollectionViewCell { var account: Account? - private var avatarRequest: ImageCache.Request? - private var headerRequest: ImageCache.Request? + private var accountImagesTask: Task? + + deinit { + accountImagesTask?.cancel() + } override func awakeFromNib() { super.awakeFromNib() @@ -62,37 +65,35 @@ class FeaturedProfileCollectionViewCell: UICollectionViewCell { noteTextView.setEmojis(account.emojis, identifier: account.id) avatarImageView.image = nil - if let avatar = account.avatar { - avatarRequest = ImageCache.avatars.get(avatar) { [weak self] (_, image) in - defer { - self?.avatarRequest = nil - } - guard let self = self, - let image = image, - self.account?.id == account.id else { + headerImageView.image = nil + + accountImagesTask?.cancel() + accountImagesTask = Task { + await updateImages(account: account) + } + } + + private nonisolated func updateImages(account: Account) async { + await withTaskGroup(of: Void.self) { group in + group.addTask { + guard let avatar = account.avatar, + let image = await ImageCache.avatars.get(avatar).1 else { return } - DispatchQueue.main.async { + await MainActor.run { self.avatarImageView.image = image } } - } - - headerImageView.image = nil - if let header = account.header { - headerRequest = ImageCache.headers.get(header) { [weak self] (_, image) in - defer { - self?.headerRequest = nil - } - guard let self = self, - let image = image, - self.account?.id == account.id else { + group.addTask { + guard let header = account.header, + let image = await ImageCache.headers.get(header).1 else { return } - DispatchQueue.main.async { + await MainActor.run { self.headerImageView.image = image } } + await group.waitForAll() } } diff --git a/Tusker/Screens/Explore/SuggestedProfilesViewController.swift b/Tusker/Screens/Explore/SuggestedProfilesViewController.swift index b9785ce8..7395fc1e 100644 --- a/Tusker/Screens/Explore/SuggestedProfilesViewController.swift +++ b/Tusker/Screens/Explore/SuggestedProfilesViewController.swift @@ -95,7 +95,9 @@ class SuggestedProfilesViewController: UIViewController, CollectionViewControlle var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.accounts]) - await dataSource.apply(snapshot) + await MainActor.run { + dataSource.apply(snapshot) + } do { let request = Client.getSuggestions(limit: 80) @@ -108,7 +110,9 @@ class SuggestedProfilesViewController: UIViewController, CollectionViewControlle var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.accounts]) snapshot.appendItems(suggestions.map { .account($0.account.id, $0.source) }) - await dataSource.apply(snapshot) + await MainActor.run { + dataSource.apply(snapshot) + } state = .loaded } catch { diff --git a/Tusker/Screens/Explore/TrendingHashtagsViewController.swift b/Tusker/Screens/Explore/TrendingHashtagsViewController.swift index 2888676b..0206790e 100644 --- a/Tusker/Screens/Explore/TrendingHashtagsViewController.swift +++ b/Tusker/Screens/Explore/TrendingHashtagsViewController.swift @@ -107,7 +107,9 @@ class TrendingHashtagsViewController: UIViewController { var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.trendingTags]) snapshot.appendItems([.loadingIndicator]) - await dataSource.apply(snapshot) + await MainActor.run { + dataSource.apply(snapshot) + } do { let request = self.request(offset: nil) @@ -115,10 +117,14 @@ class TrendingHashtagsViewController: UIViewController { snapshot.deleteItems([.loadingIndicator]) snapshot.appendItems(hashtags.map { .tag($0) }) state = .loaded - await dataSource.apply(snapshot) + await MainActor.run { + dataSource.apply(snapshot) + } } catch { snapshot.deleteItems([.loadingIndicator]) - await dataSource.apply(snapshot) + await MainActor.run { + dataSource.apply(snapshot) + } state = .unloaded let config = ToastConfiguration(from: error, with: "Error Loading Trending Tags", in: self) { [weak self] toast in @@ -140,7 +146,9 @@ class TrendingHashtagsViewController: UIViewController { var snapshot = origSnapshot if Preferences.shared.disableInfiniteScrolling { snapshot.appendItems([.confirmLoadMore(false)]) - await dataSource.apply(snapshot) + await MainActor.run { + dataSource.apply(snapshot) + } for await _ in confirmLoadMore.values { break @@ -148,10 +156,14 @@ class TrendingHashtagsViewController: UIViewController { snapshot.deleteItems([.confirmLoadMore(false)]) snapshot.appendItems([.confirmLoadMore(true)]) - await dataSource.apply(snapshot, animatingDifferences: false) + await MainActor.run { + dataSource.apply(snapshot, animatingDifferences: false) + } } else { snapshot.appendItems([.loadingIndicator]) - await dataSource.apply(snapshot) + await MainActor.run { + dataSource.apply(snapshot) + } } do { @@ -159,9 +171,13 @@ class TrendingHashtagsViewController: UIViewController { let (hashtags, _) = try await mastodonController.run(request) var snapshot = origSnapshot snapshot.appendItems(hashtags.map { .tag($0) }) - await dataSource.apply(snapshot) + await MainActor.run { + dataSource.apply(snapshot) + } } catch { - await dataSource.apply(origSnapshot) + await MainActor.run { + dataSource.apply(origSnapshot) + } let config = ToastConfiguration(from: error, with: "Error Loading More Tags", in: self) { [weak self] toast in toast.dismissToast(animated: true) diff --git a/Tusker/Screens/Explore/TrendingLinksViewController.swift b/Tusker/Screens/Explore/TrendingLinksViewController.swift index faa1d6e4..1e07eff6 100644 --- a/Tusker/Screens/Explore/TrendingLinksViewController.swift +++ b/Tusker/Screens/Explore/TrendingLinksViewController.swift @@ -128,7 +128,9 @@ class TrendingLinksViewController: UIViewController, CollectionViewController { var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.loadingIndicator]) snapshot.appendItems([.loadingIndicator]) - await dataSource.apply(snapshot) + await MainActor.run { + dataSource.apply(snapshot) + } do { let request = Client.getTrendingLinks() @@ -137,9 +139,13 @@ class TrendingLinksViewController: UIViewController, CollectionViewController { snapshot.appendSections([.links]) snapshot.appendItems(links.map { .link($0) }) state = .loaded - await dataSource.apply(snapshot) + await MainActor.run { + dataSource.apply(snapshot) + } } catch { - await dataSource.apply(NSDiffableDataSourceSnapshot()) + await MainActor.run { + dataSource.apply(NSDiffableDataSourceSnapshot()) + } state = .unloaded let config = ToastConfiguration(from: error, with: "Error Loading Trending Links", in: self) { [weak self] toast in toast.dismissToast(animated: true) @@ -161,7 +167,9 @@ class TrendingLinksViewController: UIViewController, CollectionViewController { if Preferences.shared.disableInfiniteScrolling { snapshot.appendSections([.loadingIndicator]) snapshot.appendItems([.confirmLoadMore(false)], toSection: .loadingIndicator) - await dataSource.apply(snapshot) + await MainActor.run { + dataSource.apply(snapshot) + } for await _ in confirmLoadMore.values { break @@ -169,11 +177,15 @@ class TrendingLinksViewController: UIViewController, CollectionViewController { snapshot.deleteItems([.confirmLoadMore(false)]) snapshot.appendItems([.confirmLoadMore(true)], toSection: .loadingIndicator) - await dataSource.apply(snapshot, animatingDifferences: false) + await MainActor.run { + dataSource.apply(snapshot, animatingDifferences: false) + } } else { snapshot.appendSections([.loadingIndicator]) snapshot.appendItems([.loadingIndicator], toSection: .loadingIndicator) - await dataSource.apply(snapshot) + await MainActor.run { + dataSource.apply(snapshot) + } } do { @@ -181,9 +193,13 @@ class TrendingLinksViewController: UIViewController, CollectionViewController { let (links, _) = try await mastodonController.run(request) var snapshot = origSnapshot snapshot.appendItems(links.map { .link($0) }, toSection: .links) - await dataSource.apply(snapshot) + await MainActor.run { + dataSource.apply(snapshot) + } } catch { - await dataSource.apply(origSnapshot) + await MainActor.run { + dataSource.apply(origSnapshot) + } let config = ToastConfiguration(from: error, with: "Erorr Loading More Links", in: self) { [weak self] toast in toast.dismissToast(animated: true) await self?.loadOlder() diff --git a/Tusker/Screens/Explore/TrendingStatusesViewController.swift b/Tusker/Screens/Explore/TrendingStatusesViewController.swift index f8bc4e73..177b4d15 100644 --- a/Tusker/Screens/Explore/TrendingStatusesViewController.swift +++ b/Tusker/Screens/Explore/TrendingStatusesViewController.swift @@ -126,7 +126,9 @@ class TrendingStatusesViewController: UIViewController, CollectionViewController statuses = try await mastodonController.run(Client.getTrendingStatuses()).0 } catch { let snapshot = NSDiffableDataSourceSnapshot() - await dataSource.apply(snapshot) + await MainActor.run { + dataSource.apply(snapshot) + } let config = ToastConfiguration(from: error, with: "Loading Trending Posts", in: self) { toast in toast.dismissToast(animated: true) await self.loadTrendingStatuses() @@ -138,7 +140,9 @@ class TrendingStatusesViewController: UIViewController, CollectionViewController var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.statuses]) snapshot.appendItems(statuses.map { .status(id: $0.id, collapseState: .unknown, filterState: .unknown) }) - await dataSource.apply(snapshot) + await MainActor.run { + dataSource.apply(snapshot) + } } @objc private func handleStatusDeleted(_ notification: Foundation.Notification) { diff --git a/Tusker/Screens/Explore/TrendsViewController.swift b/Tusker/Screens/Explore/TrendsViewController.swift index 1f94172e..765eccef 100644 --- a/Tusker/Screens/Explore/TrendsViewController.swift +++ b/Tusker/Screens/Explore/TrendsViewController.swift @@ -238,7 +238,7 @@ class TrendsViewController: UIViewController, CollectionViewController { } isShowingTrends = shouldShowTrends guard shouldShowTrends else { - await dataSource.apply(NSDiffableDataSourceSnapshot()) + await apply(snapshot: NSDiffableDataSourceSnapshot()) return } @@ -355,9 +355,9 @@ class TrendsViewController: UIViewController, CollectionViewController { } private func apply(snapshot: NSDiffableDataSourceSnapshot, animatingDifferences: Bool = true) async { - await Task { @MainActor in + await MainActor.run { self.dataSource.apply(snapshot, animatingDifferences: animatingDifferences) - }.value + } } @MainActor diff --git a/Tusker/Screens/Fast Account Switcher/FastSwitchingAccountView.swift b/Tusker/Screens/Fast Account Switcher/FastSwitchingAccountView.swift index 61b5b47a..a4de09d0 100644 --- a/Tusker/Screens/Fast Account Switcher/FastSwitchingAccountView.swift +++ b/Tusker/Screens/Fast Account Switcher/FastSwitchingAccountView.swift @@ -49,7 +49,7 @@ class FastSwitchingAccountView: UIView { private let instanceLabel = UILabel() private let avatarImageView = UIImageView() - private var avatarRequest: ImageCache.Request? + private var avatarTask: Task? init(account: UserAccountInfo, orientation: FastAccountSwitcherViewController.ItemOrientation) { self.orientation = orientation @@ -69,6 +69,10 @@ class FastSwitchingAccountView: UIView { fatalError("init(coder:) has not been implemented") } + deinit { + avatarTask?.cancel() + } + private func commonInit() { usernameLabel.textColor = .white usernameLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .headline), size: 0) @@ -133,16 +137,13 @@ class FastSwitchingAccountView: UIView { instanceLabel.text = account.instanceURL.host! } let controller = MastodonController.getForAccount(account) - controller.getOwnAccount { [weak self] (result) in - guard let self = self, - case let .success(account) = result, - let avatar = account.avatar else { return } - self.avatarRequest = ImageCache.avatars.get(avatar) { [weak self] (_, image) in - guard let self = self, let image = image else { return } - DispatchQueue.main.async { - self.avatarImageView.image = image - } + avatarTask = Task { + guard let account = try? await controller.getOwnAccount(), + let avatar = account.avatar, + let image = await ImageCache.avatars.get(avatar).1 else { + return } + self.avatarImageView.image = image } accessibilityLabel = "\(account.username!)@\(instanceLabel.text!)" diff --git a/Tusker/Screens/Large Image/LoadingLargeImageViewController.swift b/Tusker/Screens/Large Image/LoadingLargeImageViewController.swift index 5f81f19b..4df24731 100644 --- a/Tusker/Screens/Large Image/LoadingLargeImageViewController.swift +++ b/Tusker/Screens/Large Image/LoadingLargeImageViewController.swift @@ -105,8 +105,8 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie embedChild(loadingVC!) imageRequest = cache.get(url, loadOriginal: true) { [weak self] (data, image) in guard let self = self, let image = image else { return } - self.imageRequest = nil DispatchQueue.main.async { + self.imageRequest = nil self.loadingVC?.removeViewAndController() self.createLargeImage(data: data, image: image, url: self.url) } diff --git a/Tusker/Screens/Lists/EditListAccountsViewController.swift b/Tusker/Screens/Lists/EditListAccountsViewController.swift index 40f03354..7ce03554 100644 --- a/Tusker/Screens/Lists/EditListAccountsViewController.swift +++ b/Tusker/Screens/Lists/EditListAccountsViewController.swift @@ -195,7 +195,9 @@ class EditListAccountsViewController: UIViewController, CollectionViewController var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.accounts]) snapshot.appendItems([.loadingIndicator]) - await dataSource.apply(snapshot) + await MainActor.run { + dataSource.apply(snapshot) + } do { let (accounts, pagination) = try await results @@ -210,7 +212,9 @@ class EditListAccountsViewController: UIViewController, CollectionViewController var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.accounts]) snapshot.appendItems(accounts.map { .account(id: $0.id) }) - await dataSource.apply(snapshot) + await MainActor.run { + dataSource.apply(snapshot) + } state = .loaded } catch { @@ -221,7 +225,9 @@ class EditListAccountsViewController: UIViewController, CollectionViewController self.showToast(configuration: config, animated: true) state = .unloaded - await dataSource.apply(.init()) + await MainActor.run { + dataSource.apply(.init()) + } } } @@ -236,7 +242,9 @@ class EditListAccountsViewController: UIViewController, CollectionViewController let origSnapshot = dataSource.snapshot() var snapshot = origSnapshot snapshot.appendItems([.loadingIndicator]) - await dataSource.apply(snapshot) + await MainActor.run { + dataSource.apply(snapshot) + } do { let (accounts, pagination) = try await results @@ -250,7 +258,9 @@ class EditListAccountsViewController: UIViewController, CollectionViewController var snapshot = origSnapshot snapshot.appendItems(accounts.map { .account(id: $0.id) }) - await dataSource.apply(snapshot) + await MainActor.run { + dataSource.apply(snapshot) + } state = .loaded } catch { @@ -261,7 +271,9 @@ class EditListAccountsViewController: UIViewController, CollectionViewController self.showToast(configuration: config, animated: true) state = .loaded - await dataSource.apply(origSnapshot) + await MainActor.run { + dataSource.apply(origSnapshot) + } } } diff --git a/Tusker/Screens/Notifications/NotificationsCollectionViewController.swift b/Tusker/Screens/Notifications/NotificationsCollectionViewController.swift index dea4490f..56650da9 100644 --- a/Tusker/Screens/Notifications/NotificationsCollectionViewController.swift +++ b/Tusker/Screens/Notifications/NotificationsCollectionViewController.swift @@ -273,8 +273,11 @@ class NotificationsCollectionViewController: UIViewController, TimelineLikeColle } } - private func dismissNotificationsInGroup(at indexPath: IndexPath) async { - guard case .group(let group, let collapseState, let filterState) = dataSource.itemIdentifier(for: indexPath) else { + private nonisolated func dismissNotificationsInGroup(at indexPath: IndexPath) async { + let item = await MainActor.run { + dataSource.itemIdentifier(for: indexPath) + } + guard case .group(let group, let collapseState, let filterState) = item else { return } let notifications = group.notifications @@ -295,7 +298,9 @@ class NotificationsCollectionViewController: UIViewController, TimelineLikeColle } }) } - var snapshot = dataSource.snapshot() + var snapshot = await MainActor.run { + dataSource.snapshot() + } if dismissFailedIndices.isEmpty { snapshot.deleteItems([.group(group, collapseState, filterState)]) } else if !dismissFailedIndices.isEmpty && dismissFailedIndices.count == notifications.count { diff --git a/Tusker/Screens/Onboarding/InstanceSelectorTableViewController.swift b/Tusker/Screens/Onboarding/InstanceSelectorTableViewController.swift index 786fd409..9b3344bf 100644 --- a/Tusker/Screens/Onboarding/InstanceSelectorTableViewController.swift +++ b/Tusker/Screens/Onboarding/InstanceSelectorTableViewController.swift @@ -311,7 +311,7 @@ extension InstanceSelectorTableViewController { case selected case recommendedInstances } - enum Item: Equatable, Hashable { + enum Item: Equatable, Hashable, Sendable { case selected(URL, InstanceV1) case recommended(InstanceSelector.Instance) diff --git a/Tusker/Screens/Preferences/LocalAccountAvatarView.swift b/Tusker/Screens/Preferences/LocalAccountAvatarView.swift index b586e528..1f8d3c1d 100644 --- a/Tusker/Screens/Preferences/LocalAccountAvatarView.swift +++ b/Tusker/Screens/Preferences/LocalAccountAvatarView.swift @@ -32,20 +32,19 @@ struct LocalAccountAvatarView: View { .resizable() .frame(width: 30, height: 30) .cornerRadius(preferences.avatarStyle.cornerRadiusFraction * 30) - .onAppear(perform: self.loadImage) + .task { + await self.loadImage() + } } - func loadImage() { + func loadImage() async { let controller = MastodonController.getForAccount(localAccountInfo) - controller.getOwnAccount { (result) in - guard case let .success(account) = result, - let avatar = account.avatar else { return } - _ = ImageCache.avatars.get(avatar) { (_, image) in - DispatchQueue.main.async { - self.avatarImage = image - } - } + guard let account = try? await controller.getOwnAccount(), + let avatar = account.avatar, + let image = await ImageCache.avatars.get(avatar).1 else { + return } + self.avatarImage = image } } diff --git a/Tusker/Screens/Profile/MyProfileViewController.swift b/Tusker/Screens/Profile/MyProfileViewController.swift index a859779f..c54dfb00 100644 --- a/Tusker/Screens/Profile/MyProfileViewController.swift +++ b/Tusker/Screens/Profile/MyProfileViewController.swift @@ -18,13 +18,12 @@ class MyProfileViewController: ProfileViewController { title = "My Profile" tabBarItem.image = UIImage(systemName: "person.fill") - mastodonController.getOwnAccount { (result) in - guard case let .success(account) = result else { return } - - DispatchQueue.main.async { - self.accountID = account.id - self.setAvatarTabBarImage(account: account) + Task { + guard let account = try? await mastodonController.getOwnAccount() else { + return } + self.accountID = account.id + self.setAvatarTabBarImage(account: account) } } diff --git a/Tusker/Screens/Utilities/Previewing.swift b/Tusker/Screens/Utilities/Previewing.swift index 80047595..91b7f25f 100644 --- a/Tusker/Screens/Utilities/Previewing.swift +++ b/Tusker/Screens/Utilities/Previewing.swift @@ -18,12 +18,14 @@ protocol MenuActionProvider: AnyObject { var toastableViewController: ToastableViewController? { get } } +@MainActor protocol MenuPreviewProvider: AnyObject { typealias PreviewProviders = (content: UIContextMenuContentPreviewProvider, actions: () -> [UIMenuElement]) func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? } +@MainActor protocol CustomPreviewPresenting { func presentFromPreview(presenter: UIViewController) } @@ -478,7 +480,7 @@ extension MenuActionProvider { await fetchRelationship(accountID: accountID, mastodonController: mastodonController) } Task { @MainActor in - if let relationship = await relationship.value, + if let relationship = await relationship.value?.value, let action = builder(relationship, mastodonController) { elementHandler([action]) } else { @@ -612,20 +614,25 @@ extension MenuActionProvider { } -private func fetchRelationship(accountID: String, mastodonController: MastodonController) async -> RelationshipMO? { +private func fetchRelationship(accountID: String, mastodonController: MastodonController) async -> MainThreadBox? { let req = Client.getRelationships(accounts: [accountID]) guard let (relationships, _) = try? await mastodonController.run(req), let r = relationships.first else { return nil } return await withCheckedContinuation { continuation in - mastodonController.persistentContainer.addOrUpdate(relationship: r, in: mastodonController.persistentContainer.viewContext) { mo in - continuation.resume(returning: mo) + DispatchQueue.main.async { + mastodonController.persistentContainer.addOrUpdate(relationship: r, in: mastodonController.persistentContainer.viewContext) { mo in + continuation.resume(returning: MainThreadBox(value: mo)) + } } } } struct MenuPreviewHelper { + private init() {} + + @MainActor static func willPerformPreviewAction(animator: UIContextMenuInteractionCommitAnimating, presenter: UIViewController) { if let viewController = animator.previewViewController { animator.preferredCommitStyle = .pop diff --git a/Tusker/Shortcuts/AppShortcutItems.swift b/Tusker/Shortcuts/AppShortcutItems.swift index add8d617..18b822f8 100644 --- a/Tusker/Shortcuts/AppShortcutItems.swift +++ b/Tusker/Shortcuts/AppShortcutItems.swift @@ -54,6 +54,7 @@ enum AppShortcutItem: String, CaseIterable { } extension AppShortcutItem { + @MainActor static func createItems(for application: UIApplication) { application.shortcutItems = allCases.map { return UIApplicationShortcutItem(type: $0.rawValue, localizedTitle: $0.title, localizedSubtitle: nil, icon: $0.icon, userInfo: nil) diff --git a/Tusker/TimelineLikeController.swift b/Tusker/TimelineLikeController.swift index b02c1b35..cfc64289 100644 --- a/Tusker/TimelineLikeController.swift +++ b/Tusker/TimelineLikeController.swift @@ -10,6 +10,7 @@ import Foundation import OSLog import Combine +@MainActor protocol TimelineLikeControllerDelegate: AnyObject { associatedtype TimelineItem: Sendable @@ -217,7 +218,7 @@ class TimelineLikeController { } } - enum State: Equatable, CustomDebugStringConvertible { + enum State: Equatable, CustomDebugStringConvertible, Sendable { case notLoadedInitial case idle case restoringInitial(LoadAttemptToken, hasAddedLoadingIndicator: Bool) @@ -360,7 +361,7 @@ class TimelineLikeController { } } - class LoadAttemptToken: Equatable { + final class LoadAttemptToken: Equatable, Sendable { static func ==(lhs: LoadAttemptToken, rhs: LoadAttemptToken) -> Bool { return lhs === rhs }