From 3ca42e9916d46e17ba14cd218fad8ca3f828e587 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sat, 25 Dec 2021 14:04:45 -0500 Subject: [PATCH] Sync groups and feeds --- Fervor/Fervor.swift | 2 +- Fervor/FervorClient.swift | 16 ++++- Reader.xcodeproj/project.pbxproj | 16 +++++ Reader/CoreData/ManagedObjectExtensions.swift | 38 +++++++++++ Reader/CoreData/PersistentContainer.swift | 65 +++++++++++++++++++ Reader/FervorController.swift | 12 ++++ Reader/LocalData.swift | 9 +++ .../Reader.xcdatamodel/contents | 31 ++++++++- Reader/SceneDelegate.swift | 13 +++- 9 files changed, 197 insertions(+), 5 deletions(-) create mode 100644 Reader/CoreData/ManagedObjectExtensions.swift create mode 100644 Reader/CoreData/PersistentContainer.swift diff --git a/Fervor/Fervor.swift b/Fervor/Fervor.swift index 392db5e..f5521fe 100644 --- a/Fervor/Fervor.swift +++ b/Fervor/Fervor.swift @@ -8,4 +8,4 @@ import Foundation // todo: fervor: ids should be strings -public typealias FervorID = Int +public typealias FervorID = String diff --git a/Fervor/FervorClient.swift b/Fervor/FervorClient.swift index a18e39c..adf9b05 100644 --- a/Fervor/FervorClient.swift +++ b/Fervor/FervorClient.swift @@ -13,7 +13,11 @@ public class FervorClient { let session: URLSession public var accessToken: String? - private let decoder = JSONDecoder() + private let decoder: JSONDecoder = { + let d = JSONDecoder() + d.dateDecodingStrategy = .iso8601 + return d + }() public init(instanceURL: URL, accessToken: String?, session: URLSession = .shared) { self.instanceURL = instanceURL @@ -61,6 +65,16 @@ public class FervorClient { return try await performRequest(request) } + public func groups() async throws -> [Group] { + let request = URLRequest(url: buildURL(path: "/api/v1/groups")) + return try await performRequest(request) + } + + public func feeds() async throws -> [Feed] { + let request = URLRequest(url: buildURL(path: "/api/v1/feeds")) + return try await performRequest(request) + } + public struct Auth { public let accessToken: String public let refreshToken: String? diff --git a/Reader.xcodeproj/project.pbxproj b/Reader.xcodeproj/project.pbxproj index bdbad89..b25a28a 100644 --- a/Reader.xcodeproj/project.pbxproj +++ b/Reader.xcodeproj/project.pbxproj @@ -15,6 +15,8 @@ D65B18BC27504FE7004A9448 /* Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B18BB27504FE7004A9448 /* Token.swift */; }; D65B18BE275051A1004A9448 /* LocalData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B18BD275051A1004A9448 /* LocalData.swift */; }; D65B18C127505348004A9448 /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D65B18C027505348004A9448 /* HomeViewController.swift */; }; + D6A8A33427766C2800CCEC72 /* PersistentContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A8A33327766C2800CCEC72 /* PersistentContainer.swift */; }; + D6A8A33727766EA100CCEC72 /* ManagedObjectExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A8A33627766EA100CCEC72 /* ManagedObjectExtensions.swift */; }; D6C687EC272CD27600874C10 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C687EB272CD27600874C10 /* AppDelegate.swift */; }; D6C687EE272CD27600874C10 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C687ED272CD27600874C10 /* SceneDelegate.swift */; }; D6C687F6272CD27600874C10 /* Reader.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = D6C687F4272CD27600874C10 /* Reader.xcdatamodeld */; }; @@ -80,6 +82,8 @@ D65B18BB27504FE7004A9448 /* Token.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Token.swift; sourceTree = ""; }; D65B18BD275051A1004A9448 /* LocalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalData.swift; sourceTree = ""; }; D65B18C027505348004A9448 /* HomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewController.swift; sourceTree = ""; }; + D6A8A33327766C2800CCEC72 /* PersistentContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistentContainer.swift; sourceTree = ""; }; + D6A8A33627766EA100CCEC72 /* ManagedObjectExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedObjectExtensions.swift; sourceTree = ""; }; D6C687E8272CD27600874C10 /* Reader.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Reader.app; sourceTree = BUILT_PRODUCTS_DIR; }; D6C687EB272CD27600874C10 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; D6C687ED272CD27600874C10 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -159,6 +163,15 @@ path = Home; sourceTree = ""; }; + D6A8A33527766E9300CCEC72 /* CoreData */ = { + isa = PBXGroup; + children = ( + D6A8A33327766C2800CCEC72 /* PersistentContainer.swift */, + D6A8A33627766EA100CCEC72 /* ManagedObjectExtensions.swift */, + ); + path = CoreData; + sourceTree = ""; + }; D6C687DF272CD27600874C10 = { isa = PBXGroup; children = ( @@ -188,6 +201,7 @@ D6C687ED272CD27600874C10 /* SceneDelegate.swift */, D65B18B527504920004A9448 /* FervorController.swift */, D65B18BD275051A1004A9448 /* LocalData.swift */, + D6A8A33527766E9300CCEC72 /* CoreData */, D65B18AF2750468B004A9448 /* Screens */, D6C687F7272CD27700874C10 /* Assets.xcassets */, D6C687F9272CD27700874C10 /* LaunchScreen.storyboard */, @@ -404,9 +418,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D6A8A33427766C2800CCEC72 /* PersistentContainer.swift in Sources */, D65B18B627504920004A9448 /* FervorController.swift in Sources */, D6C687EC272CD27600874C10 /* AppDelegate.swift in Sources */, D6C687F6272CD27600874C10 /* Reader.xcdatamodeld in Sources */, + D6A8A33727766EA100CCEC72 /* ManagedObjectExtensions.swift in Sources */, D65B18BE275051A1004A9448 /* LocalData.swift in Sources */, D65B18B22750469D004A9448 /* LoginViewController.swift in Sources */, D65B18C127505348004A9448 /* HomeViewController.swift in Sources */, diff --git a/Reader/CoreData/ManagedObjectExtensions.swift b/Reader/CoreData/ManagedObjectExtensions.swift new file mode 100644 index 0000000..970a8ec --- /dev/null +++ b/Reader/CoreData/ManagedObjectExtensions.swift @@ -0,0 +1,38 @@ +// +// ManagedObjectExtensions.swift +// Reader +// +// Created by Shadowfacts on 12/24/21. +// + +import Foundation +import Fervor + +extension Group { + + func updateFromServer(_ serverGroup: Fervor.Group) { + guard self.id == nil || self.id == serverGroup.id else { return } + self.id = serverGroup.id + self.title = serverGroup.title + // feeds relationships will be updated after feeds are created in PersistentContainer.sync + } + +} + +extension Feed { + + func updateFromServer(_ serverFeed: Fervor.Feed) { + guard self.id == nil || self.id == serverFeed.id else { return } + self.id = serverFeed.id + self.title = serverFeed.title + self.url = serverFeed.url + self.lastUpdated = serverFeed.lastUpdated + // todo: check this + self.removeFromGroups(self.groups!.filtered(using: NSPredicate(format: "NOT id IN %@", serverFeed.groupIDs)) as NSSet) + let groupsToAddReq = Group.fetchRequest() + groupsToAddReq.predicate = NSPredicate(format: "id IN %@", serverFeed.groupIDs.filter { g in !self.groups!.contains { ($0 as! Group).id == g } }) + let groupsToAdd = try! self.managedObjectContext!.fetch(groupsToAddReq) + self.addToGroups(NSSet(array: groupsToAdd)) + } + +} diff --git a/Reader/CoreData/PersistentContainer.swift b/Reader/CoreData/PersistentContainer.swift new file mode 100644 index 0000000..4df009b --- /dev/null +++ b/Reader/CoreData/PersistentContainer.swift @@ -0,0 +1,65 @@ +// +// PersistentContainer.swift +// Reader +// +// Created by Shadowfacts on 12/24/21. +// + +import CoreData +import Fervor + +class PersistentContainer: NSPersistentContainer { + + private static let managedObjectModel: NSManagedObjectModel = { + let url = Bundle.main.url(forResource: "Reader", withExtension: "momd")! + return NSManagedObjectModel(contentsOf: url)! + }() + + private(set) lazy var backgroundContext: NSManagedObjectContext = { + let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) + context.parent = self.viewContext + return context + }() + + init(account: LocalData.Account) { + super.init(name: "\(account.id)", managedObjectModel: PersistentContainer.managedObjectModel) + + loadPersistentStores { description, error in + if let error = error { + fatalError("Unable to load persistent store: \(error)") + } + } + } + + func sync(serverGroups: [Fervor.Group], serverFeeds: [Fervor.Feed]) async throws { + try await backgroundContext.perform { + let existingGroups = try self.backgroundContext.fetch(Group.fetchRequest()) + for group in serverGroups { + if let existing = existingGroups.first(where: { $0.id == group.id }) { + existing.updateFromServer(group) + } else { + let mo = Group(context: self.backgroundContext) + mo.updateFromServer(group) + } + } + for removed in existingGroups where !serverGroups.contains(where: { $0.id == removed.id }) { + self.backgroundContext.delete(removed) + } + + let existingFeeds = try self.backgroundContext.fetch(Feed.fetchRequest()) + for feed in serverFeeds { + if let existing = existingFeeds.first(where: { $0.id == feed.id }) { + existing.updateFromServer(feed) + } else { + let mo = Feed(context: self.backgroundContext) + mo.updateFromServer(feed) + } + } + + if self.backgroundContext.hasChanges { + try self.backgroundContext.save() + } + } + } + +} diff --git a/Reader/FervorController.swift b/Reader/FervorController.swift index 48156c8..5f84491 100644 --- a/Reader/FervorController.swift +++ b/Reader/FervorController.swift @@ -19,6 +19,8 @@ class FervorController { private(set) var clientSecret: String? private(set) var accessToken: String? + private(set) var persistentContainer: PersistentContainer! + init(instanceURL: URL) { self.instanceURL = instanceURL self.client = FervorClient(instanceURL: instanceURL, accessToken: nil) @@ -29,6 +31,10 @@ class FervorController { self.clientID = account.clientID self.clientSecret = account.clientSecret self.accessToken = account.accessToken + + self.client.accessToken = account.accessToken + + self.persistentContainer = PersistentContainer(account: account) } func register() async throws -> ClientRegistration { @@ -44,4 +50,10 @@ class FervorController { accessToken = token.accessToken } + func syncGroupsAndFeeds() async { + async let groups = try! client.groups() + async let feeds = try! client.feeds() + try! await persistentContainer.sync(serverGroups: groups, serverFeeds: feeds) + } + } diff --git a/Reader/LocalData.swift b/Reader/LocalData.swift index 39960ff..769e779 100644 --- a/Reader/LocalData.swift +++ b/Reader/LocalData.swift @@ -28,11 +28,20 @@ struct LocalData { } struct Account: Codable { + let id: UUID let instanceURL: URL let clientID: String let clientSecret: String let accessToken: String // todo: refresh tokens + + init(instanceURL: URL, clientID: String, clientSecret: String, accessToken: String) { + self.id = UUID() + self.instanceURL = instanceURL + self.clientID = clientID + self.clientSecret = clientSecret + self.accessToken = accessToken + } } } diff --git a/Reader/Reader.xcdatamodeld/Reader.xcdatamodel/contents b/Reader/Reader.xcdatamodeld/Reader.xcdatamodel/contents index 50d2514..48be3a4 100644 --- a/Reader/Reader.xcdatamodeld/Reader.xcdatamodel/contents +++ b/Reader/Reader.xcdatamodeld/Reader.xcdatamodel/contents @@ -1,4 +1,31 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Reader/SceneDelegate.swift b/Reader/SceneDelegate.swift index 95db367..f93db8b 100644 --- a/Reader/SceneDelegate.swift +++ b/Reader/SceneDelegate.swift @@ -44,6 +44,12 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // Called when the scene has moved from an inactive state to an active state. // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. } + + @MainActor + private func fetchFeeds() async { + let feeds = try! self.fervorController.persistentContainer.viewContext.fetch(Feed.fetchRequest()) + print(feeds) + } func sceneWillResignActive(_ scene: UIScene) { // Called when the scene will move from an active state to an inactive state. @@ -68,6 +74,11 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { let home = HomeViewController(fervorController: fervorController) let nav = UINavigationController(rootViewController: home) window!.rootViewController = nav + + Task(priority: .userInitiated) { + await self.fervorController.syncGroupsAndFeeds() + await self.fetchFeeds() + } } } @@ -75,7 +86,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { extension SceneDelegate: LoginViewControllerDelegate { func didLogin(with controller: FervorController) { LocalData.account = .init(instanceURL: controller.instanceURL, clientID: controller.clientID!, clientSecret: controller.clientSecret!, accessToken: controller.accessToken!) - fervorController = controller + fervorController = FervorController(account: LocalData.account!) createAppUI() } }