From dec7a6e57fa62a273d1400b56ae0b91cb7e4e090 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 6 Mar 2022 15:05:33 -0500 Subject: [PATCH] Swift concurrency stuff i don't know if any of this is right, but it seems like it works so... --- Fervor/ClientRegistration.swift | 2 +- Fervor/Feed.swift | 4 +- Fervor/FervorClient.swift | 14 +++-- Fervor/Group.swift | 4 +- Fervor/Instance.swift | 4 +- Fervor/Item.swift | 4 +- Fervor/ItemsSyncUpdate.swift | 4 +- Reader/CoreData/PersistentContainer.swift | 11 ++-- Reader/FervorController.swift | 62 +++++++++---------- Reader/SceneDelegate.swift | 39 +++++++----- Reader/Screens/AppSplitViewController.swift | 4 +- Reader/Screens/Home/HomeViewController.swift | 2 +- .../Screens/Login/LoginViewController.swift | 23 +++---- 13 files changed, 95 insertions(+), 82 deletions(-) diff --git a/Fervor/ClientRegistration.swift b/Fervor/ClientRegistration.swift index f26ea15..40af38c 100644 --- a/Fervor/ClientRegistration.swift +++ b/Fervor/ClientRegistration.swift @@ -7,7 +7,7 @@ import Foundation -public struct ClientRegistration: Decodable { +public struct ClientRegistration: Decodable, Sendable { public let clientID: String public let clientSecret: String diff --git a/Fervor/Feed.swift b/Fervor/Feed.swift index 380a706..45f8313 100644 --- a/Fervor/Feed.swift +++ b/Fervor/Feed.swift @@ -5,9 +5,9 @@ // Created by Shadowfacts on 10/29/21. // -import Foundation +@preconcurrency import Foundation -public struct Feed: Decodable { +public struct Feed: Decodable, Sendable { public let id: FervorID public let title: String public let url: URL? diff --git a/Fervor/FervorClient.swift b/Fervor/FervorClient.swift index aec7e2d..3c8158d 100644 --- a/Fervor/FervorClient.swift +++ b/Fervor/FervorClient.swift @@ -5,13 +5,13 @@ // Created by Shadowfacts on 11/25/21. // -import Foundation +@preconcurrency import Foundation -public class FervorClient { +public actor FervorClient: Sendable { - let instanceURL: URL - let session: URLSession - public var accessToken: String? + private let instanceURL: URL + private let session: URLSession + public private(set) var accessToken: String? private let decoder: JSONDecoder = { let d = JSONDecoder() @@ -81,7 +81,9 @@ public class FervorClient { "client_id": clientID, "client_secret": clientSecret, ]) - return try await performRequest(request) + let result: Token = try await performRequest(request) + self.accessToken = result.accessToken + return result } public func groups() async throws -> [Group] { diff --git a/Fervor/Group.swift b/Fervor/Group.swift index 1fcb419..f638fba 100644 --- a/Fervor/Group.swift +++ b/Fervor/Group.swift @@ -5,9 +5,9 @@ // Created by Shadowfacts on 10/29/21. // -import Foundation +@preconcurrency import Foundation -public struct Group: Decodable { +public struct Group: Decodable, Sendable { public let id: FervorID public let title: String public let feedIDs: [FervorID] diff --git a/Fervor/Instance.swift b/Fervor/Instance.swift index 5185859..8b67336 100644 --- a/Fervor/Instance.swift +++ b/Fervor/Instance.swift @@ -5,9 +5,9 @@ // Created by Shadowfacts on 10/29/21. // -import Foundation +@preconcurrency import Foundation -public struct Instance: Decodable { +public struct Instance: Decodable, Sendable { public let name: String public let url: URL public let version: String diff --git a/Fervor/Item.swift b/Fervor/Item.swift index cb761a3..86505f4 100644 --- a/Fervor/Item.swift +++ b/Fervor/Item.swift @@ -5,9 +5,9 @@ // Created by Shadowfacts on 10/29/21. // -import Foundation +@preconcurrency import Foundation -public struct Item: Decodable { +public struct Item: Decodable, Sendable { public let id: FervorID public let feedID: FervorID public let title: String? diff --git a/Fervor/ItemsSyncUpdate.swift b/Fervor/ItemsSyncUpdate.swift index b695353..eea7b00 100644 --- a/Fervor/ItemsSyncUpdate.swift +++ b/Fervor/ItemsSyncUpdate.swift @@ -5,9 +5,9 @@ // Created by Shadowfacts on 1/9/22. // -import Foundation +@preconcurrency import Foundation -public struct ItemsSyncUpdate: Decodable { +public struct ItemsSyncUpdate: Decodable, Sendable { public let syncTimestamp: Date public let delete: [FervorID] diff --git a/Reader/CoreData/PersistentContainer.swift b/Reader/CoreData/PersistentContainer.swift index 07472e0..cc7c251 100644 --- a/Reader/CoreData/PersistentContainer.swift +++ b/Reader/CoreData/PersistentContainer.swift @@ -9,7 +9,8 @@ import CoreData import Fervor import OSLog -class PersistentContainer: NSPersistentContainer { +// todo: is this actually sendable? +class PersistentContainer: NSPersistentContainer, @unchecked Sendable { private static let managedObjectModel: NSManagedObjectModel = { let url = Bundle.main.url(forResource: "Reader", withExtension: "momd")! @@ -23,13 +24,11 @@ class PersistentContainer: NSPersistentContainer { return context }() - private weak var fervorController: FervorController? + weak var fervorController: FervorController? private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "PersistentContainer") - init(account: LocalData.Account, fervorController: FervorController) { - self.fervorController = fervorController - + init(account: LocalData.Account) { super.init(name: "\(account.id)", managedObjectModel: PersistentContainer.managedObjectModel) loadPersistentStores { description, error in @@ -40,7 +39,7 @@ class PersistentContainer: NSPersistentContainer { } @MainActor - private func saveViewContext() async throws { + func saveViewContext() throws { if viewContext.hasChanges { try viewContext.save() } diff --git a/Reader/FervorController.swift b/Reader/FervorController.swift index b150edd..3a1e657 100644 --- a/Reader/FervorController.swift +++ b/Reader/FervorController.swift @@ -5,12 +5,12 @@ // Created by Shadowfacts on 11/25/21. // -import Foundation +@preconcurrency import Foundation import Fervor -import OSLog +@preconcurrency import OSLog import Combine -class FervorController { +actor FervorController { static let oauthRedirectURI = URL(string: "frenzy://oauth-callback")! @@ -19,36 +19,40 @@ class FervorController { private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "FervorController") let client: FervorClient - private(set) var account: LocalData.Account? + nonisolated let account: LocalData.Account? private(set) var clientID: String? private(set) var clientSecret: String? private(set) var accessToken: String? - private(set) var persistentContainer: PersistentContainer! + nonisolated let persistentContainer: PersistentContainer! - @Published private(set) var syncState = SyncState.done + nonisolated let syncState = PassthroughSubject() + private var lastSyncState = SyncState.done + private var cancellables = Set() - init(instanceURL: URL) { + init(instanceURL: URL, account: LocalData.Account?) async { self.instanceURL = instanceURL - self.client = FervorClient(instanceURL: instanceURL, accessToken: nil) + self.client = FervorClient(instanceURL: instanceURL, accessToken: account?.accessToken) + self.account = account + self.clientID = account?.clientID + self.clientSecret = account?.clientSecret + + if let account = account { + self.persistentContainer = PersistentContainer(account: account) + } else { + self.persistentContainer = nil + } + + persistentContainer?.fervorController = self } - convenience init(account: LocalData.Account) { - self.init(instanceURL: account.instanceURL) - self.account = account - self.clientID = account.clientID - self.clientSecret = account.clientSecret - self.accessToken = account.accessToken - - self.client.accessToken = account.accessToken - - self.persistentContainer = PersistentContainer(account: account, fervorController: self) + convenience init(account: LocalData.Account) async { + await self.init(instanceURL: account.instanceURL, account: account) } private func setSyncState(_ state: SyncState) { - DispatchQueue.main.async { - self.syncState = state - } + lastSyncState = state + syncState.send(state) } func register() async throws -> ClientRegistration { @@ -60,12 +64,11 @@ class FervorController { func getToken(authCode: String) async throws { let token = try await client.token(authCode: authCode, redirectURI: FervorController.oauthRedirectURI, clientID: clientID!, clientSecret: clientSecret!) - client.accessToken = token.accessToken accessToken = token.accessToken } func syncAll() async throws { - guard syncState == .done else { + guard lastSyncState == .done else { return } // always return to .done, even if we throw and stop syncing early @@ -90,7 +93,6 @@ class FervorController { await ExcerptGenerator.generateAll(self) } - @MainActor func syncReadToServer() async { var count = 0 @@ -138,7 +140,7 @@ class FervorController { func markItem(_ item: Item, read: Bool) async { item.read = read do { - let f = item.read ? client.read(item:) : client.unread(item:) + let f = read ? client.read(item:) : client.unread(item:) _ = try await f(item.id!) item.needsReadStateSync = false } catch { @@ -146,12 +148,10 @@ class FervorController { item.needsReadStateSync = true } - if persistentContainer.viewContext.hasChanges { - do { - try persistentContainer.viewContext.save() - } catch { - logger.error("Failed to save view context: \(String(describing: error), privacy: .public)") - } + do { + try self.persistentContainer.saveViewContext() + } catch { + logger.error("Failed to save view context: \(String(describing: error), privacy: .public)") } } diff --git a/Reader/SceneDelegate.swift b/Reader/SceneDelegate.swift index 3ad2ac8..7f01cce 100644 --- a/Reader/SceneDelegate.swift +++ b/Reader/SceneDelegate.swift @@ -5,6 +5,7 @@ // Created by Shadowfacts on 10/29/21. // +@preconcurrency import Foundation import UIKit import OSLog @@ -33,11 +34,17 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { window!.rootViewController = loginVC } else if activity?.activityType == NSUserActivity.activateAccountType, let account = LocalData.accounts.first(where: { $0.id.uuidString == activity!.userInfo?["accountID"] as? String }) { - fervorController = FervorController(account: account) - createAppUI() + Task { @MainActor in + fervorController = await FervorController(account: account) + syncFromServer() + createAppUI() + } } else if let account = LocalData.mostRecentAccount() { - fervorController = FervorController(account: account) - createAppUI() + Task { @MainActor in + fervorController = await FervorController(account: account) + syncFromServer() + createAppUI() + } } else { let loginVC = LoginViewController() loginVC.delegate = self @@ -125,9 +132,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } } - func switchToAccount(_ account: LocalData.Account) { + func switchToAccount(_ account: LocalData.Account) async { LocalData.mostRecentAccountID = account.id - fervorController = FervorController(account: account) + fervorController = await FervorController(account: account) createAppUI() syncFromServer() } @@ -136,15 +143,17 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { extension SceneDelegate: LoginViewControllerDelegate { func didLogin(with controller: FervorController) { - let account = LocalData.Account(instanceURL: controller.instanceURL, clientID: controller.clientID!, clientSecret: controller.clientSecret!, accessToken: controller.accessToken!) - LocalData.accounts.append(account) - LocalData.mostRecentAccountID = account.id - fervorController = FervorController(account: account) - - createAppUI() - syncFromServer() - - UIMenuSystem.main.setNeedsRebuild() + Task { @MainActor in + let account = LocalData.Account(instanceURL: controller.instanceURL, clientID: await controller.clientID!, clientSecret: await controller.clientSecret!, accessToken: await controller.accessToken!) + LocalData.accounts.append(account) + LocalData.mostRecentAccountID = account.id + fervorController = await FervorController(account: account) + + createAppUI() + syncFromServer() + + UIMenuSystem.main.setNeedsRebuild() + } } } diff --git a/Reader/Screens/AppSplitViewController.swift b/Reader/Screens/AppSplitViewController.swift index d93fae2..2638f6f 100644 --- a/Reader/Screens/AppSplitViewController.swift +++ b/Reader/Screens/AppSplitViewController.swift @@ -65,7 +65,9 @@ extension AppSplitViewController: ItemsViewControllerDelegate { extension AppSplitViewController: HomeViewControllerDelegate { func switchToAccount(_ account: LocalData.Account) { if let delegate = view.window?.windowScene?.delegate as? SceneDelegate { - delegate.switchToAccount(account) + Task { @MainActor in + await delegate.switchToAccount(account) + } } } } diff --git a/Reader/Screens/Home/HomeViewController.swift b/Reader/Screens/Home/HomeViewController.swift index 1c83bd4..d8e351d 100644 --- a/Reader/Screens/Home/HomeViewController.swift +++ b/Reader/Screens/Home/HomeViewController.swift @@ -93,7 +93,7 @@ class HomeViewController: UIViewController { feedResultsController.delegate = self try! feedResultsController.performFetch() - fervorController.$syncState + fervorController.syncState .debounce(for: .milliseconds(250), scheduler: RunLoop.main, options: nil) .sink { [unowned self] in self.syncStateChanged($0) diff --git a/Reader/Screens/Login/LoginViewController.swift b/Reader/Screens/Login/LoginViewController.swift index 211c81b..ad224ec 100644 --- a/Reader/Screens/Login/LoginViewController.swift +++ b/Reader/Screens/Login/LoginViewController.swift @@ -5,6 +5,7 @@ // Created by Shadowfacts on 11/25/21. // +@preconcurrency import Foundation import UIKit import AuthenticationServices import Fervor @@ -72,7 +73,7 @@ class LoginViewController: UIViewController { textField.isEnabled = false activityIndicator.startAnimating() - let controller = FervorController(instanceURL: components.url!) + let controller = await FervorController(instanceURL: components.url!, account: nil) let registration: ClientRegistration do { @@ -99,16 +100,16 @@ class LoginViewController: UIViewController { let components = URLComponents(url: callbackURL!, resolvingAgainstBaseURL: false) guard let codeItem = components?.queryItems?.first(where: { $0.name == "code" }), let codeValue = codeItem.value else { - DispatchQueue.main.async { - let alert = UIAlertController(title: "Unable to retrieve authorization code", message: error?.localizedDescription ?? "Unknown Error", preferredStyle: .alert) - alert.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil)) - self.present(alert, animated: true) - - self.textField.isEnabled = true - self.activityIndicator.stopAnimating() - } - return - } + Task { @MainActor in + let alert = UIAlertController(title: "Unable to retrieve authorization code", message: error?.localizedDescription ?? "Unknown Error", preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil)) + self.present(alert, animated: true) + + self.textField.isEnabled = true + self.activityIndicator.stopAnimating() + } + return + } Task { @MainActor in do { try await controller.getToken(authCode: codeValue)