From b235f0e826343c5ea8d97ddda43c1a6eb704c9ae Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 28 Jan 2024 14:03:14 -0500 Subject: [PATCH] Another round of strict concurrency fixes --- OpenInTusker/ActionViewController.swift | 20 +++--- .../InstanceFeatures/InstanceFeatures.swift | 2 +- .../UserAccounts/UserAccountInfo.swift | 2 +- ShareExtension/ShareMastodonContext.swift | 6 +- Tusker/API/MastodonController.swift | 10 ++- Tusker/ImageGrayscalifier.swift | 12 ++++ .../Explore/ExploreViewController.swift | 2 +- .../ProfileDirectoryViewController.swift | 2 +- .../SuggestedProfilesViewController.swift | 2 +- .../TrendingHashtagsViewController.swift | 2 +- .../Explore/TrendingLinksViewController.swift | 2 +- .../TrendingStatusesViewController.swift | 2 +- .../LargeImageViewController.swift | 4 +- .../Main/MainSplitViewController.swift | 2 +- .../Main/MainTabBarViewController.swift | 2 +- ...otificationsCollectionViewController.swift | 2 +- .../InstanceSelectorTableViewController.swift | 7 +- .../Profile/ProfileViewController.swift | 2 +- Tusker/Screens/Report/ReportStatusView.swift | 2 + .../Search/SearchResultsViewController.swift | 2 +- .../Timeline/TimelineViewController.swift | 4 +- .../LargeAccountDetailView.swift | 2 +- Tusker/Views/CachedImageView.swift | 3 +- .../Instance Cell/InstanceTableViewCell.swift | 20 +++--- Tusker/Views/MultiSourceEmojiLabel.swift | 2 +- .../Profile Header/ProfileFieldsView.swift | 4 +- .../Profile Header/ProfileHeaderView.swift | 66 +++++++++---------- 27 files changed, 104 insertions(+), 84 deletions(-) diff --git a/OpenInTusker/ActionViewController.swift b/OpenInTusker/ActionViewController.swift index 20a94686..c252fb18 100644 --- a/OpenInTusker/ActionViewController.swift +++ b/OpenInTusker/ActionViewController.swift @@ -18,19 +18,23 @@ class ActionViewController: UIViewController { super.viewDidLoad() findURLFromWebPage { (components) in - if let components = components { - self.searchForURLInApp(components) - } else { - self.findURLItem { (components) in - if let components = components { - self.searchForURLInApp(components) + DispatchQueue.main.async { + if let components { + self.searchForURLInApp(components) + } else { + self.findURLItem { (components) in + if let components { + DispatchQueue.main.async { + self.searchForURLInApp(components) + } + } } } } } } - private func findURLFromWebPage(completion: @escaping (URLComponents?) -> Void) { + private func findURLFromWebPage(completion: @escaping @Sendable (URLComponents?) -> Void) { for item in extensionContext!.inputItems as! [NSExtensionItem] { for provider in item.attachments! { guard provider.hasItemConformingToTypeIdentifier(UTType.propertyList.identifier) else { @@ -54,7 +58,7 @@ class ActionViewController: UIViewController { completion(nil) } - private func findURLItem(completion: @escaping (URLComponents?) -> Void) { + private func findURLItem(completion: @escaping @Sendable (URLComponents?) -> Void) { for item in extensionContext!.inputItems as! [NSExtensionItem] { for provider in item.attachments! { guard provider.hasItemConformingToTypeIdentifier(UTType.url.identifier) else { diff --git a/Packages/InstanceFeatures/Sources/InstanceFeatures/InstanceFeatures.swift b/Packages/InstanceFeatures/Sources/InstanceFeatures/InstanceFeatures.swift index 5324ee1c..91b950c4 100644 --- a/Packages/InstanceFeatures/Sources/InstanceFeatures/InstanceFeatures.swift +++ b/Packages/InstanceFeatures/Sources/InstanceFeatures/InstanceFeatures.swift @@ -10,7 +10,7 @@ import Foundation import Combine import Pachyderm -public class InstanceFeatures: ObservableObject { +public final class InstanceFeatures: ObservableObject { private static let pleromaVersionRegex = try! NSRegularExpression(pattern: "\\(compatible; (pleroma|akkoma) (.*)\\)", options: .caseInsensitive) private let _featuresUpdated = PassthroughSubject() diff --git a/Packages/UserAccounts/Sources/UserAccounts/UserAccountInfo.swift b/Packages/UserAccounts/Sources/UserAccounts/UserAccountInfo.swift index 95c0134d..6d0f5c8b 100644 --- a/Packages/UserAccounts/Sources/UserAccounts/UserAccountInfo.swift +++ b/Packages/UserAccounts/Sources/UserAccounts/UserAccountInfo.swift @@ -8,7 +8,7 @@ import Foundation import CryptoKit -public struct UserAccountInfo: Equatable, Hashable, Identifiable { +public struct UserAccountInfo: Equatable, Hashable, Identifiable, Sendable { public let id: String public let instanceURL: URL public let clientID: String diff --git a/ShareExtension/ShareMastodonContext.swift b/ShareExtension/ShareMastodonContext.swift index 859f61c8..18b08c28 100644 --- a/ShareExtension/ShareMastodonContext.swift +++ b/ShareExtension/ShareMastodonContext.swift @@ -12,7 +12,7 @@ import UserAccounts import InstanceFeatures import Combine -class ShareMastodonContext: ComposeMastodonContext, ObservableObject { +final class ShareMastodonContext: ComposeMastodonContext, ObservableObject, Sendable { let accountInfo: UserAccountInfo? let client: Client let instanceFeatures: InstanceFeatures @@ -20,7 +20,9 @@ class ShareMastodonContext: ComposeMastodonContext, ObservableObject { @MainActor private var customEmojis: [Emoji]? - @Published var ownAccount: Account? + @MainActor + @Published + private(set) var ownAccount: Account? init(accountInfo: UserAccountInfo) { self.accountInfo = accountInfo diff --git a/Tusker/API/MastodonController.swift b/Tusker/API/MastodonController.swift index c308ff82..a5654569 100644 --- a/Tusker/API/MastodonController.swift +++ b/Tusker/API/MastodonController.swift @@ -20,7 +20,7 @@ import OSLog private let oauthScopes = [Scope.read, .write, .follow] private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "MastodonController") -class MastodonController: ObservableObject { +final class MastodonController: ObservableObject, Sendable { @MainActor static private(set) var all = [UserAccountInfo: MastodonController]() @@ -283,7 +283,8 @@ class MastodonController: ObservableObject { let account = try await run(Client.getSelfAccount()).0 let context = persistentContainer.viewContext - return await context.perform { + // this closure is declared separately so we can tell the compiler it's Sendable + let performBlock: @MainActor @Sendable () -> MainThreadBox = { let accountMO: AccountMO if let existing = self.persistentContainer.account(for: account.id, in: context) { accountMO = existing @@ -296,6 +297,8 @@ class MastodonController: ObservableObject { self.account = accountMO return MainThreadBox(value: accountMO) } + // it's safe to remove the MainActor annotation, because this is the view context + return await context.perform(unsafeBitCast(performBlock, to: (@Sendable () -> MainThreadBox).self)) } fetchOwnAccountTask = task return try await task.value.value @@ -342,7 +345,7 @@ class MastodonController: ObservableObject { } } - private func retrying(_ label: StaticString, action: () async throws -> T) async throws -> T { + private func retrying(_ label: StaticString, action: @Sendable () async throws -> T) async throws -> T { for attempt in 0..<4 { do { return try await action() @@ -582,6 +585,7 @@ class MastodonController: ObservableObject { ) } + @MainActor func createDraft(editing status: StatusMO, source: StatusSource) -> Draft { precondition(status.id == source.id) let draft = DraftsPersistentContainer.shared.createEditDraft( diff --git a/Tusker/ImageGrayscalifier.swift b/Tusker/ImageGrayscalifier.swift index 4119c6b8..2f6860e7 100644 --- a/Tusker/ImageGrayscalifier.swift +++ b/Tusker/ImageGrayscalifier.swift @@ -27,6 +27,18 @@ struct ImageGrayscalifier { } } + static func convertIfNecessary(url: URL?, image: UIImage) async -> UIImage? { + let grayscale = await MainActor.run { + Preferences.shared.grayscaleImages + } + if grayscale, + let source = image.cgImage { + return await convert(url: url, cgImage: source) + } else { + return image + } + } + static func convert(url: URL?, image: UIImage) -> UIImage? { if let url, let cached = cache.object(forKey: url as NSURL) { diff --git a/Tusker/Screens/Explore/ExploreViewController.swift b/Tusker/Screens/Explore/ExploreViewController.swift index 3bebfc02..f15ff818 100644 --- a/Tusker/Screens/Explore/ExploreViewController.swift +++ b/Tusker/Screens/Explore/ExploreViewController.swift @@ -14,7 +14,7 @@ import WebURLFoundationExtras class ExploreViewController: UIViewController, UICollectionViewDelegate, CollectionViewController { - weak var mastodonController: MastodonController! + private let mastodonController: MastodonController var collectionView: UICollectionView! private var dataSource: UICollectionViewDiffableDataSource! diff --git a/Tusker/Screens/Explore/ProfileDirectoryViewController.swift b/Tusker/Screens/Explore/ProfileDirectoryViewController.swift index 6b6b2499..0ae7456f 100644 --- a/Tusker/Screens/Explore/ProfileDirectoryViewController.swift +++ b/Tusker/Screens/Explore/ProfileDirectoryViewController.swift @@ -11,7 +11,7 @@ import Pachyderm class ProfileDirectoryViewController: UIViewController { - weak var mastodonController: MastodonController! + private let mastodonController: MastodonController private var collectionView: UICollectionView! private var dataSource: UICollectionViewDiffableDataSource! diff --git a/Tusker/Screens/Explore/SuggestedProfilesViewController.swift b/Tusker/Screens/Explore/SuggestedProfilesViewController.swift index 7395fc1e..7a08d984 100644 --- a/Tusker/Screens/Explore/SuggestedProfilesViewController.swift +++ b/Tusker/Screens/Explore/SuggestedProfilesViewController.swift @@ -11,7 +11,7 @@ import Pachyderm class SuggestedProfilesViewController: UIViewController, CollectionViewController { - weak var mastodonController: MastodonController! + private let mastodonController: MastodonController var collectionView: UICollectionView! private var layout: MultiColumnCollectionViewLayout! diff --git a/Tusker/Screens/Explore/TrendingHashtagsViewController.swift b/Tusker/Screens/Explore/TrendingHashtagsViewController.swift index 0206790e..6de05977 100644 --- a/Tusker/Screens/Explore/TrendingHashtagsViewController.swift +++ b/Tusker/Screens/Explore/TrendingHashtagsViewController.swift @@ -13,7 +13,7 @@ import Combine class TrendingHashtagsViewController: UIViewController { - weak var mastodonController: MastodonController! + private let mastodonController: MastodonController private var collectionView: UICollectionView! private var dataSource: UICollectionViewDiffableDataSource! diff --git a/Tusker/Screens/Explore/TrendingLinksViewController.swift b/Tusker/Screens/Explore/TrendingLinksViewController.swift index 1e07eff6..28fdf38a 100644 --- a/Tusker/Screens/Explore/TrendingLinksViewController.swift +++ b/Tusker/Screens/Explore/TrendingLinksViewController.swift @@ -14,7 +14,7 @@ import Combine class TrendingLinksViewController: UIViewController, CollectionViewController { - weak var mastodonController: MastodonController! + private let mastodonController: MastodonController var collectionView: UICollectionView! private var dataSource: UICollectionViewDiffableDataSource! diff --git a/Tusker/Screens/Explore/TrendingStatusesViewController.swift b/Tusker/Screens/Explore/TrendingStatusesViewController.swift index 177b4d15..6d8900b4 100644 --- a/Tusker/Screens/Explore/TrendingStatusesViewController.swift +++ b/Tusker/Screens/Explore/TrendingStatusesViewController.swift @@ -11,7 +11,7 @@ import Pachyderm class TrendingStatusesViewController: UIViewController, CollectionViewController { - weak var mastodonController: MastodonController! + private let mastodonController: MastodonController let filterer: Filterer var collectionView: UICollectionView! { diff --git a/Tusker/Screens/Large Image/LargeImageViewController.swift b/Tusker/Screens/Large Image/LargeImageViewController.swift index 65299289..c0dfe930 100644 --- a/Tusker/Screens/Large Image/LargeImageViewController.swift +++ b/Tusker/Screens/Large Image/LargeImageViewController.swift @@ -144,7 +144,9 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma contentViewTopConstraint, ]) contentViewSizeObservation = (contentView as UIView).observe(\.bounds, changeHandler: { [unowned self] _, _ in - self.centerImage() + MainActor.runUnsafely { + self.centerImage() + } }) } diff --git a/Tusker/Screens/Main/MainSplitViewController.swift b/Tusker/Screens/Main/MainSplitViewController.swift index 87103800..806bc4a6 100644 --- a/Tusker/Screens/Main/MainSplitViewController.swift +++ b/Tusker/Screens/Main/MainSplitViewController.swift @@ -11,7 +11,7 @@ import Combine class MainSplitViewController: UISplitViewController { - weak var mastodonController: MastodonController! + private let mastodonController: MastodonController private var sidebar: MainSidebarViewController! private var fastAccountSwitcher: FastAccountSwitcherViewController? diff --git a/Tusker/Screens/Main/MainTabBarViewController.swift b/Tusker/Screens/Main/MainTabBarViewController.swift index 8c1b69eb..ba8d26d8 100644 --- a/Tusker/Screens/Main/MainTabBarViewController.swift +++ b/Tusker/Screens/Main/MainTabBarViewController.swift @@ -11,7 +11,7 @@ import ComposeUI class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate { - weak var mastodonController: MastodonController! + private let mastodonController: MastodonController private var composePlaceholder: UIViewController! diff --git a/Tusker/Screens/Notifications/NotificationsCollectionViewController.swift b/Tusker/Screens/Notifications/NotificationsCollectionViewController.swift index 56650da9..62905220 100644 --- a/Tusker/Screens/Notifications/NotificationsCollectionViewController.swift +++ b/Tusker/Screens/Notifications/NotificationsCollectionViewController.swift @@ -15,7 +15,7 @@ import Sentry class NotificationsCollectionViewController: UIViewController, TimelineLikeCollectionViewController, CollectionViewController { - weak var mastodonController: MastodonController! + private let mastodonController: MastodonController private let filterer: Filterer private let allowedTypes: [Pachyderm.Notification.Kind] diff --git a/Tusker/Screens/Onboarding/InstanceSelectorTableViewController.swift b/Tusker/Screens/Onboarding/InstanceSelectorTableViewController.swift index 9b3344bf..17f34f94 100644 --- a/Tusker/Screens/Onboarding/InstanceSelectorTableViewController.swift +++ b/Tusker/Screens/Onboarding/InstanceSelectorTableViewController.swift @@ -27,7 +27,7 @@ class InstanceSelectorTableViewController: UITableViewController { weak var delegate: InstanceSelectorTableViewControllerDelegate? - var dataSource: DataSource! + var dataSource: UITableViewDiffableDataSource! var searchController: UISearchController! var recommendedInstances: [InstanceSelector.Instance] = [] @@ -73,7 +73,7 @@ class InstanceSelectorTableViewController: UITableViewController { tableView.estimatedRowHeight = 120 createActivityIndicatorHeader() - dataSource = DataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in + dataSource = UITableViewDiffableDataSource(tableView: tableView, cellProvider: { (tableView, indexPath, item) -> UITableViewCell? in switch item { case let .selected(_, instance): let cell = tableView.dequeueReusableCell(withIdentifier: instanceCell, for: indexPath) as! InstanceTableViewCell @@ -338,9 +338,6 @@ extension InstanceSelectorTableViewController { } } } - class DataSource: UITableViewDiffableDataSource { - - } } extension InstanceSelectorTableViewController: UISearchResultsUpdating { diff --git a/Tusker/Screens/Profile/ProfileViewController.swift b/Tusker/Screens/Profile/ProfileViewController.swift index 7140478c..26d5c578 100644 --- a/Tusker/Screens/Profile/ProfileViewController.swift +++ b/Tusker/Screens/Profile/ProfileViewController.swift @@ -12,7 +12,7 @@ import Combine class ProfileViewController: UIViewController, StateRestorableViewController { - weak var mastodonController: MastodonController! + let mastodonController: MastodonController // This property is optional because MyProfileViewController may not have the user's account ID // when first constructed. It should never be set to nil. diff --git a/Tusker/Screens/Report/ReportStatusView.swift b/Tusker/Screens/Report/ReportStatusView.swift index 2cad3df8..036ed91c 100644 --- a/Tusker/Screens/Report/ReportStatusView.swift +++ b/Tusker/Screens/Report/ReportStatusView.swift @@ -8,6 +8,7 @@ import SwiftUI +@MainActor private var converter = HTMLConverter( font: .preferredFont(forTextStyle: .body), monospaceFont: UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 17, weight: .regular)), @@ -15,6 +16,7 @@ private var converter = HTMLConverter( paragraphStyle: .default ) +@MainActor struct ReportStatusView: View { let status: StatusMO let mastodonController: MastodonController diff --git a/Tusker/Screens/Search/SearchResultsViewController.swift b/Tusker/Screens/Search/SearchResultsViewController.swift index 883a4dea..6d339d1b 100644 --- a/Tusker/Screens/Search/SearchResultsViewController.swift +++ b/Tusker/Screens/Search/SearchResultsViewController.swift @@ -30,7 +30,7 @@ extension SearchResultsViewControllerDelegate { class SearchResultsViewController: UIViewController, CollectionViewController { - weak var mastodonController: MastodonController! + let mastodonController: MastodonController weak var exploreNavigationController: UINavigationController? weak var delegate: SearchResultsViewControllerDelegate? diff --git a/Tusker/Screens/Timeline/TimelineViewController.swift b/Tusker/Screens/Timeline/TimelineViewController.swift index 07e19567..d0c26d1f 100644 --- a/Tusker/Screens/Timeline/TimelineViewController.swift +++ b/Tusker/Screens/Timeline/TimelineViewController.swift @@ -32,7 +32,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro weak var delegate: TimelineViewControllerDelegate? let timeline: Timeline - weak var mastodonController: MastodonController! + let mastodonController: MastodonController private let filterer: Filterer var persistsState = false @@ -587,7 +587,7 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro snapshot.appendSections([.statuses]) let items = allStatuses.map { Item.status(id: $0.id, collapseState: .unknown, filterState: .unknown) } snapshot.appendItems(items) - await dataSource.apply(snapshot, animatingDifferences: false) + await apply(snapshot, animatingDifferences: false) collectionView.contentOffset = CGPoint(x: 0, y: -collectionView.adjustedContentInset.top) stateRestorationLogger.debug("TimelineViewController: restored from timeline marker with last read ID: \(home.lastReadID)") diff --git a/Tusker/Views/Account Detail/LargeAccountDetailView.swift b/Tusker/Views/Account Detail/LargeAccountDetailView.swift index d3db422b..70ef2ed9 100644 --- a/Tusker/Views/Account Detail/LargeAccountDetailView.swift +++ b/Tusker/Views/Account Detail/LargeAccountDetailView.swift @@ -73,8 +73,8 @@ class LargeAccountDetailView: UIView { if let avatar = account.avatar { avatarRequest = ImageCache.avatars.get(avatar) { [weak self] (_, image) in guard let self = self, let image = image else { return } - self.avatarRequest = nil DispatchQueue.main.async { + self.avatarRequest = nil self.avatarImageView.image = image } } diff --git a/Tusker/Views/CachedImageView.swift b/Tusker/Views/CachedImageView.swift index 1344529f..fe23e4ff 100644 --- a/Tusker/Views/CachedImageView.swift +++ b/Tusker/Views/CachedImageView.swift @@ -90,8 +90,7 @@ class CachedImageView: UIImageView { return } try Task.checkCancellation() - // TODO: check that this isn't on the main thread - guard let transformedImage = ImageGrayscalifier.convertIfNecessary(url: url, image: image) else { + guard let transformedImage = await ImageGrayscalifier.convertIfNecessary(url: url, image: image) else { return } try Task.checkCancellation() diff --git a/Tusker/Views/Instance Cell/InstanceTableViewCell.swift b/Tusker/Views/Instance Cell/InstanceTableViewCell.swift index d2108147..a89a9d03 100644 --- a/Tusker/Views/Instance Cell/InstanceTableViewCell.swift +++ b/Tusker/Views/Instance Cell/InstanceTableViewCell.swift @@ -19,8 +19,11 @@ class InstanceTableViewCell: UITableViewCell { var instance: InstanceV1? var selectorInstance: InstanceSelector.Instance? - var thumbnailURL: URL? - var thumbnailRequest: ImageCache.Request? + private var thumbnailTask: Task? + + deinit { + thumbnailTask?.cancel() + } override func awakeFromNib() { super.awakeFromNib() @@ -68,20 +71,19 @@ class InstanceTableViewCell: UITableViewCell { private func updateThumbnail(url: URL) { thumbnailImageView.image = nil - thumbnailURL = url - thumbnailRequest = ImageCache.attachments.get(url) { [weak self] (_, image) in - guard let self = self, self.thumbnailURL == url, let image = image else { return } - self.thumbnailRequest = nil - DispatchQueue.main.async { - self.thumbnailImageView.image = image + thumbnailTask = Task { + guard let image = await ImageCache.attachments.get(url).1, + !Task.isCancelled else { + return } + thumbnailImageView.image = image } } override func prepareForReuse() { super.prepareForReuse() - thumbnailRequest?.cancel() + thumbnailTask?.cancel() instance = nil selectorInstance = nil } diff --git a/Tusker/Views/MultiSourceEmojiLabel.swift b/Tusker/Views/MultiSourceEmojiLabel.swift index e6978801..d8d5578e 100644 --- a/Tusker/Views/MultiSourceEmojiLabel.swift +++ b/Tusker/Views/MultiSourceEmojiLabel.swift @@ -24,7 +24,7 @@ class MultiSourceEmojiLabel: UILabel, BaseEmojiLabel { var attributedStrings = pairs.map { NSAttributedString(string: $0.0) } - let recombine = { [weak self] in + let recombine: @MainActor @Sendable () -> Void = { [weak self] in if let self, let combiner = self.combiner { self.attributedText = combiner(attributedStrings) diff --git a/Tusker/Views/Profile Header/ProfileFieldsView.swift b/Tusker/Views/Profile Header/ProfileFieldsView.swift index bc841274..8590a34b 100644 --- a/Tusker/Views/Profile Header/ProfileFieldsView.swift +++ b/Tusker/Views/Profile Header/ProfileFieldsView.swift @@ -53,7 +53,9 @@ class ProfileFieldsView: UIView { private func commonInit() { boundsObservation = observe(\.bounds, changeHandler: { [unowned self] _, _ in - self.setNeedsUpdateConstraints() + MainActor.runUnsafely { + self.setNeedsUpdateConstraints() + } }) #if os(visionOS) diff --git a/Tusker/Views/Profile Header/ProfileHeaderView.swift b/Tusker/Views/Profile Header/ProfileHeaderView.swift index c099defd..731f4787 100644 --- a/Tusker/Views/Profile Header/ProfileHeaderView.swift +++ b/Tusker/Views/Profile Header/ProfileHeaderView.swift @@ -42,8 +42,7 @@ class ProfileHeaderView: UIView { var accountID: String! - private var avatarRequest: ImageCache.Request? - private var headerRequest: ImageCache.Request? + private var imagesTask: Task? private var isGrayscale = false private var followButtonMode = FollowButtonMode.follow { @@ -56,8 +55,7 @@ class ProfileHeaderView: UIView { } deinit { - avatarRequest?.cancel() - headerRequest?.cancel() + imagesTask?.cancel() } override func awakeFromNib() { @@ -134,7 +132,12 @@ class ProfileHeaderView: UIView { usernameLabel.text = "@\(account.acct)" lockImageView.isHidden = !account.locked - updateImages(account: account) + imagesTask?.cancel() + let avatar = account.avatar + let header = account.header + imagesTask = Task { + await updateImages(avatar: avatar, header: header) + } moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: delegate?.actionsForProfile(accountID: accountID, source: .view(moreButton), fetchRelationship: false) ?? []) @@ -286,50 +289,41 @@ class ProfileHeaderView: UIView { displayNameLabel.updateForAccountDisplayName(account: account) if isGrayscale != Preferences.shared.grayscaleImages { - updateImages(account: account) + isGrayscale = Preferences.shared.grayscaleImages + imagesTask?.cancel() + let avatar = account.avatar + let header = account.header + imagesTask = Task { + await updateImages(avatar: avatar, header: header) + } } } - private func updateImages(account: AccountMO) { - isGrayscale = Preferences.shared.grayscaleImages - - let accountID = account.id - if let avatarURL = account.avatar { - // always load original for avatars, because ImageCache.avatars stores them scaled-down in memory - avatarRequest = ImageCache.avatars.get(avatarURL, loadOriginal: true) { [weak self] (_, image) in - guard let self = self, - let image = image, - self.accountID == accountID, - let transformedImage = ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) else { - DispatchQueue.main.async { - self?.avatarRequest = nil - } + private nonisolated func updateImages(avatar: URL?, header: URL?) async { + await withTaskGroup(of: Void.self) { group in + group.addTask { + guard let avatar, + let image = await ImageCache.avatars.get(avatar, loadOriginal: true).1, + let transformedImage = await ImageGrayscalifier.convertIfNecessary(url: avatar, image: image), + !Task.isCancelled else { return } - - DispatchQueue.main.async { - self.avatarRequest = nil + await MainActor.run { self.avatarImageView.image = transformedImage } } - } - if let header = account.header { - headerRequest = ImageCache.headers.get(header) { [weak self] (_, image) in - guard let self = self, - let image = image, - self.accountID == accountID, - let transformedImage = ImageGrayscalifier.convertIfNecessary(url: header, image: image) else { - DispatchQueue.main.async { - self?.headerRequest = nil - } + group.addTask { + guard let header, + let image = await ImageCache.avatars.get(header, loadOriginal: true).1, + let transformedImage = await ImageGrayscalifier.convertIfNecessary(url: header, image: image), + !Task.isCancelled else { return } - - DispatchQueue.main.async { - self.headerRequest = nil + await MainActor.run { self.headerImageView.image = transformedImage } } + await group.waitForAll() } }