Sync groups and feeds
This commit is contained in:
parent
15699b0b85
commit
3ca42e9916
|
@ -8,4 +8,4 @@
|
|||
import Foundation
|
||||
|
||||
// todo: fervor: ids should be strings
|
||||
public typealias FervorID = Int
|
||||
public typealias FervorID = String
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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 = "<group>"; };
|
||||
D65B18BD275051A1004A9448 /* LocalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalData.swift; sourceTree = "<group>"; };
|
||||
D65B18C027505348004A9448 /* HomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewController.swift; sourceTree = "<group>"; };
|
||||
D6A8A33327766C2800CCEC72 /* PersistentContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistentContainer.swift; sourceTree = "<group>"; };
|
||||
D6A8A33627766EA100CCEC72 /* ManagedObjectExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedObjectExtensions.swift; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
D6C687ED272CD27600874C10 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||
|
@ -159,6 +163,15 @@
|
|||
path = Home;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D6A8A33527766E9300CCEC72 /* CoreData */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D6A8A33327766C2800CCEC72 /* PersistentContainer.swift */,
|
||||
D6A8A33627766EA100CCEC72 /* ManagedObjectExtensions.swift */,
|
||||
);
|
||||
path = CoreData;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 */,
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,4 +1,31 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="1" systemVersion="11A491" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="false" userDefinedModelVersionIdentifier="">
|
||||
<elements/>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="19574" systemVersion="21C52" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Feed" representedClassName="Feed" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="id" attributeType="String"/>
|
||||
<attribute name="lastUpdated" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="title" attributeType="String"/>
|
||||
<attribute name="url" attributeType="URI"/>
|
||||
<relationship name="groups" toMany="YES" deletionRule="Nullify" destinationEntity="Group" inverseName="feeds" inverseEntity="Group"/>
|
||||
<relationship name="items" toMany="YES" deletionRule="Nullify" destinationEntity="Item" inverseName="feed" inverseEntity="Item"/>
|
||||
</entity>
|
||||
<entity name="Group" representedClassName="Group" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="id" attributeType="String"/>
|
||||
<attribute name="title" attributeType="String"/>
|
||||
<relationship name="feeds" toMany="YES" deletionRule="Nullify" destinationEntity="Feed" inverseName="groups" inverseEntity="Feed"/>
|
||||
</entity>
|
||||
<entity name="Item" representedClassName="Item" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="author" optional="YES" attributeType="String"/>
|
||||
<attribute name="content" optional="YES" attributeType="String"/>
|
||||
<attribute name="id" optional="YES" attributeType="String"/>
|
||||
<attribute name="published" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="read" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="title" optional="YES" attributeType="String"/>
|
||||
<attribute name="url" optional="YES" attributeType="URI"/>
|
||||
<relationship name="feed" maxCount="1" deletionRule="Nullify" destinationEntity="Feed" inverseName="items" inverseEntity="Feed"/>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="Group" positionX="-63" positionY="-18" width="128" height="74"/>
|
||||
<element name="Feed" positionX="-54" positionY="9" width="128" height="119"/>
|
||||
<element name="Item" positionX="-45" positionY="63" width="128" height="149"/>
|
||||
</elements>
|
||||
</model>
|
|
@ -45,6 +45,12 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
// 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.
|
||||
// This may occur due to temporary interruptions (ex. an incoming phone call).
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue