forked from shadowfacts/Tusker
Another round of strict concurrency fixes
This commit is contained in:
parent
27d44340e8
commit
b235f0e826
|
@ -18,19 +18,23 @@ class ActionViewController: UIViewController {
|
|||
super.viewDidLoad()
|
||||
|
||||
findURLFromWebPage { (components) in
|
||||
if let components = components {
|
||||
DispatchQueue.main.async {
|
||||
if let components {
|
||||
self.searchForURLInApp(components)
|
||||
} else {
|
||||
self.findURLItem { (components) in
|
||||
if let components = components {
|
||||
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 {
|
||||
|
|
|
@ -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<Void, Never>()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<AccountMO> = {
|
||||
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<AccountMO>).self))
|
||||
}
|
||||
fetchOwnAccountTask = task
|
||||
return try await task.value.value
|
||||
|
@ -342,7 +345,7 @@ class MastodonController: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
private func retrying<T: Sendable>(_ label: StaticString, action: () async throws -> T) async throws -> T {
|
||||
private func retrying<T: Sendable>(_ 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(
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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<Section, Item>!
|
||||
|
|
|
@ -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<Section, Item>!
|
||||
|
|
|
@ -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!
|
||||
|
|
|
@ -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<Section, Item>!
|
||||
|
|
|
@ -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<Section, Item>!
|
||||
|
|
|
@ -11,7 +11,7 @@ import Pachyderm
|
|||
|
||||
class TrendingStatusesViewController: UIViewController, CollectionViewController {
|
||||
|
||||
weak var mastodonController: MastodonController!
|
||||
private let mastodonController: MastodonController
|
||||
let filterer: Filterer
|
||||
|
||||
var collectionView: UICollectionView! {
|
||||
|
|
|
@ -144,7 +144,9 @@ class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeIma
|
|||
contentViewTopConstraint,
|
||||
])
|
||||
contentViewSizeObservation = (contentView as UIView).observe(\.bounds, changeHandler: { [unowned self] _, _ in
|
||||
MainActor.runUnsafely {
|
||||
self.centerImage()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -11,7 +11,7 @@ import ComposeUI
|
|||
|
||||
class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
||||
|
||||
weak var mastodonController: MastodonController!
|
||||
private let mastodonController: MastodonController
|
||||
|
||||
private var composePlaceholder: UIViewController!
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -27,7 +27,7 @@ class InstanceSelectorTableViewController: UITableViewController {
|
|||
|
||||
weak var delegate: InstanceSelectorTableViewControllerDelegate?
|
||||
|
||||
var dataSource: DataSource!
|
||||
var dataSource: UITableViewDiffableDataSource<Section, Item>!
|
||||
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<Section, Item> {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
extension InstanceSelectorTableViewController: UISearchResultsUpdating {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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)")
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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<Void, Never>?
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -53,7 +53,9 @@ class ProfileFieldsView: UIView {
|
|||
|
||||
private func commonInit() {
|
||||
boundsObservation = observe(\.bounds, changeHandler: { [unowned self] _, _ in
|
||||
MainActor.runUnsafely {
|
||||
self.setNeedsUpdateConstraints()
|
||||
}
|
||||
})
|
||||
|
||||
#if os(visionOS)
|
||||
|
|
|
@ -42,8 +42,7 @@ class ProfileHeaderView: UIView {
|
|||
|
||||
var accountID: String!
|
||||
|
||||
private var avatarRequest: ImageCache.Request?
|
||||
private var headerRequest: ImageCache.Request?
|
||||
private var imagesTask: Task<Void, Never>?
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
imagesTask?.cancel()
|
||||
let avatar = account.avatar
|
||||
let header = account.header
|
||||
imagesTask = Task {
|
||||
await updateImages(avatar: avatar, header: header)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue