Sync and show items
This commit is contained in:
parent
8acc303a80
commit
2b38b883fe
|
@ -10,7 +10,7 @@ import Foundation
|
|||
public struct Feed: Decodable {
|
||||
public let id: FervorID
|
||||
public let title: String
|
||||
public let url: URL
|
||||
public let url: URL?
|
||||
public let serviceURL: URL?
|
||||
public let feedURL: URL
|
||||
public let lastUpdated: Date
|
||||
|
@ -21,7 +21,7 @@ public struct Feed: Decodable {
|
|||
|
||||
self.id = try container.decode(FervorID.self, forKey: .id)
|
||||
self.title = try container.decode(String.self, forKey: .title)
|
||||
self.url = try container.decode(URL.self, forKey: .url)
|
||||
self.url = try container.decode(URL?.self, forKey: .url)
|
||||
self.serviceURL = try container.decodeIfPresent(URL.self, forKey: .serviceURL)
|
||||
self.feedURL = try container.decode(URL.self, forKey: .feedURL)
|
||||
self.lastUpdated = try container.decode(Date.self, forKey: .lastUpdated)
|
||||
|
|
|
@ -15,7 +15,22 @@ public class FervorClient {
|
|||
|
||||
private let decoder: JSONDecoder = {
|
||||
let d = JSONDecoder()
|
||||
d.dateDecodingStrategy = .iso8601
|
||||
let withFractionalSeconds = ISO8601DateFormatter()
|
||||
withFractionalSeconds.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||
let without = ISO8601DateFormatter()
|
||||
without.formatOptions = [.withInternetDateTime]
|
||||
// because fucking ISO8601DateFormatter isn't a DateFormatter
|
||||
d.dateDecodingStrategy = .custom({ decoder in
|
||||
let s = try decoder.singleValueContainer().decode(String.self)
|
||||
// try both because Elixir's DateTime.to_iso8601 omits the .0 if the date doesn't have fractional seconds
|
||||
if let d = withFractionalSeconds.date(from: s) {
|
||||
return d
|
||||
} else if let d = without.date(from: s) {
|
||||
return d
|
||||
} else {
|
||||
throw DateDecodingError()
|
||||
}
|
||||
})
|
||||
return d
|
||||
}()
|
||||
|
||||
|
@ -25,9 +40,10 @@ public class FervorClient {
|
|||
self.session = session
|
||||
}
|
||||
|
||||
private func buildURL(path: String) -> URL {
|
||||
private func buildURL(path: String, queryItems: [URLQueryItem] = []) -> URL {
|
||||
var components = URLComponents(url: instanceURL, resolvingAgainstBaseURL: false)!
|
||||
components.path = path
|
||||
components.queryItems = queryItems
|
||||
return components.url!
|
||||
}
|
||||
|
||||
|
@ -36,7 +52,10 @@ public class FervorClient {
|
|||
if let accessToken = accessToken {
|
||||
request.addValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
|
||||
}
|
||||
let (data, _) = try await session.data(for: request, delegate: nil)
|
||||
let (data, response) = try await session.data(for: request, delegate: nil)
|
||||
if (response as! HTTPURLResponse).statusCode == 404 {
|
||||
throw Error.notFound
|
||||
}
|
||||
let decoded = try decoder.decode(T.self, from: data)
|
||||
return decoded
|
||||
}
|
||||
|
@ -75,6 +94,31 @@ public class FervorClient {
|
|||
return try await performRequest(request)
|
||||
}
|
||||
|
||||
public func syncItems(lastSync: Date?) async throws -> ItemsSyncUpdate {
|
||||
let request = URLRequest(url: buildURL(path: "/api/v1/items/sync", queryItems: [
|
||||
URLQueryItem(name: "last_sync", value: lastSync?.formatted(.iso8601))
|
||||
]))
|
||||
return try await performRequest(request)
|
||||
}
|
||||
|
||||
public func items(feed id: FervorID) async throws -> [Item] {
|
||||
let request = URLRequest(url: buildURL(path: "/api/v1/feeds/\(id)/items"))
|
||||
return try await performRequest(request)
|
||||
}
|
||||
|
||||
public func item(id: FervorID) async throws -> Item? {
|
||||
let request = URLRequest(url: buildURL(path: "/api/v1/items/\(id)"))
|
||||
do {
|
||||
return try await performRequest(request)
|
||||
} catch {
|
||||
if let error = error as? Error, case .notFound = error {
|
||||
return nil
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct Auth {
|
||||
public let accessToken: String
|
||||
public let refreshToken: String?
|
||||
|
@ -83,6 +127,11 @@ public class FervorClient {
|
|||
public enum Error: Swift.Error {
|
||||
case urlSession(Swift.Error)
|
||||
case decode(Swift.Error)
|
||||
case notFound
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private struct DateDecodingError: Error {
|
||||
|
||||
}
|
||||
|
|
|
@ -8,17 +8,17 @@
|
|||
import Foundation
|
||||
|
||||
public struct Item: Decodable {
|
||||
let id: FervorID
|
||||
let feedID: FervorID
|
||||
let title: String
|
||||
let author: String
|
||||
let published: Date?
|
||||
let createdAt: Date?
|
||||
let content: String?
|
||||
let summary: String?
|
||||
let url: URL
|
||||
let serviceURL: URL?
|
||||
let read: Bool?
|
||||
public let id: FervorID
|
||||
public let feedID: FervorID
|
||||
public let title: String
|
||||
public let author: String
|
||||
public let published: Date?
|
||||
public let createdAt: Date?
|
||||
public let content: String?
|
||||
public let summary: String?
|
||||
public let url: URL
|
||||
public let serviceURL: URL?
|
||||
public let read: Bool?
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
//
|
||||
// ItemsSyncUpdate.swift
|
||||
// Fervor
|
||||
//
|
||||
// Created by Shadowfacts on 1/9/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct ItemsSyncUpdate: Decodable {
|
||||
|
||||
public let syncTimestamp: Date
|
||||
public let delete: [FervorID]
|
||||
public let upsert: [Item]
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
self.syncTimestamp = try container.decode(Date.self, forKey: .syncTimestamp)
|
||||
self.delete = try container.decode([FervorID].self, forKey: .delete)
|
||||
self.upsert = try container.decode([Item].self, forKey: .upsert)
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case syncTimestamp = "sync_timestamp"
|
||||
case delete
|
||||
case upsert
|
||||
}
|
||||
|
||||
}
|
|
@ -33,6 +33,9 @@
|
|||
D6C68834272CD44900874C10 /* Fervor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C68833272CD44900874C10 /* Fervor.swift */; };
|
||||
D6C68856272CD7C600874C10 /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C68855272CD7C600874C10 /* Item.swift */; };
|
||||
D6C68858272CD8CD00874C10 /* Group.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C68857272CD8CD00874C10 /* Group.swift */; };
|
||||
D6E2434C278B456A0005E546 /* ItemsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E2434B278B456A0005E546 /* ItemsViewController.swift */; };
|
||||
D6E24350278B62F60005E546 /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E2434F278B62F60005E546 /* Item.swift */; };
|
||||
D6E24352278B6DF90005E546 /* ItemsSyncUpdate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E24351278B6DF90005E546 /* ItemsSyncUpdate.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
|
@ -103,6 +106,9 @@
|
|||
D6C68833272CD44900874C10 /* Fervor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fervor.swift; sourceTree = "<group>"; };
|
||||
D6C68855272CD7C600874C10 /* Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Item.swift; sourceTree = "<group>"; };
|
||||
D6C68857272CD8CD00874C10 /* Group.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Group.swift; sourceTree = "<group>"; };
|
||||
D6E2434B278B456A0005E546 /* ItemsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemsViewController.swift; sourceTree = "<group>"; };
|
||||
D6E2434F278B62F60005E546 /* Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Item.swift; sourceTree = "<group>"; };
|
||||
D6E24351278B6DF90005E546 /* ItemsSyncUpdate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemsSyncUpdate.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
@ -143,6 +149,7 @@
|
|||
children = (
|
||||
D65B18BF2750533E004A9448 /* Home */,
|
||||
D65B18B027504691004A9448 /* Login */,
|
||||
D6E2434A278B455C0005E546 /* Items */,
|
||||
);
|
||||
path = Screens;
|
||||
sourceTree = "<group>";
|
||||
|
@ -207,6 +214,7 @@
|
|||
D6C687F9272CD27700874C10 /* LaunchScreen.storyboard */,
|
||||
D6C687FC272CD27700874C10 /* Info.plist */,
|
||||
D6C687F4272CD27600874C10 /* Reader.xcdatamodeld */,
|
||||
D6E2434F278B62F60005E546 /* Item.swift */,
|
||||
);
|
||||
path = Reader;
|
||||
sourceTree = "<group>";
|
||||
|
@ -240,11 +248,20 @@
|
|||
D6C68857272CD8CD00874C10 /* Group.swift */,
|
||||
D6C6882F272CD2CF00874C10 /* Instance.swift */,
|
||||
D6C68855272CD7C600874C10 /* Item.swift */,
|
||||
D6E24351278B6DF90005E546 /* ItemsSyncUpdate.swift */,
|
||||
D65B18BB27504FE7004A9448 /* Token.swift */,
|
||||
);
|
||||
path = Fervor;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D6E2434A278B455C0005E546 /* Items */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D6E2434B278B456A0005E546 /* ItemsViewController.swift */,
|
||||
);
|
||||
path = Items;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXHeadersBuildPhase section */
|
||||
|
@ -423,6 +440,8 @@
|
|||
D6C687EC272CD27600874C10 /* AppDelegate.swift in Sources */,
|
||||
D6C687F6272CD27600874C10 /* Reader.xcdatamodeld in Sources */,
|
||||
D6A8A33727766EA100CCEC72 /* ManagedObjectExtensions.swift in Sources */,
|
||||
D6E2434C278B456A0005E546 /* ItemsViewController.swift in Sources */,
|
||||
D6E24350278B62F60005E546 /* Item.swift in Sources */,
|
||||
D65B18BE275051A1004A9448 /* LocalData.swift in Sources */,
|
||||
D65B18B22750469D004A9448 /* LoginViewController.swift in Sources */,
|
||||
D65B18C127505348004A9448 /* HomeViewController.swift in Sources */,
|
||||
|
@ -454,6 +473,7 @@
|
|||
D65B18BC27504FE7004A9448 /* Token.swift in Sources */,
|
||||
D6C68856272CD7C600874C10 /* Item.swift in Sources */,
|
||||
D6C68830272CD2CF00874C10 /* Instance.swift in Sources */,
|
||||
D6E24352278B6DF90005E546 /* ItemsSyncUpdate.swift in Sources */,
|
||||
D6C68832272CD40600874C10 /* Feed.swift in Sources */,
|
||||
D6C68858272CD8CD00874C10 /* Group.swift in Sources */,
|
||||
D6C68834272CD44900874C10 /* Fervor.swift in Sources */,
|
||||
|
|
|
@ -36,3 +36,23 @@ extension Feed {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
extension Item {
|
||||
|
||||
func updateFromServer(_ serverItem: Fervor.Item) {
|
||||
guard self.id == nil || self.id == serverItem.id else { return }
|
||||
self.id = serverItem.id
|
||||
self.author = serverItem.author
|
||||
self.content = serverItem.content
|
||||
self.title = serverItem.title
|
||||
self.read = serverItem.read ?? false
|
||||
self.published = serverItem.published
|
||||
self.url = serverItem.url
|
||||
if self.feed?.id != serverItem.feedID {
|
||||
let feedReq = Feed.fetchRequest()
|
||||
feedReq.predicate = NSPredicate(format: "id = %@", serverItem.feedID)
|
||||
self.feed = try! self.managedObjectContext!.fetch(feedReq).first!
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import CoreData
|
||||
import Fervor
|
||||
import OSLog
|
||||
|
||||
class PersistentContainer: NSPersistentContainer {
|
||||
|
||||
|
@ -22,7 +23,13 @@ class PersistentContainer: NSPersistentContainer {
|
|||
return context
|
||||
}()
|
||||
|
||||
init(account: LocalData.Account) {
|
||||
private weak var fervorController: FervorController?
|
||||
|
||||
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "PersistentContainer")
|
||||
|
||||
init(account: LocalData.Account, fervorController: FervorController) {
|
||||
self.fervorController = fervorController
|
||||
|
||||
super.init(name: "\(account.id)", managedObjectModel: PersistentContainer.managedObjectModel)
|
||||
|
||||
loadPersistentStores { description, error in
|
||||
|
@ -32,6 +39,27 @@ class PersistentContainer: NSPersistentContainer {
|
|||
}
|
||||
}
|
||||
|
||||
func lastSyncDate() async throws -> Date? {
|
||||
return try await backgroundContext.perform {
|
||||
let state = try self.backgroundContext.fetch(SyncState.fetchRequest()).first
|
||||
return state?.lastSync
|
||||
}
|
||||
}
|
||||
|
||||
func updateLastSyncDate(_ date: Date) async throws {
|
||||
try await backgroundContext.perform {
|
||||
if let state = try self.backgroundContext.fetch(SyncState.fetchRequest()).first {
|
||||
state.lastSync = date
|
||||
} else {
|
||||
let state = SyncState(context: self.backgroundContext)
|
||||
state.lastSync = date
|
||||
}
|
||||
|
||||
try self.backgroundContext.save()
|
||||
try self.viewContext.save()
|
||||
}
|
||||
}
|
||||
|
||||
func sync(serverGroups: [Fervor.Group], serverFeeds: [Fervor.Feed]) async throws {
|
||||
try await backgroundContext.perform {
|
||||
let existingGroups = try self.backgroundContext.fetch(Group.fetchRequest())
|
||||
|
@ -64,4 +92,40 @@ class PersistentContainer: NSPersistentContainer {
|
|||
}
|
||||
}
|
||||
|
||||
func syncItems(_ syncUpdate: ItemsSyncUpdate) async throws {
|
||||
try await backgroundContext.perform {
|
||||
self.logger.debug("syncItems: deleting \(syncUpdate.delete.count, privacy: .public) items")
|
||||
let deleteReq = Item.fetchRequest()
|
||||
deleteReq.predicate = NSPredicate(format: "id in %@", syncUpdate.delete)
|
||||
let delete = NSBatchDeleteRequest(fetchRequest: deleteReq as! NSFetchRequest<NSFetchRequestResult>)
|
||||
delete.resultType = .resultTypeObjectIDs
|
||||
let result = try self.backgroundContext.execute(delete)
|
||||
|
||||
if let deleteResult = result as? NSBatchDeleteResult,
|
||||
let objectIDs = deleteResult.result as? [NSManagedObjectID] {
|
||||
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: [NSDeletedObjectsKey: objectIDs], into: [self.backgroundContext])
|
||||
// todo: does the background/view contexts need to get saved then?
|
||||
}
|
||||
|
||||
let req = Item.fetchRequest()
|
||||
req.predicate = NSPredicate(format: "id in %@", syncUpdate.upsert.map(\.id))
|
||||
let existing = try self.backgroundContext.fetch(req)
|
||||
self.logger.debug("syncItems: updating \(existing.count, privacy: .public) items, inserting \(syncUpdate.upsert.count - existing.count, privacy: .public)")
|
||||
// todo: this feels like it'll be slow when there are many items
|
||||
for item in syncUpdate.upsert {
|
||||
if let existing = existing.first(where: { $0.id == item.id }) {
|
||||
existing.updateFromServer(item)
|
||||
} else {
|
||||
let mo = Item(context: self.backgroundContext)
|
||||
mo.updateFromServer(item)
|
||||
}
|
||||
}
|
||||
|
||||
if self.backgroundContext.hasChanges {
|
||||
try self.backgroundContext.save()
|
||||
try self.viewContext.save()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import Foundation
|
||||
import Fervor
|
||||
import OSLog
|
||||
|
||||
class FervorController {
|
||||
|
||||
|
@ -14,7 +15,9 @@ class FervorController {
|
|||
|
||||
let instanceURL: URL
|
||||
|
||||
private let client: FervorClient
|
||||
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "FervorController")
|
||||
|
||||
let client: FervorClient
|
||||
private(set) var clientID: String?
|
||||
private(set) var clientSecret: String?
|
||||
private(set) var accessToken: String?
|
||||
|
@ -34,7 +37,7 @@ class FervorController {
|
|||
|
||||
self.client.accessToken = account.accessToken
|
||||
|
||||
self.persistentContainer = PersistentContainer(account: account)
|
||||
self.persistentContainer = PersistentContainer(account: account, fervorController: self)
|
||||
}
|
||||
|
||||
func register() async throws -> ClientRegistration {
|
||||
|
@ -50,10 +53,17 @@ class FervorController {
|
|||
accessToken = token.accessToken
|
||||
}
|
||||
|
||||
func syncGroupsAndFeeds() async {
|
||||
func syncAll() async {
|
||||
logger.info("Syncing groups and feeds")
|
||||
async let groups = try! client.groups()
|
||||
async let feeds = try! client.feeds()
|
||||
try! await persistentContainer.sync(serverGroups: groups, serverFeeds: feeds)
|
||||
|
||||
let lastSync = try! await persistentContainer.lastSyncDate()
|
||||
logger.info("Syncing items with last sync date: \(String(describing: lastSync), privacy: .public)")
|
||||
let update = try! await client.syncItems(lastSync: lastSync)
|
||||
try! await persistentContainer.syncItems(update)
|
||||
try! await persistentContainer.updateLastSyncDate(update.syncTimestamp)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
//
|
||||
// Item.swift
|
||||
// Reader
|
||||
//
|
||||
// Created by Shadowfacts on 1/9/22.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
|
||||
@objc(Item)
|
||||
public final class Item: NSManagedObject {
|
||||
|
||||
@nonobjc public class func fetchRequest() -> NSFetchRequest<Item> {
|
||||
return NSFetchRequest<Item>(entityName: "Item")
|
||||
}
|
||||
|
||||
@NSManaged public var author: String?
|
||||
@NSManaged public var content: String?
|
||||
@NSManaged public var id: String?
|
||||
@NSManaged public var title: String?
|
||||
@NSManaged public var read: Bool
|
||||
@NSManaged public var published: Date?
|
||||
@NSManaged public var url: URL?
|
||||
|
||||
@NSManaged public var feed: Feed?
|
||||
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
<attribute name="id" attributeType="String"/>
|
||||
<attribute name="lastUpdated" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="title" attributeType="String"/>
|
||||
<attribute name="url" attributeType="URI"/>
|
||||
<attribute name="url" optional="YES" 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>
|
||||
|
@ -13,19 +13,23 @@
|
|||
<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">
|
||||
<entity name="Item" representedClassName="Item" syncable="YES">
|
||||
<attribute name="author" optional="YES" attributeType="String"/>
|
||||
<attribute name="content" optional="YES" attributeType="String"/>
|
||||
<attribute name="id" optional="YES" attributeType="String"/>
|
||||
<attribute name="id" attributeType="String"/>
|
||||
<attribute name="published" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="read" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="read" 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>
|
||||
<entity name="SyncState" representedClassName="SyncState" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="lastSync" attributeType="Date" usesScalarValueType="NO"/>
|
||||
</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="Group" positionX="-63" positionY="-18" width="128" height="74"/>
|
||||
<element name="Item" positionX="-45" positionY="63" width="128" height="149"/>
|
||||
<element name="SyncState" positionX="-63" positionY="90" width="128" height="44"/>
|
||||
</elements>
|
||||
</model>
|
|
@ -70,7 +70,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|||
window!.rootViewController = nav
|
||||
|
||||
Task(priority: .userInitiated) {
|
||||
await self.fervorController.syncGroupsAndFeeds()
|
||||
await self.fervorController.syncAll()
|
||||
await self.fetchFeeds()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import UIKit
|
||||
import CoreData
|
||||
|
||||
class HomeViewController: UIViewController, UICollectionViewDelegate {
|
||||
class HomeViewController: UIViewController {
|
||||
|
||||
let fervorController: FervorController
|
||||
|
||||
|
@ -50,8 +50,6 @@ class HomeViewController: UIViewController, UICollectionViewDelegate {
|
|||
|
||||
dataSource = createDataSource()
|
||||
|
||||
// todo: maybe NSFetchedResultsController to automatically update when the database changes?
|
||||
// applyInitialSnapshot()
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
snapshot.appendSections([.groups, .feeds])
|
||||
dataSource.apply(snapshot, animatingDifferences: false)
|
||||
|
@ -83,17 +81,6 @@ class HomeViewController: UIViewController, UICollectionViewDelegate {
|
|||
return dataSource
|
||||
}
|
||||
|
||||
// private func applyInitialSnapshot() {
|
||||
// let groups = try! fervorController.persistentContainer.viewContext.fetch(Group.fetchRequest())
|
||||
// let feeds = try! fervorController.persistentContainer.viewContext.fetch(Feed.fetchRequest())
|
||||
//
|
||||
// var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
// snapshot.appendSections([.groups, .feeds])
|
||||
// snapshot.appendItems(groups.map { .group($0) }, toSection: .groups)
|
||||
// snapshot.appendItems(feeds.map { .feed($0) }, toSection: .feeds)
|
||||
// dataSource.apply(snapshot, animatingDifferences: false)
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
extension HomeViewController {
|
||||
|
@ -134,3 +121,20 @@ extension HomeViewController: NSFetchedResultsControllerDelegate {
|
|||
dataSource.apply(snapshot, animatingDifferences: false)
|
||||
}
|
||||
}
|
||||
|
||||
extension HomeViewController: UICollectionViewDelegate {
|
||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
collectionView.deselectItem(at: indexPath, animated: true)
|
||||
guard let item = dataSource.itemIdentifier(for: indexPath) else {
|
||||
return
|
||||
}
|
||||
let req = Reader.Item.fetchRequest()
|
||||
switch item {
|
||||
case .group(let group):
|
||||
req.predicate = NSPredicate(format: "feed in %@", group.feeds!)
|
||||
case .feed(let feed):
|
||||
req.predicate = NSPredicate(format: "feed = %@", feed)
|
||||
}
|
||||
show(ItemsViewController(fervorController: fervorController, fetchRequest: req), sender: nil)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
//
|
||||
// ItemsViewController.swift
|
||||
// Reader
|
||||
//
|
||||
// Created by Shadowfacts on 1/9/22.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import CoreData
|
||||
|
||||
class ItemsViewController: UIViewController {
|
||||
|
||||
let fervorController: FervorController
|
||||
let fetchRequest: NSFetchRequest<Item>
|
||||
|
||||
private var collectionView: UICollectionView!
|
||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||
private var resultsController: NSFetchedResultsController<Item>!
|
||||
|
||||
init(fervorController: FervorController, fetchRequest: NSFetchRequest<Item>) {
|
||||
self.fervorController = fervorController
|
||||
self.fetchRequest = fetchRequest
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
let configuration = UICollectionLayoutListConfiguration(appearance: .plain)
|
||||
let layout = UICollectionViewCompositionalLayout.list(using: configuration)
|
||||
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
|
||||
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
view.addSubview(collectionView)
|
||||
|
||||
dataSource = createDataSource()
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
snapshot.appendSections([.items])
|
||||
dataSource.apply(snapshot, animatingDifferences: false)
|
||||
|
||||
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "published", ascending: false)]
|
||||
resultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: fervorController.persistentContainer.viewContext, sectionNameKeyPath: nil, cacheName: nil)
|
||||
resultsController.delegate = self
|
||||
try! resultsController.performFetch()
|
||||
}
|
||||
|
||||
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
|
||||
let listCell = UICollectionView.CellRegistration<UICollectionViewListCell, Item> { cell, indexPath, item in
|
||||
var config = cell.defaultContentConfiguration()
|
||||
config.text = item.title
|
||||
cell.contentConfiguration = config
|
||||
|
||||
cell.accessories = [.disclosureIndicator()]
|
||||
}
|
||||
let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, item in
|
||||
return collectionView.dequeueConfiguredReusableCell(using: listCell, for: indexPath, item: item)
|
||||
}
|
||||
return dataSource
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ItemsViewController {
|
||||
enum Section: Hashable {
|
||||
case items
|
||||
}
|
||||
}
|
||||
|
||||
extension ItemsViewController: NSFetchedResultsControllerDelegate {
|
||||
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
|
||||
var snapshot = self.dataSource.snapshot()
|
||||
snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .items))
|
||||
// use resultsController here instead of controller so we don't have to cast
|
||||
snapshot.appendItems(resultsController.fetchedObjects!, toSection: .items)
|
||||
self.dataSource.apply(snapshot, animatingDifferences: false)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue