Even more strict concurrency fixes

This commit is contained in:
Shadowfacts 2024-01-27 15:48:58 -05:00
parent fc26c9fb54
commit 27d44340e8
24 changed files with 179 additions and 119 deletions

View File

@ -7,7 +7,7 @@
import Foundation import Foundation
public struct StatusSource: Decodable { public struct StatusSource: Decodable, Sendable {
public let id: String public let id: String
public let text: String public let text: String
public let spoilerText: String public let spoilerText: String

View File

@ -49,7 +49,7 @@ public class InstanceSelector {
} }
public extension InstanceSelector { public extension InstanceSelector {
struct Instance: Codable { struct Instance: Codable, Sendable {
public let domain: String public let domain: String
public let description: String public let description: String
public let proxiedThumbnailURL: URL public let proxiedThumbnailURL: URL

View File

@ -51,6 +51,7 @@ class MastodonController: ObservableObject {
let instanceURL: URL let instanceURL: URL
let accountInfo: UserAccountInfo? let accountInfo: UserAccountInfo?
@MainActor
private(set) var accountPreferences: AccountPreferences! private(set) var accountPreferences: AccountPreferences!
private(set) var client: Client! private(set) var client: Client!
@ -271,17 +272,6 @@ class MastodonController: ObservableObject {
} }
} }
func getOwnAccount(completion: (@Sendable (Result<AccountMO, any Error>) -> Void)? = nil) {
Task.detached {
do {
let account = try await self.getOwnAccount()
completion?(.success(account))
} catch {
completion?(.failure(error))
}
}
}
@MainActor @MainActor
func getOwnAccount() async throws -> AccountMO { func getOwnAccount() async throws -> AccountMO {
if let account { if let account {
@ -307,15 +297,8 @@ class MastodonController: ObservableObject {
return MainThreadBox(value: accountMO) return MainThreadBox(value: accountMO)
} }
} }
} fetchOwnAccountTask = task
if let account = account { return try await task.value.value
return account
} else {
return try await withCheckedThrowingContinuation({ continuation in
self.getOwnAccount { result in
continuation.resume(with: result)
}
})
} }
} }

View File

@ -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 { DispatchQueue.main.async {
if changedHashtags { if hashtags {
NotificationCenter.default.post(name: .savedHashtagsChanged, object: nil) NotificationCenter.default.post(name: .savedHashtagsChanged, object: nil)
} }
if changedInstances { if instances {
NotificationCenter.default.post(name: .savedInstancesChanged, object: nil) 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 { guard let timelinePosition = try? self.viewContext.existingObject(with: id) as? TimelinePosition else {
continue continue
} }
@ -565,7 +570,7 @@ class MastodonCachePersistentStore: NSPersistentCloudKitContainer {
timelinePosition.changedRemotely() timelinePosition.changedRemotely()
NotificationCenter.default.post(name: .timelinePositionChanged, object: timelinePosition) NotificationCenter.default.post(name: .timelinePositionChanged, object: timelinePosition)
} }
if changedAccountPrefs { if accountPrefs {
NotificationCenter.default.post(name: .accountPreferencesChangedRemotely, object: nil) NotificationCenter.default.post(name: .accountPreferencesChangedRemotely, object: nil)
} }
} }

View File

@ -30,6 +30,7 @@ extension StatusSwipeAction {
} }
} }
@MainActor
protocol StatusSwipeActionContainer: UIView { protocol StatusSwipeActionContainer: UIView {
var mastodonController: MastodonController! { get } var mastodonController: MastodonController! { get }
var navigationDelegate: any TuskerNavigationDelegate { get } var navigationDelegate: any TuskerNavigationDelegate { get }

View File

@ -296,7 +296,7 @@ extension ConversationCollectionViewController {
case mainStatus case mainStatus
case childThread(firstStatusID: String) case childThread(firstStatusID: String)
} }
enum Item: Hashable { enum Item: Hashable, Sendable {
case status(id: String, node: ConversationNode, state: CollapseState, prevLink: Bool, nextLink: Bool) case status(id: String, node: ConversationNode, state: CollapseState, prevLink: Bool, nextLink: Bool)
case expandThread(childThreads: [ConversationNode], inline: Bool) case expandThread(childThreads: [ConversationNode], inline: Bool)
case loadingIndicator 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)): 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 return a == b && aPrev == bPrev && aNext == bNext
case let (.expandThread(childThreads: a, inline: aInline), .expandThread(childThreads: b, inline: bInline)): 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): case (.loadingIndicator, .loadingIndicator):
return true return true
default: default:
@ -324,7 +324,7 @@ extension ConversationCollectionViewController {
case .expandThread(childThreads: let childThreads, inline: let inline): case .expandThread(childThreads: let childThreads, inline: let inline):
hasher.combine(1) hasher.combine(1)
for thread in childThreads { for thread in childThreads {
hasher.combine(thread.status.id) hasher.combine(thread.statusID)
} }
hasher.combine(inline) hasher.combine(inline)
case .loadingIndicator: case .loadingIndicator:

View File

@ -9,15 +9,20 @@
import Foundation import Foundation
import Pachyderm import Pachyderm
@MainActor
class ConversationNode { class ConversationNode {
let statusID: String
let status: StatusMO let status: StatusMO
var children: [ConversationNode] var children: [ConversationNode]
init(status: StatusMO) { init(status: StatusMO) {
self.statusID = status.id
self.status = status self.status = status
self.children = [] self.children = []
} }
} }
@MainActor
struct ConversationTree { struct ConversationTree {
let ancestors: [ConversationNode] let ancestors: [ConversationNode]
let mainStatus: ConversationNode let mainStatus: ConversationNode

View File

@ -194,7 +194,7 @@ class ConversationViewController: UIViewController {
if let cached = mastodonController.persistentContainer.status(for: mainStatusID) { if let cached = mastodonController.persistentContainer.status(for: mainStatusID) {
// if we have a cached copy, display it immediately but still try to refresh it // if we have a cached copy, display it immediately but still try to refresh it
Task { Task {
await doLoadMainStatus() _ = await doLoadMainStatus()
} }
mainStatusLoaded(cached) mainStatusLoaded(cached)
} else { } else {

View File

@ -20,8 +20,11 @@ class FeaturedProfileCollectionViewCell: UICollectionViewCell {
var account: Account? var account: Account?
private var avatarRequest: ImageCache.Request? private var accountImagesTask: Task<Void, Never>?
private var headerRequest: ImageCache.Request?
deinit {
accountImagesTask?.cancel()
}
override func awakeFromNib() { override func awakeFromNib() {
super.awakeFromNib() super.awakeFromNib()
@ -62,37 +65,35 @@ class FeaturedProfileCollectionViewCell: UICollectionViewCell {
noteTextView.setEmojis(account.emojis, identifier: account.id) noteTextView.setEmojis(account.emojis, identifier: account.id)
avatarImageView.image = nil avatarImageView.image = nil
if let avatar = account.avatar { headerImageView.image = nil
avatarRequest = ImageCache.avatars.get(avatar) { [weak self] (_, image) in
defer { accountImagesTask?.cancel()
self?.avatarRequest = nil accountImagesTask = Task {
} await updateImages(account: account)
guard let self = self, }
let image = image, }
self.account?.id == account.id else {
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 return
} }
DispatchQueue.main.async { await MainActor.run {
self.avatarImageView.image = image self.avatarImageView.image = image
} }
} }
} group.addTask {
guard let header = account.header,
headerImageView.image = nil let image = await ImageCache.headers.get(header).1 else {
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 {
return return
} }
DispatchQueue.main.async { await MainActor.run {
self.headerImageView.image = image self.headerImageView.image = image
} }
} }
await group.waitForAll()
} }
} }

View File

@ -95,7 +95,9 @@ class SuggestedProfilesViewController: UIViewController, CollectionViewControlle
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>() var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.accounts]) snapshot.appendSections([.accounts])
await dataSource.apply(snapshot) await MainActor.run {
dataSource.apply(snapshot)
}
do { do {
let request = Client.getSuggestions(limit: 80) let request = Client.getSuggestions(limit: 80)
@ -108,7 +110,9 @@ class SuggestedProfilesViewController: UIViewController, CollectionViewControlle
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>() var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.accounts]) snapshot.appendSections([.accounts])
snapshot.appendItems(suggestions.map { .account($0.account.id, $0.source) }) snapshot.appendItems(suggestions.map { .account($0.account.id, $0.source) })
await dataSource.apply(snapshot) await MainActor.run {
dataSource.apply(snapshot)
}
state = .loaded state = .loaded
} catch { } catch {

View File

@ -107,7 +107,9 @@ class TrendingHashtagsViewController: UIViewController {
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>() var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.trendingTags]) snapshot.appendSections([.trendingTags])
snapshot.appendItems([.loadingIndicator]) snapshot.appendItems([.loadingIndicator])
await dataSource.apply(snapshot) await MainActor.run {
dataSource.apply(snapshot)
}
do { do {
let request = self.request(offset: nil) let request = self.request(offset: nil)
@ -115,10 +117,14 @@ class TrendingHashtagsViewController: UIViewController {
snapshot.deleteItems([.loadingIndicator]) snapshot.deleteItems([.loadingIndicator])
snapshot.appendItems(hashtags.map { .tag($0) }) snapshot.appendItems(hashtags.map { .tag($0) })
state = .loaded state = .loaded
await dataSource.apply(snapshot) await MainActor.run {
dataSource.apply(snapshot)
}
} catch { } catch {
snapshot.deleteItems([.loadingIndicator]) snapshot.deleteItems([.loadingIndicator])
await dataSource.apply(snapshot) await MainActor.run {
dataSource.apply(snapshot)
}
state = .unloaded state = .unloaded
let config = ToastConfiguration(from: error, with: "Error Loading Trending Tags", in: self) { [weak self] toast in 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 var snapshot = origSnapshot
if Preferences.shared.disableInfiniteScrolling { if Preferences.shared.disableInfiniteScrolling {
snapshot.appendItems([.confirmLoadMore(false)]) snapshot.appendItems([.confirmLoadMore(false)])
await dataSource.apply(snapshot) await MainActor.run {
dataSource.apply(snapshot)
}
for await _ in confirmLoadMore.values { for await _ in confirmLoadMore.values {
break break
@ -148,10 +156,14 @@ class TrendingHashtagsViewController: UIViewController {
snapshot.deleteItems([.confirmLoadMore(false)]) snapshot.deleteItems([.confirmLoadMore(false)])
snapshot.appendItems([.confirmLoadMore(true)]) snapshot.appendItems([.confirmLoadMore(true)])
await dataSource.apply(snapshot, animatingDifferences: false) await MainActor.run {
dataSource.apply(snapshot, animatingDifferences: false)
}
} else { } else {
snapshot.appendItems([.loadingIndicator]) snapshot.appendItems([.loadingIndicator])
await dataSource.apply(snapshot) await MainActor.run {
dataSource.apply(snapshot)
}
} }
do { do {
@ -159,9 +171,13 @@ class TrendingHashtagsViewController: UIViewController {
let (hashtags, _) = try await mastodonController.run(request) let (hashtags, _) = try await mastodonController.run(request)
var snapshot = origSnapshot var snapshot = origSnapshot
snapshot.appendItems(hashtags.map { .tag($0) }) snapshot.appendItems(hashtags.map { .tag($0) })
await dataSource.apply(snapshot) await MainActor.run {
dataSource.apply(snapshot)
}
} catch { } 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 let config = ToastConfiguration(from: error, with: "Error Loading More Tags", in: self) { [weak self] toast in
toast.dismissToast(animated: true) toast.dismissToast(animated: true)

View File

@ -128,7 +128,9 @@ class TrendingLinksViewController: UIViewController, CollectionViewController {
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>() var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.loadingIndicator]) snapshot.appendSections([.loadingIndicator])
snapshot.appendItems([.loadingIndicator]) snapshot.appendItems([.loadingIndicator])
await dataSource.apply(snapshot) await MainActor.run {
dataSource.apply(snapshot)
}
do { do {
let request = Client.getTrendingLinks() let request = Client.getTrendingLinks()
@ -137,9 +139,13 @@ class TrendingLinksViewController: UIViewController, CollectionViewController {
snapshot.appendSections([.links]) snapshot.appendSections([.links])
snapshot.appendItems(links.map { .link($0) }) snapshot.appendItems(links.map { .link($0) })
state = .loaded state = .loaded
await dataSource.apply(snapshot) await MainActor.run {
dataSource.apply(snapshot)
}
} catch { } catch {
await dataSource.apply(NSDiffableDataSourceSnapshot()) await MainActor.run {
dataSource.apply(NSDiffableDataSourceSnapshot())
}
state = .unloaded state = .unloaded
let config = ToastConfiguration(from: error, with: "Error Loading Trending Links", in: self) { [weak self] toast in let config = ToastConfiguration(from: error, with: "Error Loading Trending Links", in: self) { [weak self] toast in
toast.dismissToast(animated: true) toast.dismissToast(animated: true)
@ -161,7 +167,9 @@ class TrendingLinksViewController: UIViewController, CollectionViewController {
if Preferences.shared.disableInfiniteScrolling { if Preferences.shared.disableInfiniteScrolling {
snapshot.appendSections([.loadingIndicator]) snapshot.appendSections([.loadingIndicator])
snapshot.appendItems([.confirmLoadMore(false)], toSection: .loadingIndicator) snapshot.appendItems([.confirmLoadMore(false)], toSection: .loadingIndicator)
await dataSource.apply(snapshot) await MainActor.run {
dataSource.apply(snapshot)
}
for await _ in confirmLoadMore.values { for await _ in confirmLoadMore.values {
break break
@ -169,11 +177,15 @@ class TrendingLinksViewController: UIViewController, CollectionViewController {
snapshot.deleteItems([.confirmLoadMore(false)]) snapshot.deleteItems([.confirmLoadMore(false)])
snapshot.appendItems([.confirmLoadMore(true)], toSection: .loadingIndicator) snapshot.appendItems([.confirmLoadMore(true)], toSection: .loadingIndicator)
await dataSource.apply(snapshot, animatingDifferences: false) await MainActor.run {
dataSource.apply(snapshot, animatingDifferences: false)
}
} else { } else {
snapshot.appendSections([.loadingIndicator]) snapshot.appendSections([.loadingIndicator])
snapshot.appendItems([.loadingIndicator], toSection: .loadingIndicator) snapshot.appendItems([.loadingIndicator], toSection: .loadingIndicator)
await dataSource.apply(snapshot) await MainActor.run {
dataSource.apply(snapshot)
}
} }
do { do {
@ -181,9 +193,13 @@ class TrendingLinksViewController: UIViewController, CollectionViewController {
let (links, _) = try await mastodonController.run(request) let (links, _) = try await mastodonController.run(request)
var snapshot = origSnapshot var snapshot = origSnapshot
snapshot.appendItems(links.map { .link($0) }, toSection: .links) snapshot.appendItems(links.map { .link($0) }, toSection: .links)
await dataSource.apply(snapshot) await MainActor.run {
dataSource.apply(snapshot)
}
} catch { } 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 let config = ToastConfiguration(from: error, with: "Erorr Loading More Links", in: self) { [weak self] toast in
toast.dismissToast(animated: true) toast.dismissToast(animated: true)
await self?.loadOlder() await self?.loadOlder()

View File

@ -126,7 +126,9 @@ class TrendingStatusesViewController: UIViewController, CollectionViewController
statuses = try await mastodonController.run(Client.getTrendingStatuses()).0 statuses = try await mastodonController.run(Client.getTrendingStatuses()).0
} catch { } catch {
let snapshot = NSDiffableDataSourceSnapshot<Section, Item>() let snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
await dataSource.apply(snapshot) await MainActor.run {
dataSource.apply(snapshot)
}
let config = ToastConfiguration(from: error, with: "Loading Trending Posts", in: self) { toast in let config = ToastConfiguration(from: error, with: "Loading Trending Posts", in: self) { toast in
toast.dismissToast(animated: true) toast.dismissToast(animated: true)
await self.loadTrendingStatuses() await self.loadTrendingStatuses()
@ -138,7 +140,9 @@ class TrendingStatusesViewController: UIViewController, CollectionViewController
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>() var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.statuses]) snapshot.appendSections([.statuses])
snapshot.appendItems(statuses.map { .status(id: $0.id, collapseState: .unknown, filterState: .unknown) }) 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) { @objc private func handleStatusDeleted(_ notification: Foundation.Notification) {

View File

@ -238,7 +238,7 @@ class TrendsViewController: UIViewController, CollectionViewController {
} }
isShowingTrends = shouldShowTrends isShowingTrends = shouldShowTrends
guard shouldShowTrends else { guard shouldShowTrends else {
await dataSource.apply(NSDiffableDataSourceSnapshot()) await apply(snapshot: NSDiffableDataSourceSnapshot())
return return
} }
@ -355,9 +355,9 @@ class TrendsViewController: UIViewController, CollectionViewController {
} }
private func apply(snapshot: NSDiffableDataSourceSnapshot<Section, Item>, animatingDifferences: Bool = true) async { private func apply(snapshot: NSDiffableDataSourceSnapshot<Section, Item>, animatingDifferences: Bool = true) async {
await Task { @MainActor in await MainActor.run {
self.dataSource.apply(snapshot, animatingDifferences: animatingDifferences) self.dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
}.value }
} }
@MainActor @MainActor

View File

@ -49,7 +49,7 @@ class FastSwitchingAccountView: UIView {
private let instanceLabel = UILabel() private let instanceLabel = UILabel()
private let avatarImageView = UIImageView() private let avatarImageView = UIImageView()
private var avatarRequest: ImageCache.Request? private var avatarTask: Task<Void, Never>?
init(account: UserAccountInfo, orientation: FastAccountSwitcherViewController.ItemOrientation) { init(account: UserAccountInfo, orientation: FastAccountSwitcherViewController.ItemOrientation) {
self.orientation = orientation self.orientation = orientation
@ -69,6 +69,10 @@ class FastSwitchingAccountView: UIView {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
deinit {
avatarTask?.cancel()
}
private func commonInit() { private func commonInit() {
usernameLabel.textColor = .white usernameLabel.textColor = .white
usernameLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .headline), size: 0) usernameLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .headline), size: 0)
@ -133,16 +137,13 @@ class FastSwitchingAccountView: UIView {
instanceLabel.text = account.instanceURL.host! instanceLabel.text = account.instanceURL.host!
} }
let controller = MastodonController.getForAccount(account) let controller = MastodonController.getForAccount(account)
controller.getOwnAccount { [weak self] (result) in avatarTask = Task {
guard let self = self, guard let account = try? await controller.getOwnAccount(),
case let .success(account) = result, let avatar = account.avatar,
let avatar = account.avatar else { return } let image = await ImageCache.avatars.get(avatar).1 else {
self.avatarRequest = ImageCache.avatars.get(avatar) { [weak self] (_, image) in return
guard let self = self, let image = image else { return }
DispatchQueue.main.async {
self.avatarImageView.image = image
}
} }
self.avatarImageView.image = image
} }
accessibilityLabel = "\(account.username!)@\(instanceLabel.text!)" accessibilityLabel = "\(account.username!)@\(instanceLabel.text!)"

View File

@ -105,8 +105,8 @@ class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableVie
embedChild(loadingVC!) embedChild(loadingVC!)
imageRequest = cache.get(url, loadOriginal: true) { [weak self] (data, image) in imageRequest = cache.get(url, loadOriginal: true) { [weak self] (data, image) in
guard let self = self, let image = image else { return } guard let self = self, let image = image else { return }
self.imageRequest = nil
DispatchQueue.main.async { DispatchQueue.main.async {
self.imageRequest = nil
self.loadingVC?.removeViewAndController() self.loadingVC?.removeViewAndController()
self.createLargeImage(data: data, image: image, url: self.url) self.createLargeImage(data: data, image: image, url: self.url)
} }

View File

@ -195,7 +195,9 @@ class EditListAccountsViewController: UIViewController, CollectionViewController
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>() var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.accounts]) snapshot.appendSections([.accounts])
snapshot.appendItems([.loadingIndicator]) snapshot.appendItems([.loadingIndicator])
await dataSource.apply(snapshot) await MainActor.run {
dataSource.apply(snapshot)
}
do { do {
let (accounts, pagination) = try await results let (accounts, pagination) = try await results
@ -210,7 +212,9 @@ class EditListAccountsViewController: UIViewController, CollectionViewController
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>() var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.accounts]) snapshot.appendSections([.accounts])
snapshot.appendItems(accounts.map { .account(id: $0.id) }) snapshot.appendItems(accounts.map { .account(id: $0.id) })
await dataSource.apply(snapshot) await MainActor.run {
dataSource.apply(snapshot)
}
state = .loaded state = .loaded
} catch { } catch {
@ -221,7 +225,9 @@ class EditListAccountsViewController: UIViewController, CollectionViewController
self.showToast(configuration: config, animated: true) self.showToast(configuration: config, animated: true)
state = .unloaded state = .unloaded
await dataSource.apply(.init()) await MainActor.run {
dataSource.apply(.init())
}
} }
} }
@ -236,7 +242,9 @@ class EditListAccountsViewController: UIViewController, CollectionViewController
let origSnapshot = dataSource.snapshot() let origSnapshot = dataSource.snapshot()
var snapshot = origSnapshot var snapshot = origSnapshot
snapshot.appendItems([.loadingIndicator]) snapshot.appendItems([.loadingIndicator])
await dataSource.apply(snapshot) await MainActor.run {
dataSource.apply(snapshot)
}
do { do {
let (accounts, pagination) = try await results let (accounts, pagination) = try await results
@ -250,7 +258,9 @@ class EditListAccountsViewController: UIViewController, CollectionViewController
var snapshot = origSnapshot var snapshot = origSnapshot
snapshot.appendItems(accounts.map { .account(id: $0.id) }) snapshot.appendItems(accounts.map { .account(id: $0.id) })
await dataSource.apply(snapshot) await MainActor.run {
dataSource.apply(snapshot)
}
state = .loaded state = .loaded
} catch { } catch {
@ -261,7 +271,9 @@ class EditListAccountsViewController: UIViewController, CollectionViewController
self.showToast(configuration: config, animated: true) self.showToast(configuration: config, animated: true)
state = .loaded state = .loaded
await dataSource.apply(origSnapshot) await MainActor.run {
dataSource.apply(origSnapshot)
}
} }
} }

View File

@ -273,8 +273,11 @@ class NotificationsCollectionViewController: UIViewController, TimelineLikeColle
} }
} }
private func dismissNotificationsInGroup(at indexPath: IndexPath) async { private nonisolated func dismissNotificationsInGroup(at indexPath: IndexPath) async {
guard case .group(let group, let collapseState, let filterState) = dataSource.itemIdentifier(for: indexPath) else { let item = await MainActor.run {
dataSource.itemIdentifier(for: indexPath)
}
guard case .group(let group, let collapseState, let filterState) = item else {
return return
} }
let notifications = group.notifications 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 { if dismissFailedIndices.isEmpty {
snapshot.deleteItems([.group(group, collapseState, filterState)]) snapshot.deleteItems([.group(group, collapseState, filterState)])
} else if !dismissFailedIndices.isEmpty && dismissFailedIndices.count == notifications.count { } else if !dismissFailedIndices.isEmpty && dismissFailedIndices.count == notifications.count {

View File

@ -311,7 +311,7 @@ extension InstanceSelectorTableViewController {
case selected case selected
case recommendedInstances case recommendedInstances
} }
enum Item: Equatable, Hashable { enum Item: Equatable, Hashable, Sendable {
case selected(URL, InstanceV1) case selected(URL, InstanceV1)
case recommended(InstanceSelector.Instance) case recommended(InstanceSelector.Instance)

View File

@ -32,20 +32,19 @@ struct LocalAccountAvatarView: View {
.resizable() .resizable()
.frame(width: 30, height: 30) .frame(width: 30, height: 30)
.cornerRadius(preferences.avatarStyle.cornerRadiusFraction * 30) .cornerRadius(preferences.avatarStyle.cornerRadiusFraction * 30)
.onAppear(perform: self.loadImage) .task {
await self.loadImage()
}
} }
func loadImage() { func loadImage() async {
let controller = MastodonController.getForAccount(localAccountInfo) let controller = MastodonController.getForAccount(localAccountInfo)
controller.getOwnAccount { (result) in guard let account = try? await controller.getOwnAccount(),
guard case let .success(account) = result, let avatar = account.avatar,
let avatar = account.avatar else { return } let image = await ImageCache.avatars.get(avatar).1 else {
_ = ImageCache.avatars.get(avatar) { (_, image) in return
DispatchQueue.main.async {
self.avatarImage = image
}
}
} }
self.avatarImage = image
} }
} }

View File

@ -18,13 +18,12 @@ class MyProfileViewController: ProfileViewController {
title = "My Profile" title = "My Profile"
tabBarItem.image = UIImage(systemName: "person.fill") tabBarItem.image = UIImage(systemName: "person.fill")
mastodonController.getOwnAccount { (result) in Task {
guard case let .success(account) = result else { return } guard let account = try? await mastodonController.getOwnAccount() else {
return
DispatchQueue.main.async {
self.accountID = account.id
self.setAvatarTabBarImage(account: account)
} }
self.accountID = account.id
self.setAvatarTabBarImage(account: account)
} }
} }

View File

@ -18,12 +18,14 @@ protocol MenuActionProvider: AnyObject {
var toastableViewController: ToastableViewController? { get } var toastableViewController: ToastableViewController? { get }
} }
@MainActor
protocol MenuPreviewProvider: AnyObject { protocol MenuPreviewProvider: AnyObject {
typealias PreviewProviders = (content: UIContextMenuContentPreviewProvider, actions: () -> [UIMenuElement]) typealias PreviewProviders = (content: UIContextMenuContentPreviewProvider, actions: () -> [UIMenuElement])
func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders? func getPreviewProviders(for location: CGPoint, sourceViewController: UIViewController) -> PreviewProviders?
} }
@MainActor
protocol CustomPreviewPresenting { protocol CustomPreviewPresenting {
func presentFromPreview(presenter: UIViewController) func presentFromPreview(presenter: UIViewController)
} }
@ -478,7 +480,7 @@ extension MenuActionProvider {
await fetchRelationship(accountID: accountID, mastodonController: mastodonController) await fetchRelationship(accountID: accountID, mastodonController: mastodonController)
} }
Task { @MainActor in Task { @MainActor in
if let relationship = await relationship.value, if let relationship = await relationship.value?.value,
let action = builder(relationship, mastodonController) { let action = builder(relationship, mastodonController) {
elementHandler([action]) elementHandler([action])
} else { } 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<RelationshipMO>? {
let req = Client.getRelationships(accounts: [accountID]) let req = Client.getRelationships(accounts: [accountID])
guard let (relationships, _) = try? await mastodonController.run(req), guard let (relationships, _) = try? await mastodonController.run(req),
let r = relationships.first else { let r = relationships.first else {
return nil return nil
} }
return await withCheckedContinuation { continuation in return await withCheckedContinuation { continuation in
mastodonController.persistentContainer.addOrUpdate(relationship: r, in: mastodonController.persistentContainer.viewContext) { mo in DispatchQueue.main.async {
continuation.resume(returning: mo) mastodonController.persistentContainer.addOrUpdate(relationship: r, in: mastodonController.persistentContainer.viewContext) { mo in
continuation.resume(returning: MainThreadBox(value: mo))
}
} }
} }
} }
struct MenuPreviewHelper { struct MenuPreviewHelper {
private init() {}
@MainActor
static func willPerformPreviewAction(animator: UIContextMenuInteractionCommitAnimating, presenter: UIViewController) { static func willPerformPreviewAction(animator: UIContextMenuInteractionCommitAnimating, presenter: UIViewController) {
if let viewController = animator.previewViewController { if let viewController = animator.previewViewController {
animator.preferredCommitStyle = .pop animator.preferredCommitStyle = .pop

View File

@ -54,6 +54,7 @@ enum AppShortcutItem: String, CaseIterable {
} }
extension AppShortcutItem { extension AppShortcutItem {
@MainActor
static func createItems(for application: UIApplication) { static func createItems(for application: UIApplication) {
application.shortcutItems = allCases.map { application.shortcutItems = allCases.map {
return UIApplicationShortcutItem(type: $0.rawValue, localizedTitle: $0.title, localizedSubtitle: nil, icon: $0.icon, userInfo: nil) return UIApplicationShortcutItem(type: $0.rawValue, localizedTitle: $0.title, localizedSubtitle: nil, icon: $0.icon, userInfo: nil)

View File

@ -10,6 +10,7 @@ import Foundation
import OSLog import OSLog
import Combine import Combine
@MainActor
protocol TimelineLikeControllerDelegate<TimelineItem>: AnyObject { protocol TimelineLikeControllerDelegate<TimelineItem>: AnyObject {
associatedtype TimelineItem: Sendable associatedtype TimelineItem: Sendable
@ -217,7 +218,7 @@ class TimelineLikeController<Item: Sendable> {
} }
} }
enum State: Equatable, CustomDebugStringConvertible { enum State: Equatable, CustomDebugStringConvertible, Sendable {
case notLoadedInitial case notLoadedInitial
case idle case idle
case restoringInitial(LoadAttemptToken, hasAddedLoadingIndicator: Bool) case restoringInitial(LoadAttemptToken, hasAddedLoadingIndicator: Bool)
@ -360,7 +361,7 @@ class TimelineLikeController<Item: Sendable> {
} }
} }
class LoadAttemptToken: Equatable { final class LoadAttemptToken: Equatable, Sendable {
static func ==(lhs: LoadAttemptToken, rhs: LoadAttemptToken) -> Bool { static func ==(lhs: LoadAttemptToken, rhs: LoadAttemptToken) -> Bool {
return lhs === rhs return lhs === rhs
} }