From c2402303cc7bd94ef3d47f20ee97c277b2f1e1b0 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Fri, 26 Jan 2024 11:02:40 -0500 Subject: [PATCH] First pass at strict concurrency checking --- Packages/Duckable/Sources/Duckable/API.swift | 1 + .../Pachyderm/Model/SearchOperatorType.swift | 2 +- .../Sources/Pachyderm/Model/StatusEdit.swift | 6 +++--- .../TuskerPreferences/PostVisibility.swift | 3 ++- .../Sources/TuskerPreferences/Preferences.swift | 3 +++ .../TuskerPreferences/StatusSwipeAction.swift | 2 +- ShareExtension/ShareHostingController.swift | 2 +- ShareExtension/SwitchAccountContainerView.swift | 1 + Tusker.xcodeproj/project.pbxproj | 3 +++ Tusker/API/MastodonController.swift | 7 ++++--- Tusker/AppDelegate.swift | 2 +- Tusker/Caching/ImageCache.swift | 11 ++++++++--- Tusker/Extensions/MainActor+Unsafe.swift | 16 ++++++++++------ Tusker/Extensions/PKDrawing+Render.swift | 4 +++- Tusker/Extensions/View+AppListStyle.swift | 1 + Tusker/MenuController.swift | 1 + Tusker/MultiThreadDictionary.swift | 2 +- Tusker/Preferences/StatusSwipeActions.swift | 11 ++++++++--- Tusker/SavedDataManager.swift | 14 ++++++-------- Tusker/Scenes/TuskerSceneDelegate.swift | 1 + .../Compose/ComposeDrawingViewController.swift | 1 + .../Compose/ComposeHostingController.swift | 1 + .../ConversationViewController.swift | 2 +- .../IssueReporterViewController.swift | 16 +++++++++------- .../FastAccountSwitcherViewController.swift | 1 + .../GalleryFallbackViewController.swift | 8 +++++--- .../Large Image/LargeImageContentView.swift | 1 + .../LargeImageExpandAnimationController.swift | 1 + ...AccountSwitchingContainerViewController.swift | 1 + .../Screens/Main/MainSidebarViewController.swift | 1 + .../Screens/Main/MainSplitViewController.swift | 1 + .../Screens/Main/MainTabBarViewController.swift | 1 + .../Screens/Main/TuskerRootViewController.swift | 1 + .../InstanceSelectorTableViewController.swift | 1 + .../WidescreenNavigationPrefsView.swift | 2 ++ .../Search/SearchResultsViewController.swift | 5 +++-- .../StatusEditHistoryViewController.swift | 2 +- .../InstanceTimelineViewController.swift | 1 + .../Timeline/TimelineViewController.swift | 1 + .../Utilities/BackgroundableViewController.swift | 1 + .../Utilities/CollectionViewController.swift | 1 + .../Utilities/RefreshableViewController.swift | 1 + .../Utilities/SplitNavigationController.swift | 1 + .../StateRestorableViewController.swift | 1 + .../StatusBarTappableViewController.swift | 1 + .../TabBarScrollableViewController.swift | 1 + .../Utilities/TabbedPageViewController.swift | 1 + Tusker/TuskerNavigationDelegate.swift | 1 + Tusker/Views/Attachments/AttachmentView.swift | 1 + Tusker/Views/BaseEmojiLabel.swift | 8 +++++--- Tusker/Views/ContentTextView.swift | 4 +++- .../Views/Profile Header/ProfileHeaderView.swift | 1 + Tusker/Views/Status/StatusContentContainer.swift | 7 +++++-- Tusker/Views/Toast/ToastConfiguration.swift | 8 ++++---- 54 files changed, 122 insertions(+), 57 deletions(-) diff --git a/Packages/Duckable/Sources/Duckable/API.swift b/Packages/Duckable/Sources/Duckable/API.swift index a92e34e6..86b682fe 100644 --- a/Packages/Duckable/Sources/Duckable/API.swift +++ b/Packages/Duckable/Sources/Duckable/API.swift @@ -7,6 +7,7 @@ import UIKit +@MainActor public protocol DuckableViewController: UIViewController { func duckableViewControllerShouldDuck() -> DuckAttemptAction diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/SearchOperatorType.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/SearchOperatorType.swift index 68459a24..097b75a7 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/SearchOperatorType.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/SearchOperatorType.swift @@ -7,7 +7,7 @@ import Foundation -public enum SearchOperatorType: String, CaseIterable, Equatable { +public enum SearchOperatorType: String, CaseIterable, Equatable, Sendable { case has case `is` case language diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/StatusEdit.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/StatusEdit.swift index 53aec8ad..34f978c9 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/StatusEdit.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/StatusEdit.swift @@ -7,7 +7,7 @@ import Foundation -public struct StatusEdit: Decodable { +public struct StatusEdit: Decodable, Sendable { public let content: String public let spoilerText: String public let sensitive: Bool @@ -28,10 +28,10 @@ public struct StatusEdit: Decodable { case emojis } - public struct Poll: Decodable { + public struct Poll: Decodable, Sendable { public let options: [Option] - public struct Option: Decodable { + public struct Option: Decodable, Sendable { public let title: String } } diff --git a/Packages/TuskerPreferences/Sources/TuskerPreferences/PostVisibility.swift b/Packages/TuskerPreferences/Sources/TuskerPreferences/PostVisibility.swift index c0a7eb8d..00cdc97f 100644 --- a/Packages/TuskerPreferences/Sources/TuskerPreferences/PostVisibility.swift +++ b/Packages/TuskerPreferences/Sources/TuskerPreferences/PostVisibility.swift @@ -8,7 +8,7 @@ import Foundation import Pachyderm -public enum PostVisibility: Codable, Hashable, CaseIterable { +public enum PostVisibility: Codable, Hashable, CaseIterable, Sendable { case serverDefault case visibility(Visibility) @@ -59,6 +59,7 @@ public enum ReplyVisibility: Codable, Hashable, CaseIterable { public static var allCases: [ReplyVisibility] = [.sameAsPost] + Visibility.allCases.map { .visibility($0) } + @MainActor public func resolved(withServerDefault serverDefault: Visibility?) -> Visibility { switch self { case .sameAsPost: diff --git a/Packages/TuskerPreferences/Sources/TuskerPreferences/Preferences.swift b/Packages/TuskerPreferences/Sources/TuskerPreferences/Preferences.swift index 92cbb5ae..ea9af424 100644 --- a/Packages/TuskerPreferences/Sources/TuskerPreferences/Preferences.swift +++ b/Packages/TuskerPreferences/Sources/TuskerPreferences/Preferences.swift @@ -12,12 +12,14 @@ import Combine public final class Preferences: Codable, ObservableObject { + @MainActor public static var shared: Preferences = load() private static var documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! private static var appGroupDirectory = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.space.vaccor.Tusker")! private static var archiveURL = appGroupDirectory.appendingPathComponent("preferences").appendingPathExtension("plist") + @MainActor public static func save() { let encoder = PropertyListEncoder() let data = try? encoder.encode(shared) @@ -33,6 +35,7 @@ public final class Preferences: Codable, ObservableObject { return Preferences() } + @MainActor public static func migrate(from url: URL) -> Result { do { try? FileManager.default.removeItem(at: archiveURL) diff --git a/Packages/TuskerPreferences/Sources/TuskerPreferences/StatusSwipeAction.swift b/Packages/TuskerPreferences/Sources/TuskerPreferences/StatusSwipeAction.swift index ae3aa98b..401ca502 100644 --- a/Packages/TuskerPreferences/Sources/TuskerPreferences/StatusSwipeAction.swift +++ b/Packages/TuskerPreferences/Sources/TuskerPreferences/StatusSwipeAction.swift @@ -9,7 +9,7 @@ import UIKit import Pachyderm -public enum StatusSwipeAction: String, Codable, Hashable, CaseIterable { +public enum StatusSwipeAction: String, Codable, Hashable, CaseIterable, Sendable { case reply case favorite case reblog diff --git a/ShareExtension/ShareHostingController.swift b/ShareExtension/ShareHostingController.swift index abf9fa10..086c1651 100644 --- a/ShareExtension/ShareHostingController.swift +++ b/ShareExtension/ShareHostingController.swift @@ -128,7 +128,7 @@ extension UIColor { return .systemBackground } } - + static let appGroupedBackground = UIColor { traitCollection in if case .dark = traitCollection.userInterfaceStyle, !Preferences.shared.pureBlackDarkMode { diff --git a/ShareExtension/SwitchAccountContainerView.swift b/ShareExtension/SwitchAccountContainerView.swift index 42cfab67..9ac6f649 100644 --- a/ShareExtension/SwitchAccountContainerView.swift +++ b/ShareExtension/SwitchAccountContainerView.swift @@ -54,6 +54,7 @@ struct SwitchAccountContainerView: View { } } +@MainActor private struct AccountButtonLabel: View { static let urlSession = URLSession(configuration: .ephemeral) diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 7a937708..020d34de 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -2392,6 +2392,7 @@ SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_OPTIMIZE_OBJECT_LIFETIME = YES; + SWIFT_STRICT_CONCURRENCY = complete; VALIDATE_PRODUCT = YES; }; name = Dist; @@ -2636,6 +2637,7 @@ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZE_OBJECT_LIFETIME = YES; + SWIFT_STRICT_CONCURRENCY = complete; }; name = Debug; }; @@ -2694,6 +2696,7 @@ SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_OPTIMIZE_OBJECT_LIFETIME = YES; + SWIFT_STRICT_CONCURRENCY = complete; VALIDATE_PRODUCT = YES; }; name = Release; diff --git a/Tusker/API/MastodonController.swift b/Tusker/API/MastodonController.swift index f0028fab..3d1c1fbd 100644 --- a/Tusker/API/MastodonController.swift +++ b/Tusker/API/MastodonController.swift @@ -18,11 +18,9 @@ private let oauthScopes = [Scope.read, .write, .follow] class MastodonController: ObservableObject { + @MainActor static private(set) var all = [UserAccountInfo: MastodonController]() - @available(*, message: "do something less dumb") - static var first: MastodonController { all.first!.value } - @MainActor static func getForAccount(_ account: UserAccountInfo) -> MastodonController { if let controller = all[account] { @@ -34,10 +32,12 @@ class MastodonController: ObservableObject { } } + @MainActor static func removeForAccount(_ account: UserAccountInfo) { all.removeValue(forKey: account) } + @MainActor static func resetAll() { all = [:] } @@ -544,6 +544,7 @@ class MastodonController: ObservableObject { filters = (try? persistentContainer.viewContext.fetch(FilterMO.fetchRequest())) ?? [] } + @MainActor func createDraft(inReplyToID: String? = nil, mentioningAcct: String? = nil, text: String? = nil) -> Draft { var acctsToMention = [String]() diff --git a/Tusker/AppDelegate.swift b/Tusker/AppDelegate.swift index 7d0c21b0..710d1d3c 100644 --- a/Tusker/AppDelegate.swift +++ b/Tusker/AppDelegate.swift @@ -19,7 +19,7 @@ typealias Preferences = TuskerPreferences.Preferences let stateRestorationLogger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "StateRestoration") private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "AppDelegate") -@UIApplicationMain +@main class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { diff --git a/Tusker/Caching/ImageCache.swift b/Tusker/Caching/ImageCache.swift index 9b5cdd05..2043411a 100644 --- a/Tusker/Caching/ImageCache.swift +++ b/Tusker/Caching/ImageCache.swift @@ -8,11 +8,15 @@ import UIKit -class ImageCache { +final class ImageCache: @unchecked Sendable { + @MainActor static let avatars = ImageCache(name: "Avatars", memoryExpiry: .seconds(60 * 60), diskExpiry: .seconds(60 * 60 * 24 * 7), desiredSize: CGSize(width: 50, height: 50)) + @MainActor static let headers = ImageCache(name: "Headers", memoryExpiry: .seconds(60 * 5), diskExpiry: .seconds(60 * 60 * 24 * 7)) + @MainActor static let attachments = ImageCache(name: "Attachments", memoryExpiry: .seconds(60 * 2)) + @MainActor static let emojis = ImageCache(name: "Emojis", memoryExpiry: .seconds(60 * 5), diskExpiry: .seconds(60 * 60 * 24 * 7)) #if DEBUG @@ -24,6 +28,7 @@ class ImageCache { private let cache: ImageDataCache private let desiredPixelSize: CGSize? + @MainActor init(name: String, memoryExpiry: CacheExpiry, diskExpiry: CacheExpiry? = nil, desiredSize: CGSize? = nil) { // todo: might not always want to use UIScreen.main for this, e.g. Catalyst? let pixelSize = desiredSize?.applying(.init(scaleX: UIScreen.main.scale, y: UIScreen.main.scale)) @@ -31,7 +36,7 @@ class ImageCache { self.cache = ImageDataCache(name: name, memoryExpiry: memoryExpiry, diskExpiry: diskExpiry, storeOriginalDataInMemory: diskExpiry == nil, desiredPixelSize: pixelSize) } - func get(_ url: URL, loadOriginal: Bool = false, completion: ((Data?, UIImage?) -> Void)?) -> Request? { + func get(_ url: URL, loadOriginal: Bool = false, completion: (@Sendable (Data?, UIImage?) -> Void)?) -> Request? { if !ImageCache.disableCaching, let entry = try? cache.get(url.absoluteString, loadOriginal: loadOriginal) { completion?(entry.data, entry.image) @@ -41,7 +46,7 @@ class ImageCache { } } - func getFromSource(_ url: URL, completion: ((Data?, UIImage?) -> Void)?) -> Request? { + func getFromSource(_ url: URL, completion: (@Sendable (Data?, UIImage?) -> Void)?) -> Request? { return Task.detached(priority: .userInitiated) { let result = await self.fetch(url: url) switch result { diff --git a/Tusker/Extensions/MainActor+Unsafe.swift b/Tusker/Extensions/MainActor+Unsafe.swift index 69728ac9..2920e9cf 100644 --- a/Tusker/Extensions/MainActor+Unsafe.swift +++ b/Tusker/Extensions/MainActor+Unsafe.swift @@ -9,7 +9,7 @@ import Foundation /* - Copied from https://github.com/ChimeHQ/ConcurrencyPlus/blob/fe3b3fd5436b196d8c5211ab2cc4b69fc35524fe/Sources/ConcurrencyPlus/MainActor%2BUnsafe.swift + Copied from https://github.com/ChimeHQ/ConcurrencyPlus/blob/daf69ed837fa04d4ba666f5a99378cf1815f0dab/Sources/ConcurrencyPlus/MainActor%2BUnsafe.swift Copyright (c) 2022, Chime All rights reserved. @@ -46,10 +46,14 @@ public extension MainActor { /// This function exists to work around libraries with incorrect/inconsistent concurrency annotations. You should be **extremely** careful when using it, and only as a last resort. /// /// It will crash if run on any non-main thread. - @MainActor(unsafe) + @_unavailableFromAsync static func runUnsafely(_ body: @MainActor () throws -> T) rethrows -> T { - dispatchPrecondition(condition: .onQueue(.main)) + if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) { + return try MainActor.assumeIsolated(body) + } - return try body() - } -} + dispatchPrecondition(condition: .onQueue(.main)) + return try withoutActuallyEscaping(body) { fn in + try unsafeBitCast(fn, to: (() throws -> T).self)() + } + }} diff --git a/Tusker/Extensions/PKDrawing+Render.swift b/Tusker/Extensions/PKDrawing+Render.swift index 10c2ccaf..4c07871a 100644 --- a/Tusker/Extensions/PKDrawing+Render.swift +++ b/Tusker/Extensions/PKDrawing+Render.swift @@ -11,7 +11,9 @@ import PencilKit extension PKDrawing { - func imageInLightMode(from rect: CGRect, scale: CGFloat = UIScreen.main.scale) -> UIImage { + @MainActor + func imageInLightMode(from rect: CGRect, scale: CGFloat? = nil) -> UIImage { + let scale = scale ?? UIScreen.main.scale let lightTraitCollection = UITraitCollection(userInterfaceStyle: .light) var drawingImage: UIImage! lightTraitCollection.performAsCurrent { diff --git a/Tusker/Extensions/View+AppListStyle.swift b/Tusker/Extensions/View+AppListStyle.swift index 6df35170..1ca3c12b 100644 --- a/Tusker/Extensions/View+AppListStyle.swift +++ b/Tusker/Extensions/View+AppListStyle.swift @@ -10,6 +10,7 @@ import SwiftUI import Combine extension View { + @MainActor @ViewBuilder func appGroupedListBackground(container: UIAppearanceContainer.Type, applyBackground: Bool = true) -> some View { if #available(iOS 16.0, *) { diff --git a/Tusker/MenuController.swift b/Tusker/MenuController.swift index eb6da265..24afdff2 100644 --- a/Tusker/MenuController.swift +++ b/Tusker/MenuController.swift @@ -8,6 +8,7 @@ import UIKit +@MainActor struct MenuController { static let composeCommand: UIKeyCommand = { diff --git a/Tusker/MultiThreadDictionary.swift b/Tusker/MultiThreadDictionary.swift index 2b0147fa..fde87bd4 100644 --- a/Tusker/MultiThreadDictionary.swift +++ b/Tusker/MultiThreadDictionary.swift @@ -12,7 +12,7 @@ import os // once we target iOS 16, replace uses of this with OSAllocatedUnfairLock<[Key: Value]> // to make the lock semantics more clear @available(iOS, obsoleted: 16.0) -class MultiThreadDictionary { +final class MultiThreadDictionary: @unchecked Sendable { private let lock: any Lock<[Key: Value]> init() { diff --git a/Tusker/Preferences/StatusSwipeActions.swift b/Tusker/Preferences/StatusSwipeActions.swift index 055c3c8d..5e2a6eaf 100644 --- a/Tusker/Preferences/StatusSwipeActions.swift +++ b/Tusker/Preferences/StatusSwipeActions.swift @@ -11,6 +11,7 @@ import Pachyderm import TuskerPreferences extension StatusSwipeAction { + @MainActor func createAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction? { switch self { case .reply: @@ -40,6 +41,7 @@ protocol StatusSwipeActionContainer: UIView { func performReplyAction() } +@MainActor private func createReplyAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction? { guard container.mastodonController.loggedIn else { return nil @@ -53,6 +55,7 @@ private func createReplyAction(status: StatusMO, container: StatusSwipeActionCon return action } +@MainActor private func createFavoriteAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction? { guard container.mastodonController.loggedIn else { return nil @@ -69,6 +72,7 @@ private func createFavoriteAction(status: StatusMO, container: StatusSwipeAction return action } +@MainActor private func createReblogAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction? { guard container.mastodonController.loggedIn, container.canReblog else { @@ -86,6 +90,7 @@ private func createReblogAction(status: StatusMO, container: StatusSwipeActionCo return action } +@MainActor private func createShareAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction { let action = UIContextualAction(style: .normal, title: "Share") { [unowned container] _, _, completion in MainActor.runUnsafely { @@ -100,6 +105,7 @@ private func createShareAction(status: StatusMO, container: StatusSwipeActionCon return action } +@MainActor private func createBookmarkAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction? { guard container.mastodonController.loggedIn else { return nil @@ -124,11 +130,10 @@ private func createBookmarkAction(status: StatusMO, container: StatusSwipeAction return action } +@MainActor private func createOpenInSafariAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction { let action = UIContextualAction(style: .normal, title: "Open in Safari") { [unowned container] _, _, completion in - MainActor.runUnsafely { - container.navigationDelegate.selected(url: status.url!, allowUniversalLinks: false) - } + container.navigationDelegate.selected(url: status.url!, allowUniversalLinks: false) completion(true) } action.image = UIImage(systemName: "safari") diff --git a/Tusker/SavedDataManager.swift b/Tusker/SavedDataManager.swift index 52aee5ea..8ccd418e 100644 --- a/Tusker/SavedDataManager.swift +++ b/Tusker/SavedDataManager.swift @@ -10,10 +10,14 @@ import Foundation import Pachyderm import CoreData +// TODO: remove this class eventually class SavedDataManager: Codable { + @MainActor private static var documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! + @MainActor private static var archiveURL = SavedDataManager.documentsDirectory.appendingPathComponent("saved_data").appendingPathExtension("plist") + @MainActor static func load() -> SavedDataManager? { let decoder = PropertyListDecoder() if let data = try? Data(contentsOf: archiveURL), @@ -23,6 +27,7 @@ class SavedDataManager: Codable { return nil } + @MainActor static func destroy() throws { try FileManager.default.removeItem(at: archiveURL) } @@ -39,6 +44,7 @@ class SavedDataManager: Codable { return s } + @MainActor private func save() { let encoder = PropertyListEncoder() let data = try? encoder.encode(self) @@ -46,8 +52,6 @@ class SavedDataManager: Codable { } func migrateToCoreData(accountID: String, context: NSManagedObjectContext) throws { - var changed = false - if let hashtags = savedHashtags[accountID] { let objects: [[String: Any]] = hashtags.map { ["url": $0.url, "name": $0.name] @@ -55,7 +59,6 @@ class SavedDataManager: Codable { let hashtagsReq = NSBatchInsertRequest(entity: SavedHashtag.entity(), objects: objects) try context.execute(hashtagsReq) savedHashtags.removeValue(forKey: accountID) - changed = true } if let instances = savedInstances[accountID] { @@ -65,11 +68,6 @@ class SavedDataManager: Codable { let instancesReq = NSBatchInsertRequest(entity: SavedInstance.entity(), objects: objects) try context.execute(instancesReq) savedInstances.removeValue(forKey: accountID) - changed = true - } - - if changed { - save() } } } diff --git a/Tusker/Scenes/TuskerSceneDelegate.swift b/Tusker/Scenes/TuskerSceneDelegate.swift index 25de0c44..f6a57f7c 100644 --- a/Tusker/Scenes/TuskerSceneDelegate.swift +++ b/Tusker/Scenes/TuskerSceneDelegate.swift @@ -9,6 +9,7 @@ import UIKit import Sentry +@MainActor protocol TuskerSceneDelegate: UISceneDelegate { var window: UIWindow? { get } var rootViewController: TuskerRootViewController? { get } diff --git a/Tusker/Screens/Compose/ComposeDrawingViewController.swift b/Tusker/Screens/Compose/ComposeDrawingViewController.swift index cc124ad3..f544f5ce 100644 --- a/Tusker/Screens/Compose/ComposeDrawingViewController.swift +++ b/Tusker/Screens/Compose/ComposeDrawingViewController.swift @@ -9,6 +9,7 @@ import UIKit import PencilKit +@MainActor protocol ComposeDrawingViewControllerDelegate: AnyObject { func composeDrawingViewControllerClose(_ drawingController: ComposeDrawingViewController) func composeDrawingViewController(_ drawingController: ComposeDrawingViewController, saveDrawing drawing: PKDrawing) diff --git a/Tusker/Screens/Compose/ComposeHostingController.swift b/Tusker/Screens/Compose/ComposeHostingController.swift index 2e7b1a86..8efacb63 100644 --- a/Tusker/Screens/Compose/ComposeHostingController.swift +++ b/Tusker/Screens/Compose/ComposeHostingController.swift @@ -16,6 +16,7 @@ import Pachyderm import CoreData import Duckable +@MainActor protocol ComposeHostingControllerDelegate: AnyObject { func dismissCompose(mode: DismissMode) -> Bool } diff --git a/Tusker/Screens/Conversation/ConversationViewController.swift b/Tusker/Screens/Conversation/ConversationViewController.swift index 764c9f13..42e6ff1f 100644 --- a/Tusker/Screens/Conversation/ConversationViewController.swift +++ b/Tusker/Screens/Conversation/ConversationViewController.swift @@ -216,7 +216,7 @@ class ConversationViewController: UIViewController { state = .loading(indicator) let effectiveURL: String - class RedirectBlocker: NSObject, URLSessionTaskDelegate { + final class RedirectBlocker: NSObject, URLSessionTaskDelegate, Sendable { func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) { completionHandler(nil) } diff --git a/Tusker/Screens/Crash Reporter/IssueReporterViewController.swift b/Tusker/Screens/Crash Reporter/IssueReporterViewController.swift index 79932f84..be1c86e6 100644 --- a/Tusker/Screens/Crash Reporter/IssueReporterViewController.swift +++ b/Tusker/Screens/Crash Reporter/IssueReporterViewController.swift @@ -166,13 +166,15 @@ class IssueReporterViewController: UIViewController { } extension IssueReporterViewController: MFMailComposeViewControllerDelegate { - func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { - controller.dismiss(animated: true) { - if result == .cancelled { - // don't dismiss ourself, to allowe the user to send the report a different way - } else { - self.finishedReport() - self.doDismiss() + nonisolated func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { + MainActor.runUnsafely { + controller.dismiss(animated: true) { + if result == .cancelled { + // don't dismiss ourself, to allowe the user to send the report a different way + } else { + self.finishedReport() + self.doDismiss() + } } } } diff --git a/Tusker/Screens/Fast Account Switcher/FastAccountSwitcherViewController.swift b/Tusker/Screens/Fast Account Switcher/FastAccountSwitcherViewController.swift index f8b81106..805d1aca 100644 --- a/Tusker/Screens/Fast Account Switcher/FastAccountSwitcherViewController.swift +++ b/Tusker/Screens/Fast Account Switcher/FastAccountSwitcherViewController.swift @@ -9,6 +9,7 @@ import UIKit import UserAccounts +@MainActor protocol FastAccountSwitcherViewControllerDelegate: AnyObject { func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController) /// - Parameter point: In the coordinate space of the view to which the pan gesture recognizer is attached. diff --git a/Tusker/Screens/Large Image/GalleryFallbackViewController.swift b/Tusker/Screens/Large Image/GalleryFallbackViewController.swift index bd993d39..acadb35c 100644 --- a/Tusker/Screens/Large Image/GalleryFallbackViewController.swift +++ b/Tusker/Screens/Large Image/GalleryFallbackViewController.swift @@ -57,12 +57,14 @@ class GalleryFallbackViewController: QLPreviewController { } extension GalleryFallbackViewController: QLPreviewControllerDataSource { - func numberOfPreviewItems(in controller: QLPreviewController) -> Int { + nonisolated func numberOfPreviewItems(in controller: QLPreviewController) -> Int { return 1 } - func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem { - return previewItem + nonisolated func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem { + return MainActor.runUnsafely { + previewItem + } } } diff --git a/Tusker/Screens/Large Image/LargeImageContentView.swift b/Tusker/Screens/Large Image/LargeImageContentView.swift index 68f0b7b6..419763ea 100644 --- a/Tusker/Screens/Large Image/LargeImageContentView.swift +++ b/Tusker/Screens/Large Image/LargeImageContentView.swift @@ -12,6 +12,7 @@ import Pachyderm @preconcurrency import VisionKit import TuskerComponents +@MainActor protocol LargeImageContentView: UIView { var animationImage: UIImage? { get } var activityItemsForSharing: [Any] { get } diff --git a/Tusker/Screens/Large Image/Transitions/LargeImageExpandAnimationController.swift b/Tusker/Screens/Large Image/Transitions/LargeImageExpandAnimationController.swift index 20d0a793..ac257539 100644 --- a/Tusker/Screens/Large Image/Transitions/LargeImageExpandAnimationController.swift +++ b/Tusker/Screens/Large Image/Transitions/LargeImageExpandAnimationController.swift @@ -9,6 +9,7 @@ import UIKit import TuskerComponents +@MainActor protocol LargeImageAnimatableViewController: UIViewController { var animationSourceView: UIImageView? { get } var largeImageController: LargeImageViewController? { get } diff --git a/Tusker/Screens/Main/AccountSwitchingContainerViewController.swift b/Tusker/Screens/Main/AccountSwitchingContainerViewController.swift index e76781c3..c0d3aaa7 100644 --- a/Tusker/Screens/Main/AccountSwitchingContainerViewController.swift +++ b/Tusker/Screens/Main/AccountSwitchingContainerViewController.swift @@ -11,6 +11,7 @@ import ScreenCorners import UserAccounts import ComposeUI +@MainActor protocol AccountSwitchableViewController: TuskerRootViewController { var isFastAccountSwitcherActive: Bool { get } } diff --git a/Tusker/Screens/Main/MainSidebarViewController.swift b/Tusker/Screens/Main/MainSidebarViewController.swift index 615457ab..970e0c12 100644 --- a/Tusker/Screens/Main/MainSidebarViewController.swift +++ b/Tusker/Screens/Main/MainSidebarViewController.swift @@ -10,6 +10,7 @@ import UIKit import Pachyderm import Combine +@MainActor protocol MainSidebarViewControllerDelegate: AnyObject { func sidebarRequestPresentCompose(_ sidebarViewController: MainSidebarViewController) func sidebar(_ sidebarViewController: MainSidebarViewController, didSelectItem item: MainSidebarViewController.Item) diff --git a/Tusker/Screens/Main/MainSplitViewController.swift b/Tusker/Screens/Main/MainSplitViewController.swift index 406d1eec..87103800 100644 --- a/Tusker/Screens/Main/MainSplitViewController.swift +++ b/Tusker/Screens/Main/MainSplitViewController.swift @@ -481,6 +481,7 @@ extension MainSplitViewController: MainSidebarViewControllerDelegate { } fileprivate extension MainSidebarViewController.Item { + @MainActor func createRootViewController(_ mastodonController: MastodonController) -> UIViewController? { switch self { case let .tab(tab): diff --git a/Tusker/Screens/Main/MainTabBarViewController.swift b/Tusker/Screens/Main/MainTabBarViewController.swift index 3cb9186d..42fdbce0 100644 --- a/Tusker/Screens/Main/MainTabBarViewController.swift +++ b/Tusker/Screens/Main/MainTabBarViewController.swift @@ -216,6 +216,7 @@ extension MainTabBarViewController { case explore case myProfile + @MainActor func createViewController(_ mastodonController: MastodonController) -> UIViewController { switch self { case .timelines: diff --git a/Tusker/Screens/Main/TuskerRootViewController.swift b/Tusker/Screens/Main/TuskerRootViewController.swift index bd6b7bd5..c3f0cbbb 100644 --- a/Tusker/Screens/Main/TuskerRootViewController.swift +++ b/Tusker/Screens/Main/TuskerRootViewController.swift @@ -83,6 +83,7 @@ enum TuskerRoute { // case myProfile //} // +@MainActor protocol NavigationControllerProtocol: UIViewController { var viewControllers: [UIViewController] { get set } var topViewController: UIViewController? { get } diff --git a/Tusker/Screens/Onboarding/InstanceSelectorTableViewController.swift b/Tusker/Screens/Onboarding/InstanceSelectorTableViewController.swift index 30f06a82..6758d1d8 100644 --- a/Tusker/Screens/Onboarding/InstanceSelectorTableViewController.swift +++ b/Tusker/Screens/Onboarding/InstanceSelectorTableViewController.swift @@ -10,6 +10,7 @@ import UIKit import Combine import Pachyderm +@MainActor protocol InstanceSelectorTableViewControllerDelegate: AnyObject { func didSelectInstance(url: URL) } diff --git a/Tusker/Screens/Preferences/WidescreenNavigationPrefsView.swift b/Tusker/Screens/Preferences/WidescreenNavigationPrefsView.swift index 89bf1792..912fa19c 100644 --- a/Tusker/Screens/Preferences/WidescreenNavigationPrefsView.swift +++ b/Tusker/Screens/Preferences/WidescreenNavigationPrefsView.swift @@ -103,6 +103,7 @@ private struct WideCapsule: Shape { } } +@MainActor private protocol NavigationModePreview: UIView { init(startAnimation: PassthroughSubject) } @@ -118,6 +119,7 @@ private struct NavigationModeRepresentable: U } } +@MainActor private let timingParams = UISpringTimingParameters(mass: 1, stiffness: 70, damping: 16, initialVelocity: .zero) private final class StackNavigationPreview: UIView, NavigationModePreview { diff --git a/Tusker/Screens/Search/SearchResultsViewController.swift b/Tusker/Screens/Search/SearchResultsViewController.swift index fb5c0975..197b9ac5 100644 --- a/Tusker/Screens/Search/SearchResultsViewController.swift +++ b/Tusker/Screens/Search/SearchResultsViewController.swift @@ -15,6 +15,7 @@ fileprivate let accountCell = "accountCell" fileprivate let statusCell = "statusCell" fileprivate let hashtagCell = "hashtagCell" +@MainActor protocol SearchResultsViewControllerDelegate: AnyObject { func selectedSearchResult(account accountID: String) func selectedSearchResult(hashtag: Hashtag) @@ -370,7 +371,7 @@ extension SearchResultsViewController { } extension SearchResultsViewController { - enum Section: Hashable { + enum Section: Hashable, Sendable { case tokenSuggestions(SearchOperatorType) case loadingIndicator case accounts @@ -392,7 +393,7 @@ extension SearchResultsViewController { } } } - enum Item: Hashable { + enum Item: Hashable, Sendable { case tokenSuggestion(String) case loadingIndicator case account(String) diff --git a/Tusker/Screens/Status Edit History/StatusEditHistoryViewController.swift b/Tusker/Screens/Status Edit History/StatusEditHistoryViewController.swift index 8a672a14..e6f4376c 100644 --- a/Tusker/Screens/Status Edit History/StatusEditHistoryViewController.swift +++ b/Tusker/Screens/Status Edit History/StatusEditHistoryViewController.swift @@ -162,7 +162,7 @@ extension StatusEditHistoryViewController { enum Section { case edits } - enum Item: Hashable, Equatable { + enum Item: Hashable, Equatable, Sendable { case edit(StatusEdit, CollapseState, index: Int) case loadingIndicator diff --git a/Tusker/Screens/Timeline/InstanceTimelineViewController.swift b/Tusker/Screens/Timeline/InstanceTimelineViewController.swift index 8ee54fea..4c565c00 100644 --- a/Tusker/Screens/Timeline/InstanceTimelineViewController.swift +++ b/Tusker/Screens/Timeline/InstanceTimelineViewController.swift @@ -9,6 +9,7 @@ import UIKit import Pachyderm +@MainActor protocol InstanceTimelineViewControllerDelegate: AnyObject { func didSaveInstance(url: URL) func didUnsaveInstance(url: URL) diff --git a/Tusker/Screens/Timeline/TimelineViewController.swift b/Tusker/Screens/Timeline/TimelineViewController.swift index ef0212b9..ea4cbf6d 100644 --- a/Tusker/Screens/Timeline/TimelineViewController.swift +++ b/Tusker/Screens/Timeline/TimelineViewController.swift @@ -11,6 +11,7 @@ import Pachyderm import Combine import Sentry +@MainActor protocol TimelineViewControllerDelegate: AnyObject { func timelineViewController(_ timelineViewController: TimelineViewController, willShowJumpToPresentToastWith animator: UIViewPropertyAnimator?) func timelineViewController(_ timelineViewController: TimelineViewController, willDismissJumpToPresentToastWith animator: UIViewPropertyAnimator?) diff --git a/Tusker/Screens/Utilities/BackgroundableViewController.swift b/Tusker/Screens/Utilities/BackgroundableViewController.swift index da50cdb6..1cad91e7 100644 --- a/Tusker/Screens/Utilities/BackgroundableViewController.swift +++ b/Tusker/Screens/Utilities/BackgroundableViewController.swift @@ -8,6 +8,7 @@ import Foundation +@MainActor protocol BackgroundableViewController { func sceneDidEnterBackground() } diff --git a/Tusker/Screens/Utilities/CollectionViewController.swift b/Tusker/Screens/Utilities/CollectionViewController.swift index 22b6fcc2..2ba5d99e 100644 --- a/Tusker/Screens/Utilities/CollectionViewController.swift +++ b/Tusker/Screens/Utilities/CollectionViewController.swift @@ -8,6 +8,7 @@ import UIKit +@MainActor protocol CollectionViewController: UIViewController { var collectionView: UICollectionView! { get } } diff --git a/Tusker/Screens/Utilities/RefreshableViewController.swift b/Tusker/Screens/Utilities/RefreshableViewController.swift index de912216..70a316b6 100644 --- a/Tusker/Screens/Utilities/RefreshableViewController.swift +++ b/Tusker/Screens/Utilities/RefreshableViewController.swift @@ -8,6 +8,7 @@ import UIKit +@MainActor @objc protocol RefreshableViewController { func refresh() diff --git a/Tusker/Screens/Utilities/SplitNavigationController.swift b/Tusker/Screens/Utilities/SplitNavigationController.swift index f7ee5506..0b179737 100644 --- a/Tusker/Screens/Utilities/SplitNavigationController.swift +++ b/Tusker/Screens/Utilities/SplitNavigationController.swift @@ -339,6 +339,7 @@ private class SplitSecondaryNavigationController: EnhancedNavigationViewControll } +@MainActor protocol NestedResponderProvider { var innerResponder: UIResponder? { get } } diff --git a/Tusker/Screens/Utilities/StateRestorableViewController.swift b/Tusker/Screens/Utilities/StateRestorableViewController.swift index 5044586b..6f4701ab 100644 --- a/Tusker/Screens/Utilities/StateRestorableViewController.swift +++ b/Tusker/Screens/Utilities/StateRestorableViewController.swift @@ -8,6 +8,7 @@ import UIKit +@MainActor protocol StateRestorableViewController: UIViewController { func stateRestorationActivity() -> NSUserActivity? } diff --git a/Tusker/Screens/Utilities/StatusBarTappableViewController.swift b/Tusker/Screens/Utilities/StatusBarTappableViewController.swift index aef7ceb5..13f5cc17 100644 --- a/Tusker/Screens/Utilities/StatusBarTappableViewController.swift +++ b/Tusker/Screens/Utilities/StatusBarTappableViewController.swift @@ -8,6 +8,7 @@ import UIKit +@MainActor protocol StatusBarTappableViewController: UIViewController { func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult } diff --git a/Tusker/Screens/Utilities/TabBarScrollableViewController.swift b/Tusker/Screens/Utilities/TabBarScrollableViewController.swift index 8c7df902..681dadf7 100644 --- a/Tusker/Screens/Utilities/TabBarScrollableViewController.swift +++ b/Tusker/Screens/Utilities/TabBarScrollableViewController.swift @@ -8,6 +8,7 @@ import UIKit +@MainActor protocol TabBarScrollableViewController: UIViewController { func tabBarScrollToTop() } diff --git a/Tusker/Screens/Utilities/TabbedPageViewController.swift b/Tusker/Screens/Utilities/TabbedPageViewController.swift index c1da3d08..905d6cfe 100644 --- a/Tusker/Screens/Utilities/TabbedPageViewController.swift +++ b/Tusker/Screens/Utilities/TabbedPageViewController.swift @@ -8,6 +8,7 @@ import UIKit +@MainActor @objc protocol TabbedPageViewController { func selectNextPage() func selectPrevPage() diff --git a/Tusker/TuskerNavigationDelegate.swift b/Tusker/TuskerNavigationDelegate.swift index c4989a25..c62ab848 100644 --- a/Tusker/TuskerNavigationDelegate.swift +++ b/Tusker/TuskerNavigationDelegate.swift @@ -180,6 +180,7 @@ enum PopoverSource { case view(WeakHolder) case barButtonItem(WeakHolder) + @MainActor func apply(to viewController: UIViewController) { if let popoverPresentationController = viewController.popoverPresentationController { switch self { diff --git a/Tusker/Views/Attachments/AttachmentView.swift b/Tusker/Views/Attachments/AttachmentView.swift index fa1b657b..1cc03512 100644 --- a/Tusker/Views/Attachments/AttachmentView.swift +++ b/Tusker/Views/Attachments/AttachmentView.swift @@ -11,6 +11,7 @@ import Pachyderm import AVFoundation import TuskerComponents +@MainActor protocol AttachmentViewDelegate: AnyObject { func attachmentViewGallery(startingAt index: Int) -> GalleryViewController? func attachmentViewPresent(_ vc: UIViewController, animated: Bool) diff --git a/Tusker/Views/BaseEmojiLabel.swift b/Tusker/Views/BaseEmojiLabel.swift index b8f8e551..40b21f87 100644 --- a/Tusker/Views/BaseEmojiLabel.swift +++ b/Tusker/Views/BaseEmojiLabel.swift @@ -12,6 +12,7 @@ import WebURLFoundationExtras private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: []) +@MainActor protocol BaseEmojiLabel: AnyObject { var emojiIdentifier: AnyHashable? { get set } var emojiRequests: [ImageCache.Request] { get set } @@ -42,10 +43,11 @@ extension BaseEmojiLabel { // Based on https://github.com/ReticentJohn/Amaroq/blob/7c5b7088eb9fd1611dcb0f47d43bf8df093e142c/DireFloof/InlineImageHelpers.m let adjustedCapHeight = emojiFont.capHeight - 1 + let screenScale = UIScreen.main.scale + @Sendable func emojiImageSize(_ image: UIImage) -> CGSize { var imageSizeMatchingFontSize = CGSize(width: image.size.width * (adjustedCapHeight / image.size.height), height: adjustedCapHeight) - var scale: CGFloat = 1.4 - scale *= UIScreen.main.scale + let scale = 1.4 * screenScale imageSizeMatchingFontSize = CGSize(width: imageSizeMatchingFontSize.width * scale, height: imageSizeMatchingFontSize.height * scale) return imageSizeMatchingFontSize } @@ -88,7 +90,7 @@ extension BaseEmojiLabel { } image.prepareThumbnail(of: emojiImageSize(image)) { thumbnail in guard let thumbnail = thumbnail?.cgImage, - case let rescaled = UIImage(cgImage: thumbnail, scale: UIScreen.main.scale, orientation: .up), + case let rescaled = UIImage(cgImage: thumbnail, scale: screenScale, orientation: .up), let transformedImage = ImageGrayscalifier.convertIfNecessary(url: URL(emoji.url)!, image: rescaled) else { group.leave() return diff --git a/Tusker/Views/ContentTextView.swift b/Tusker/Views/ContentTextView.swift index d5e0e3ba..69e86777 100644 --- a/Tusker/Views/ContentTextView.swift +++ b/Tusker/Views/ContentTextView.swift @@ -87,7 +87,9 @@ class ContentTextView: LinkTextView, BaseEmojiLabel { updateLinkUnderlineStyle() } - private func updateLinkUnderlineStyle(preference: Bool = Preferences.shared.underlineTextLinks) { + @MainActor + private func updateLinkUnderlineStyle(preference: Bool? = nil) { + let preference = preference ?? Preferences.shared.underlineTextLinks if UIAccessibility.buttonShapesEnabled || preference { linkTextAttributes[.underlineStyle] = NSUnderlineStyle.single.rawValue } else { diff --git a/Tusker/Views/Profile Header/ProfileHeaderView.swift b/Tusker/Views/Profile Header/ProfileHeaderView.swift index 5c49eb70..13bc1263 100644 --- a/Tusker/Views/Profile Header/ProfileHeaderView.swift +++ b/Tusker/Views/Profile Header/ProfileHeaderView.swift @@ -9,6 +9,7 @@ import UIKit import Pachyderm +@MainActor protocol ProfileHeaderViewDelegate: TuskerNavigationDelegate, MenuActionProvider { func profileHeader(_ headerView: ProfileHeaderView, selectedPageChangedTo newPage: ProfileViewController.Page) } diff --git a/Tusker/Views/Status/StatusContentContainer.swift b/Tusker/Views/Status/StatusContentContainer.swift index 84aebebe..add721fc 100644 --- a/Tusker/Views/Status/StatusContentContainer.swift +++ b/Tusker/Views/Status/StatusContentContainer.swift @@ -179,13 +179,16 @@ extension StatusContentContainer { } private extension UIView { - func observeIsHidden(_ f: @escaping () -> Void) -> NSKeyValueObservation { + func observeIsHidden(_ f: @escaping @Sendable @MainActor () -> Void) -> NSKeyValueObservation { self.observe(\.isHidden) { _, _ in - f() + MainActor.runUnsafely { + f() + } } } } +@MainActor protocol StatusContentView: UIView { var statusContentFillsHorizontally: Bool { get } func estimateHeight(effectiveWidth: CGFloat) -> CGFloat diff --git a/Tusker/Views/Toast/ToastConfiguration.swift b/Tusker/Views/Toast/ToastConfiguration.swift index dcad6ec4..c5fb5578 100644 --- a/Tusker/Views/Toast/ToastConfiguration.swift +++ b/Tusker/Views/Toast/ToastConfiguration.swift @@ -18,8 +18,8 @@ struct ToastConfiguration { var title: String var subtitle: String? var actionTitle: String? - var action: ((ToastView) -> Void)? - var longPressAction: ((ToastView) -> Void)? + var action: (@MainActor (ToastView) -> Void)? + var longPressAction: (@MainActor (ToastView) -> Void)? var edgeSpacing: CGFloat = 8 var edge: Edge = .automatic var dismissOnScroll = true @@ -40,7 +40,7 @@ struct ToastConfiguration { } extension ToastConfiguration { - init(from error: Error, with title: String, in viewController: TuskerNavigationDelegate, retryAction: ((ToastView) -> Void)?) { + init(from error: Error, with title: String, in viewController: TuskerNavigationDelegate, retryAction: (@MainActor (ToastView) -> Void)?) { self.init(title: title) // localizedDescription is statically dispatched, so we need to call it after the downcast if let error = error as? Pachyderm.Client.Error { @@ -71,7 +71,7 @@ extension ToastConfiguration { } } - init(from error: Error, with title: String, in viewController: TuskerNavigationDelegate, retryAction: @escaping @MainActor (ToastView) async -> Void) { + init(from error: Error, with title: String, in viewController: TuskerNavigationDelegate, retryAction: @Sendable @escaping @MainActor (ToastView) async -> Void) { self.init(from: error, with: title, in: viewController) { toast in Task { await retryAction(toast)