First pass at strict concurrency checking

This commit is contained in:
Shadowfacts 2024-01-26 11:02:40 -05:00
parent 5cef76e494
commit c2402303cc
54 changed files with 122 additions and 57 deletions

View File

@ -7,6 +7,7 @@
import UIKit import UIKit
@MainActor
public protocol DuckableViewController: UIViewController { public protocol DuckableViewController: UIViewController {
func duckableViewControllerShouldDuck() -> DuckAttemptAction func duckableViewControllerShouldDuck() -> DuckAttemptAction

View File

@ -7,7 +7,7 @@
import Foundation import Foundation
public enum SearchOperatorType: String, CaseIterable, Equatable { public enum SearchOperatorType: String, CaseIterable, Equatable, Sendable {
case has case has
case `is` case `is`
case language case language

View File

@ -7,7 +7,7 @@
import Foundation import Foundation
public struct StatusEdit: Decodable { public struct StatusEdit: Decodable, Sendable {
public let content: String public let content: String
public let spoilerText: String public let spoilerText: String
public let sensitive: Bool public let sensitive: Bool
@ -28,10 +28,10 @@ public struct StatusEdit: Decodable {
case emojis case emojis
} }
public struct Poll: Decodable { public struct Poll: Decodable, Sendable {
public let options: [Option] public let options: [Option]
public struct Option: Decodable { public struct Option: Decodable, Sendable {
public let title: String public let title: String
} }
} }

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
import Pachyderm import Pachyderm
public enum PostVisibility: Codable, Hashable, CaseIterable { public enum PostVisibility: Codable, Hashable, CaseIterable, Sendable {
case serverDefault case serverDefault
case visibility(Visibility) case visibility(Visibility)
@ -59,6 +59,7 @@ public enum ReplyVisibility: Codable, Hashable, CaseIterable {
public static var allCases: [ReplyVisibility] = [.sameAsPost] + Visibility.allCases.map { .visibility($0) } public static var allCases: [ReplyVisibility] = [.sameAsPost] + Visibility.allCases.map { .visibility($0) }
@MainActor
public func resolved(withServerDefault serverDefault: Visibility?) -> Visibility { public func resolved(withServerDefault serverDefault: Visibility?) -> Visibility {
switch self { switch self {
case .sameAsPost: case .sameAsPost:

View File

@ -12,12 +12,14 @@ import Combine
public final class Preferences: Codable, ObservableObject { public final class Preferences: Codable, ObservableObject {
@MainActor
public static var shared: Preferences = load() public static var shared: Preferences = load()
private static var documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! 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 appGroupDirectory = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.space.vaccor.Tusker")!
private static var archiveURL = appGroupDirectory.appendingPathComponent("preferences").appendingPathExtension("plist") private static var archiveURL = appGroupDirectory.appendingPathComponent("preferences").appendingPathExtension("plist")
@MainActor
public static func save() { public static func save() {
let encoder = PropertyListEncoder() let encoder = PropertyListEncoder()
let data = try? encoder.encode(shared) let data = try? encoder.encode(shared)
@ -33,6 +35,7 @@ public final class Preferences: Codable, ObservableObject {
return Preferences() return Preferences()
} }
@MainActor
public static func migrate(from url: URL) -> Result<Void, any Error> { public static func migrate(from url: URL) -> Result<Void, any Error> {
do { do {
try? FileManager.default.removeItem(at: archiveURL) try? FileManager.default.removeItem(at: archiveURL)

View File

@ -9,7 +9,7 @@
import UIKit import UIKit
import Pachyderm import Pachyderm
public enum StatusSwipeAction: String, Codable, Hashable, CaseIterable { public enum StatusSwipeAction: String, Codable, Hashable, CaseIterable, Sendable {
case reply case reply
case favorite case favorite
case reblog case reblog

View File

@ -54,6 +54,7 @@ struct SwitchAccountContainerView: View {
} }
} }
@MainActor
private struct AccountButtonLabel: View { private struct AccountButtonLabel: View {
static let urlSession = URLSession(configuration: .ephemeral) static let urlSession = URLSession(configuration: .ephemeral)

View File

@ -2392,6 +2392,7 @@
SWIFT_COMPILATION_MODE = wholemodule; SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_OPTIMIZE_OBJECT_LIFETIME = YES; SWIFT_OPTIMIZE_OBJECT_LIFETIME = YES;
SWIFT_STRICT_CONCURRENCY = complete;
VALIDATE_PRODUCT = YES; VALIDATE_PRODUCT = YES;
}; };
name = Dist; name = Dist;
@ -2636,6 +2637,7 @@
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_OPTIMIZE_OBJECT_LIFETIME = YES; SWIFT_OPTIMIZE_OBJECT_LIFETIME = YES;
SWIFT_STRICT_CONCURRENCY = complete;
}; };
name = Debug; name = Debug;
}; };
@ -2694,6 +2696,7 @@
SWIFT_COMPILATION_MODE = wholemodule; SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_OPTIMIZE_OBJECT_LIFETIME = YES; SWIFT_OPTIMIZE_OBJECT_LIFETIME = YES;
SWIFT_STRICT_CONCURRENCY = complete;
VALIDATE_PRODUCT = YES; VALIDATE_PRODUCT = YES;
}; };
name = Release; name = Release;

View File

@ -18,11 +18,9 @@ private let oauthScopes = [Scope.read, .write, .follow]
class MastodonController: ObservableObject { class MastodonController: ObservableObject {
@MainActor
static private(set) var all = [UserAccountInfo: MastodonController]() static private(set) var all = [UserAccountInfo: MastodonController]()
@available(*, message: "do something less dumb")
static var first: MastodonController { all.first!.value }
@MainActor @MainActor
static func getForAccount(_ account: UserAccountInfo) -> MastodonController { static func getForAccount(_ account: UserAccountInfo) -> MastodonController {
if let controller = all[account] { if let controller = all[account] {
@ -34,10 +32,12 @@ class MastodonController: ObservableObject {
} }
} }
@MainActor
static func removeForAccount(_ account: UserAccountInfo) { static func removeForAccount(_ account: UserAccountInfo) {
all.removeValue(forKey: account) all.removeValue(forKey: account)
} }
@MainActor
static func resetAll() { static func resetAll() {
all = [:] all = [:]
} }
@ -544,6 +544,7 @@ class MastodonController: ObservableObject {
filters = (try? persistentContainer.viewContext.fetch(FilterMO.fetchRequest())) ?? [] filters = (try? persistentContainer.viewContext.fetch(FilterMO.fetchRequest())) ?? []
} }
@MainActor
func createDraft(inReplyToID: String? = nil, mentioningAcct: String? = nil, text: String? = nil) -> Draft { func createDraft(inReplyToID: String? = nil, mentioningAcct: String? = nil, text: String? = nil) -> Draft {
var acctsToMention = [String]() var acctsToMention = [String]()

View File

@ -19,7 +19,7 @@ typealias Preferences = TuskerPreferences.Preferences
let stateRestorationLogger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "StateRestoration") let stateRestorationLogger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "StateRestoration")
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "AppDelegate") private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "AppDelegate")
@UIApplicationMain @main
class AppDelegate: UIResponder, UIApplicationDelegate { class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

View File

@ -8,11 +8,15 @@
import UIKit 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)) 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)) 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)) 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)) static let emojis = ImageCache(name: "Emojis", memoryExpiry: .seconds(60 * 5), diskExpiry: .seconds(60 * 60 * 24 * 7))
#if DEBUG #if DEBUG
@ -24,6 +28,7 @@ class ImageCache {
private let cache: ImageDataCache private let cache: ImageDataCache
private let desiredPixelSize: CGSize? private let desiredPixelSize: CGSize?
@MainActor
init(name: String, memoryExpiry: CacheExpiry, diskExpiry: CacheExpiry? = nil, desiredSize: CGSize? = nil) { 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? // 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)) 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) 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, if !ImageCache.disableCaching,
let entry = try? cache.get(url.absoluteString, loadOriginal: loadOriginal) { let entry = try? cache.get(url.absoluteString, loadOriginal: loadOriginal) {
completion?(entry.data, entry.image) 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) { return Task.detached(priority: .userInitiated) {
let result = await self.fetch(url: url) let result = await self.fetch(url: url)
switch result { switch result {

View File

@ -9,7 +9,7 @@
import Foundation 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 Copyright (c) 2022, Chime
All rights reserved. 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. /// 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. /// It will crash if run on any non-main thread.
@MainActor(unsafe) @_unavailableFromAsync
static func runUnsafely<T>(_ body: @MainActor () throws -> T) rethrows -> T { static func runUnsafely<T>(_ 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)()
} }
}}

View File

@ -11,7 +11,9 @@ import PencilKit
extension PKDrawing { 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) let lightTraitCollection = UITraitCollection(userInterfaceStyle: .light)
var drawingImage: UIImage! var drawingImage: UIImage!
lightTraitCollection.performAsCurrent { lightTraitCollection.performAsCurrent {

View File

@ -10,6 +10,7 @@ import SwiftUI
import Combine import Combine
extension View { extension View {
@MainActor
@ViewBuilder @ViewBuilder
func appGroupedListBackground(container: UIAppearanceContainer.Type, applyBackground: Bool = true) -> some View { func appGroupedListBackground(container: UIAppearanceContainer.Type, applyBackground: Bool = true) -> some View {
if #available(iOS 16.0, *) { if #available(iOS 16.0, *) {

View File

@ -8,6 +8,7 @@
import UIKit import UIKit
@MainActor
struct MenuController { struct MenuController {
static let composeCommand: UIKeyCommand = { static let composeCommand: UIKeyCommand = {

View File

@ -12,7 +12,7 @@ import os
// once we target iOS 16, replace uses of this with OSAllocatedUnfairLock<[Key: Value]> // once we target iOS 16, replace uses of this with OSAllocatedUnfairLock<[Key: Value]>
// to make the lock semantics more clear // to make the lock semantics more clear
@available(iOS, obsoleted: 16.0) @available(iOS, obsoleted: 16.0)
class MultiThreadDictionary<Key: Hashable & Sendable, Value: Sendable> { final class MultiThreadDictionary<Key: Hashable & Sendable, Value: Sendable>: @unchecked Sendable {
private let lock: any Lock<[Key: Value]> private let lock: any Lock<[Key: Value]>
init() { init() {

View File

@ -11,6 +11,7 @@ import Pachyderm
import TuskerPreferences import TuskerPreferences
extension StatusSwipeAction { extension StatusSwipeAction {
@MainActor
func createAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction? { func createAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction? {
switch self { switch self {
case .reply: case .reply:
@ -40,6 +41,7 @@ protocol StatusSwipeActionContainer: UIView {
func performReplyAction() func performReplyAction()
} }
@MainActor
private func createReplyAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction? { private func createReplyAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction? {
guard container.mastodonController.loggedIn else { guard container.mastodonController.loggedIn else {
return nil return nil
@ -53,6 +55,7 @@ private func createReplyAction(status: StatusMO, container: StatusSwipeActionCon
return action return action
} }
@MainActor
private func createFavoriteAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction? { private func createFavoriteAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction? {
guard container.mastodonController.loggedIn else { guard container.mastodonController.loggedIn else {
return nil return nil
@ -69,6 +72,7 @@ private func createFavoriteAction(status: StatusMO, container: StatusSwipeAction
return action return action
} }
@MainActor
private func createReblogAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction? { private func createReblogAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction? {
guard container.mastodonController.loggedIn, guard container.mastodonController.loggedIn,
container.canReblog else { container.canReblog else {
@ -86,6 +90,7 @@ private func createReblogAction(status: StatusMO, container: StatusSwipeActionCo
return action return action
} }
@MainActor
private func createShareAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction { private func createShareAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction {
let action = UIContextualAction(style: .normal, title: "Share") { [unowned container] _, _, completion in let action = UIContextualAction(style: .normal, title: "Share") { [unowned container] _, _, completion in
MainActor.runUnsafely { MainActor.runUnsafely {
@ -100,6 +105,7 @@ private func createShareAction(status: StatusMO, container: StatusSwipeActionCon
return action return action
} }
@MainActor
private func createBookmarkAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction? { private func createBookmarkAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction? {
guard container.mastodonController.loggedIn else { guard container.mastodonController.loggedIn else {
return nil return nil
@ -124,11 +130,10 @@ private func createBookmarkAction(status: StatusMO, container: StatusSwipeAction
return action return action
} }
@MainActor
private func createOpenInSafariAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction { private func createOpenInSafariAction(status: StatusMO, container: StatusSwipeActionContainer) -> UIContextualAction {
let action = UIContextualAction(style: .normal, title: "Open in Safari") { [unowned container] _, _, completion in 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) completion(true)
} }
action.image = UIImage(systemName: "safari") action.image = UIImage(systemName: "safari")

View File

@ -10,10 +10,14 @@ import Foundation
import Pachyderm import Pachyderm
import CoreData import CoreData
// TODO: remove this class eventually
class SavedDataManager: Codable { class SavedDataManager: Codable {
@MainActor
private static var documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! private static var documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
@MainActor
private static var archiveURL = SavedDataManager.documentsDirectory.appendingPathComponent("saved_data").appendingPathExtension("plist") private static var archiveURL = SavedDataManager.documentsDirectory.appendingPathComponent("saved_data").appendingPathExtension("plist")
@MainActor
static func load() -> SavedDataManager? { static func load() -> SavedDataManager? {
let decoder = PropertyListDecoder() let decoder = PropertyListDecoder()
if let data = try? Data(contentsOf: archiveURL), if let data = try? Data(contentsOf: archiveURL),
@ -23,6 +27,7 @@ class SavedDataManager: Codable {
return nil return nil
} }
@MainActor
static func destroy() throws { static func destroy() throws {
try FileManager.default.removeItem(at: archiveURL) try FileManager.default.removeItem(at: archiveURL)
} }
@ -39,6 +44,7 @@ class SavedDataManager: Codable {
return s return s
} }
@MainActor
private func save() { private func save() {
let encoder = PropertyListEncoder() let encoder = PropertyListEncoder()
let data = try? encoder.encode(self) let data = try? encoder.encode(self)
@ -46,8 +52,6 @@ class SavedDataManager: Codable {
} }
func migrateToCoreData(accountID: String, context: NSManagedObjectContext) throws { func migrateToCoreData(accountID: String, context: NSManagedObjectContext) throws {
var changed = false
if let hashtags = savedHashtags[accountID] { if let hashtags = savedHashtags[accountID] {
let objects: [[String: Any]] = hashtags.map { let objects: [[String: Any]] = hashtags.map {
["url": $0.url, "name": $0.name] ["url": $0.url, "name": $0.name]
@ -55,7 +59,6 @@ class SavedDataManager: Codable {
let hashtagsReq = NSBatchInsertRequest(entity: SavedHashtag.entity(), objects: objects) let hashtagsReq = NSBatchInsertRequest(entity: SavedHashtag.entity(), objects: objects)
try context.execute(hashtagsReq) try context.execute(hashtagsReq)
savedHashtags.removeValue(forKey: accountID) savedHashtags.removeValue(forKey: accountID)
changed = true
} }
if let instances = savedInstances[accountID] { if let instances = savedInstances[accountID] {
@ -65,11 +68,6 @@ class SavedDataManager: Codable {
let instancesReq = NSBatchInsertRequest(entity: SavedInstance.entity(), objects: objects) let instancesReq = NSBatchInsertRequest(entity: SavedInstance.entity(), objects: objects)
try context.execute(instancesReq) try context.execute(instancesReq)
savedInstances.removeValue(forKey: accountID) savedInstances.removeValue(forKey: accountID)
changed = true
}
if changed {
save()
} }
} }
} }

View File

@ -9,6 +9,7 @@
import UIKit import UIKit
import Sentry import Sentry
@MainActor
protocol TuskerSceneDelegate: UISceneDelegate { protocol TuskerSceneDelegate: UISceneDelegate {
var window: UIWindow? { get } var window: UIWindow? { get }
var rootViewController: TuskerRootViewController? { get } var rootViewController: TuskerRootViewController? { get }

View File

@ -9,6 +9,7 @@
import UIKit import UIKit
import PencilKit import PencilKit
@MainActor
protocol ComposeDrawingViewControllerDelegate: AnyObject { protocol ComposeDrawingViewControllerDelegate: AnyObject {
func composeDrawingViewControllerClose(_ drawingController: ComposeDrawingViewController) func composeDrawingViewControllerClose(_ drawingController: ComposeDrawingViewController)
func composeDrawingViewController(_ drawingController: ComposeDrawingViewController, saveDrawing drawing: PKDrawing) func composeDrawingViewController(_ drawingController: ComposeDrawingViewController, saveDrawing drawing: PKDrawing)

View File

@ -16,6 +16,7 @@ import Pachyderm
import CoreData import CoreData
import Duckable import Duckable
@MainActor
protocol ComposeHostingControllerDelegate: AnyObject { protocol ComposeHostingControllerDelegate: AnyObject {
func dismissCompose(mode: DismissMode) -> Bool func dismissCompose(mode: DismissMode) -> Bool
} }

View File

@ -216,7 +216,7 @@ class ConversationViewController: UIViewController {
state = .loading(indicator) state = .loading(indicator)
let effectiveURL: String 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) { func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) {
completionHandler(nil) completionHandler(nil)
} }

View File

@ -166,7 +166,8 @@ class IssueReporterViewController: UIViewController {
} }
extension IssueReporterViewController: MFMailComposeViewControllerDelegate { extension IssueReporterViewController: MFMailComposeViewControllerDelegate {
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { nonisolated func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
MainActor.runUnsafely {
controller.dismiss(animated: true) { controller.dismiss(animated: true) {
if result == .cancelled { if result == .cancelled {
// don't dismiss ourself, to allowe the user to send the report a different way // don't dismiss ourself, to allowe the user to send the report a different way
@ -177,3 +178,4 @@ extension IssueReporterViewController: MFMailComposeViewControllerDelegate {
} }
} }
} }
}

View File

@ -9,6 +9,7 @@
import UIKit import UIKit
import UserAccounts import UserAccounts
@MainActor
protocol FastAccountSwitcherViewControllerDelegate: AnyObject { protocol FastAccountSwitcherViewControllerDelegate: AnyObject {
func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController) func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController)
/// - Parameter point: In the coordinate space of the view to which the pan gesture recognizer is attached. /// - Parameter point: In the coordinate space of the view to which the pan gesture recognizer is attached.

View File

@ -57,12 +57,14 @@ class GalleryFallbackViewController: QLPreviewController {
} }
extension GalleryFallbackViewController: QLPreviewControllerDataSource { extension GalleryFallbackViewController: QLPreviewControllerDataSource {
func numberOfPreviewItems(in controller: QLPreviewController) -> Int { nonisolated func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
return 1 return 1
} }
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem { nonisolated func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
return previewItem return MainActor.runUnsafely {
previewItem
}
} }
} }

View File

@ -12,6 +12,7 @@ import Pachyderm
@preconcurrency import VisionKit @preconcurrency import VisionKit
import TuskerComponents import TuskerComponents
@MainActor
protocol LargeImageContentView: UIView { protocol LargeImageContentView: UIView {
var animationImage: UIImage? { get } var animationImage: UIImage? { get }
var activityItemsForSharing: [Any] { get } var activityItemsForSharing: [Any] { get }

View File

@ -9,6 +9,7 @@
import UIKit import UIKit
import TuskerComponents import TuskerComponents
@MainActor
protocol LargeImageAnimatableViewController: UIViewController { protocol LargeImageAnimatableViewController: UIViewController {
var animationSourceView: UIImageView? { get } var animationSourceView: UIImageView? { get }
var largeImageController: LargeImageViewController? { get } var largeImageController: LargeImageViewController? { get }

View File

@ -11,6 +11,7 @@ import ScreenCorners
import UserAccounts import UserAccounts
import ComposeUI import ComposeUI
@MainActor
protocol AccountSwitchableViewController: TuskerRootViewController { protocol AccountSwitchableViewController: TuskerRootViewController {
var isFastAccountSwitcherActive: Bool { get } var isFastAccountSwitcherActive: Bool { get }
} }

View File

@ -10,6 +10,7 @@ import UIKit
import Pachyderm import Pachyderm
import Combine import Combine
@MainActor
protocol MainSidebarViewControllerDelegate: AnyObject { protocol MainSidebarViewControllerDelegate: AnyObject {
func sidebarRequestPresentCompose(_ sidebarViewController: MainSidebarViewController) func sidebarRequestPresentCompose(_ sidebarViewController: MainSidebarViewController)
func sidebar(_ sidebarViewController: MainSidebarViewController, didSelectItem item: MainSidebarViewController.Item) func sidebar(_ sidebarViewController: MainSidebarViewController, didSelectItem item: MainSidebarViewController.Item)

View File

@ -481,6 +481,7 @@ extension MainSplitViewController: MainSidebarViewControllerDelegate {
} }
fileprivate extension MainSidebarViewController.Item { fileprivate extension MainSidebarViewController.Item {
@MainActor
func createRootViewController(_ mastodonController: MastodonController) -> UIViewController? { func createRootViewController(_ mastodonController: MastodonController) -> UIViewController? {
switch self { switch self {
case let .tab(tab): case let .tab(tab):

View File

@ -216,6 +216,7 @@ extension MainTabBarViewController {
case explore case explore
case myProfile case myProfile
@MainActor
func createViewController(_ mastodonController: MastodonController) -> UIViewController { func createViewController(_ mastodonController: MastodonController) -> UIViewController {
switch self { switch self {
case .timelines: case .timelines:

View File

@ -83,6 +83,7 @@ enum TuskerRoute {
// case myProfile // case myProfile
//} //}
// //
@MainActor
protocol NavigationControllerProtocol: UIViewController { protocol NavigationControllerProtocol: UIViewController {
var viewControllers: [UIViewController] { get set } var viewControllers: [UIViewController] { get set }
var topViewController: UIViewController? { get } var topViewController: UIViewController? { get }

View File

@ -10,6 +10,7 @@ import UIKit
import Combine import Combine
import Pachyderm import Pachyderm
@MainActor
protocol InstanceSelectorTableViewControllerDelegate: AnyObject { protocol InstanceSelectorTableViewControllerDelegate: AnyObject {
func didSelectInstance(url: URL) func didSelectInstance(url: URL)
} }

View File

@ -103,6 +103,7 @@ private struct WideCapsule: Shape {
} }
} }
@MainActor
private protocol NavigationModePreview: UIView { private protocol NavigationModePreview: UIView {
init(startAnimation: PassthroughSubject<Void, Never>) init(startAnimation: PassthroughSubject<Void, Never>)
} }
@ -118,6 +119,7 @@ private struct NavigationModeRepresentable<UIViewType: NavigationModePreview>: U
} }
} }
@MainActor
private let timingParams = UISpringTimingParameters(mass: 1, stiffness: 70, damping: 16, initialVelocity: .zero) private let timingParams = UISpringTimingParameters(mass: 1, stiffness: 70, damping: 16, initialVelocity: .zero)
private final class StackNavigationPreview: UIView, NavigationModePreview { private final class StackNavigationPreview: UIView, NavigationModePreview {

View File

@ -15,6 +15,7 @@ fileprivate let accountCell = "accountCell"
fileprivate let statusCell = "statusCell" fileprivate let statusCell = "statusCell"
fileprivate let hashtagCell = "hashtagCell" fileprivate let hashtagCell = "hashtagCell"
@MainActor
protocol SearchResultsViewControllerDelegate: AnyObject { protocol SearchResultsViewControllerDelegate: AnyObject {
func selectedSearchResult(account accountID: String) func selectedSearchResult(account accountID: String)
func selectedSearchResult(hashtag: Hashtag) func selectedSearchResult(hashtag: Hashtag)
@ -370,7 +371,7 @@ extension SearchResultsViewController {
} }
extension SearchResultsViewController { extension SearchResultsViewController {
enum Section: Hashable { enum Section: Hashable, Sendable {
case tokenSuggestions(SearchOperatorType) case tokenSuggestions(SearchOperatorType)
case loadingIndicator case loadingIndicator
case accounts case accounts
@ -392,7 +393,7 @@ extension SearchResultsViewController {
} }
} }
} }
enum Item: Hashable { enum Item: Hashable, Sendable {
case tokenSuggestion(String) case tokenSuggestion(String)
case loadingIndicator case loadingIndicator
case account(String) case account(String)

View File

@ -162,7 +162,7 @@ extension StatusEditHistoryViewController {
enum Section { enum Section {
case edits case edits
} }
enum Item: Hashable, Equatable { enum Item: Hashable, Equatable, Sendable {
case edit(StatusEdit, CollapseState, index: Int) case edit(StatusEdit, CollapseState, index: Int)
case loadingIndicator case loadingIndicator

View File

@ -9,6 +9,7 @@
import UIKit import UIKit
import Pachyderm import Pachyderm
@MainActor
protocol InstanceTimelineViewControllerDelegate: AnyObject { protocol InstanceTimelineViewControllerDelegate: AnyObject {
func didSaveInstance(url: URL) func didSaveInstance(url: URL)
func didUnsaveInstance(url: URL) func didUnsaveInstance(url: URL)

View File

@ -11,6 +11,7 @@ import Pachyderm
import Combine import Combine
import Sentry import Sentry
@MainActor
protocol TimelineViewControllerDelegate: AnyObject { protocol TimelineViewControllerDelegate: AnyObject {
func timelineViewController(_ timelineViewController: TimelineViewController, willShowJumpToPresentToastWith animator: UIViewPropertyAnimator?) func timelineViewController(_ timelineViewController: TimelineViewController, willShowJumpToPresentToastWith animator: UIViewPropertyAnimator?)
func timelineViewController(_ timelineViewController: TimelineViewController, willDismissJumpToPresentToastWith animator: UIViewPropertyAnimator?) func timelineViewController(_ timelineViewController: TimelineViewController, willDismissJumpToPresentToastWith animator: UIViewPropertyAnimator?)

View File

@ -8,6 +8,7 @@
import Foundation import Foundation
@MainActor
protocol BackgroundableViewController { protocol BackgroundableViewController {
func sceneDidEnterBackground() func sceneDidEnterBackground()
} }

View File

@ -8,6 +8,7 @@
import UIKit import UIKit
@MainActor
protocol CollectionViewController: UIViewController { protocol CollectionViewController: UIViewController {
var collectionView: UICollectionView! { get } var collectionView: UICollectionView! { get }
} }

View File

@ -8,6 +8,7 @@
import UIKit import UIKit
@MainActor
@objc protocol RefreshableViewController { @objc protocol RefreshableViewController {
func refresh() func refresh()

View File

@ -339,6 +339,7 @@ private class SplitSecondaryNavigationController: EnhancedNavigationViewControll
} }
@MainActor
protocol NestedResponderProvider { protocol NestedResponderProvider {
var innerResponder: UIResponder? { get } var innerResponder: UIResponder? { get }
} }

View File

@ -8,6 +8,7 @@
import UIKit import UIKit
@MainActor
protocol StateRestorableViewController: UIViewController { protocol StateRestorableViewController: UIViewController {
func stateRestorationActivity() -> NSUserActivity? func stateRestorationActivity() -> NSUserActivity?
} }

View File

@ -8,6 +8,7 @@
import UIKit import UIKit
@MainActor
protocol StatusBarTappableViewController: UIViewController { protocol StatusBarTappableViewController: UIViewController {
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult
} }

View File

@ -8,6 +8,7 @@
import UIKit import UIKit
@MainActor
protocol TabBarScrollableViewController: UIViewController { protocol TabBarScrollableViewController: UIViewController {
func tabBarScrollToTop() func tabBarScrollToTop()
} }

View File

@ -8,6 +8,7 @@
import UIKit import UIKit
@MainActor
@objc protocol TabbedPageViewController { @objc protocol TabbedPageViewController {
func selectNextPage() func selectNextPage()
func selectPrevPage() func selectPrevPage()

View File

@ -180,6 +180,7 @@ enum PopoverSource {
case view(WeakHolder<UIView>) case view(WeakHolder<UIView>)
case barButtonItem(WeakHolder<UIBarButtonItem>) case barButtonItem(WeakHolder<UIBarButtonItem>)
@MainActor
func apply(to viewController: UIViewController) { func apply(to viewController: UIViewController) {
if let popoverPresentationController = viewController.popoverPresentationController { if let popoverPresentationController = viewController.popoverPresentationController {
switch self { switch self {

View File

@ -11,6 +11,7 @@ import Pachyderm
import AVFoundation import AVFoundation
import TuskerComponents import TuskerComponents
@MainActor
protocol AttachmentViewDelegate: AnyObject { protocol AttachmentViewDelegate: AnyObject {
func attachmentViewGallery(startingAt index: Int) -> GalleryViewController? func attachmentViewGallery(startingAt index: Int) -> GalleryViewController?
func attachmentViewPresent(_ vc: UIViewController, animated: Bool) func attachmentViewPresent(_ vc: UIViewController, animated: Bool)

View File

@ -12,6 +12,7 @@ import WebURLFoundationExtras
private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: []) private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: [])
@MainActor
protocol BaseEmojiLabel: AnyObject { protocol BaseEmojiLabel: AnyObject {
var emojiIdentifier: AnyHashable? { get set } var emojiIdentifier: AnyHashable? { get set }
var emojiRequests: [ImageCache.Request] { 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 // Based on https://github.com/ReticentJohn/Amaroq/blob/7c5b7088eb9fd1611dcb0f47d43bf8df093e142c/DireFloof/InlineImageHelpers.m
let adjustedCapHeight = emojiFont.capHeight - 1 let adjustedCapHeight = emojiFont.capHeight - 1
let screenScale = UIScreen.main.scale
@Sendable
func emojiImageSize(_ image: UIImage) -> CGSize { func emojiImageSize(_ image: UIImage) -> CGSize {
var imageSizeMatchingFontSize = CGSize(width: image.size.width * (adjustedCapHeight / image.size.height), height: adjustedCapHeight) var imageSizeMatchingFontSize = CGSize(width: image.size.width * (adjustedCapHeight / image.size.height), height: adjustedCapHeight)
var scale: CGFloat = 1.4 let scale = 1.4 * screenScale
scale *= UIScreen.main.scale
imageSizeMatchingFontSize = CGSize(width: imageSizeMatchingFontSize.width * scale, height: imageSizeMatchingFontSize.height * scale) imageSizeMatchingFontSize = CGSize(width: imageSizeMatchingFontSize.width * scale, height: imageSizeMatchingFontSize.height * scale)
return imageSizeMatchingFontSize return imageSizeMatchingFontSize
} }
@ -88,7 +90,7 @@ extension BaseEmojiLabel {
} }
image.prepareThumbnail(of: emojiImageSize(image)) { thumbnail in image.prepareThumbnail(of: emojiImageSize(image)) { thumbnail in
guard let thumbnail = thumbnail?.cgImage, 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 { let transformedImage = ImageGrayscalifier.convertIfNecessary(url: URL(emoji.url)!, image: rescaled) else {
group.leave() group.leave()
return return

View File

@ -87,7 +87,9 @@ class ContentTextView: LinkTextView, BaseEmojiLabel {
updateLinkUnderlineStyle() 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 { if UIAccessibility.buttonShapesEnabled || preference {
linkTextAttributes[.underlineStyle] = NSUnderlineStyle.single.rawValue linkTextAttributes[.underlineStyle] = NSUnderlineStyle.single.rawValue
} else { } else {

View File

@ -9,6 +9,7 @@
import UIKit import UIKit
import Pachyderm import Pachyderm
@MainActor
protocol ProfileHeaderViewDelegate: TuskerNavigationDelegate, MenuActionProvider { protocol ProfileHeaderViewDelegate: TuskerNavigationDelegate, MenuActionProvider {
func profileHeader(_ headerView: ProfileHeaderView, selectedPageChangedTo newPage: ProfileViewController.Page) func profileHeader(_ headerView: ProfileHeaderView, selectedPageChangedTo newPage: ProfileViewController.Page)
} }

View File

@ -179,13 +179,16 @@ extension StatusContentContainer {
} }
private extension UIView { private extension UIView {
func observeIsHidden(_ f: @escaping () -> Void) -> NSKeyValueObservation { func observeIsHidden(_ f: @escaping @Sendable @MainActor () -> Void) -> NSKeyValueObservation {
self.observe(\.isHidden) { _, _ in self.observe(\.isHidden) { _, _ in
MainActor.runUnsafely {
f() f()
} }
} }
} }
}
@MainActor
protocol StatusContentView: UIView { protocol StatusContentView: UIView {
var statusContentFillsHorizontally: Bool { get } var statusContentFillsHorizontally: Bool { get }
func estimateHeight(effectiveWidth: CGFloat) -> CGFloat func estimateHeight(effectiveWidth: CGFloat) -> CGFloat

View File

@ -18,8 +18,8 @@ struct ToastConfiguration {
var title: String var title: String
var subtitle: String? var subtitle: String?
var actionTitle: String? var actionTitle: String?
var action: ((ToastView) -> Void)? var action: (@MainActor (ToastView) -> Void)?
var longPressAction: ((ToastView) -> Void)? var longPressAction: (@MainActor (ToastView) -> Void)?
var edgeSpacing: CGFloat = 8 var edgeSpacing: CGFloat = 8
var edge: Edge = .automatic var edge: Edge = .automatic
var dismissOnScroll = true var dismissOnScroll = true
@ -40,7 +40,7 @@ struct ToastConfiguration {
} }
extension 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) self.init(title: title)
// localizedDescription is statically dispatched, so we need to call it after the downcast // localizedDescription is statically dispatched, so we need to call it after the downcast
if let error = error as? Pachyderm.Client.Error { 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 self.init(from: error, with: title, in: viewController) { toast in
Task { Task {
await retryAction(toast) await retryAction(toast)