Compare commits

..

No commits in common. "e264e8842c2270f792ac080690aecc3db373df85" and "307299dd4d17d6bc6691d2dbf8dcf27008194d37" have entirely different histories.

13 changed files with 22 additions and 141 deletions

View File

@ -19,9 +19,8 @@ public class PersistentContainer: NSPersistentContainer, @unchecked Sendable {
public private(set) lazy var backgroundContext: NSManagedObjectContext = { public private(set) lazy var backgroundContext: NSManagedObjectContext = {
let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
// the background context needs to be parented directly to the PSC // todo: should the background context really be parented to the view context, or should they both be direct children of the PSC?
// if it's parented to the viewContext, it blocks the viewContext (and potentially the main thread) when it needs to look things up context.parent = self.viewContext
context.persistentStoreCoordinator = self.persistentStoreCoordinator
return context return context
}() }()

View File

@ -3,6 +3,6 @@
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>_XCCurrentVersionName</key> <key>_XCCurrentVersionName</key>
<string>Reader 2.xcdatamodel</string> <string>Reader.xcdatamodel</string>
</dict> </dict>
</plist> </plist>

View File

@ -1,39 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21279" systemVersion="22A5331f" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Feed" representedClassName="Feed" syncable="YES">
<attribute name="id" attributeType="String"/>
<attribute name="lastUpdated" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="title" attributeType="String"/>
<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="Cascade" destinationEntity="Item" inverseName="feed" inverseEntity="Item"/>
</entity>
<entity name="Group" representedClassName="Group" syncable="YES">
<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" versionHashModifier="5" syncable="YES">
<attribute name="author" optional="YES" attributeType="String"/>
<attribute name="content" optional="YES" attributeType="String"/>
<attribute name="excerpt" optional="YES" attributeType="String"/>
<attribute name="generatedExcerpt" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="id" attributeType="String"/>
<attribute name="needsReadStateSync" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="published" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<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"/>
<fetchIndex name="byID">
<fetchIndexElement property="id" type="Binary" order="ascending"/>
</fetchIndex>
<fetchIndex name="byRead">
<fetchIndexElement property="read" type="Binary" order="ascending"/>
</fetchIndex>
</entity>
<entity name="SyncState" representedClassName="SyncState" syncable="YES" codeGenerationType="class">
<attribute name="lastSync" attributeType="Date" usesScalarValueType="NO"/>
</entity>
<fetchRequest name="FetchRequest" entity="Item" predicateString="TRUEPREDICATE"/>
</model>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21279" systemVersion="22A5331f" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier=""> <model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21223.11" systemVersion="22A5266r" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Feed" representedClassName="Feed" syncable="YES"> <entity name="Feed" representedClassName="Feed" syncable="YES">
<attribute name="id" attributeType="String"/> <attribute name="id" attributeType="String"/>
<attribute name="lastUpdated" optional="YES" attributeType="Date" usesScalarValueType="NO"/> <attribute name="lastUpdated" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
@ -30,4 +30,10 @@
<attribute name="lastSync" attributeType="Date" usesScalarValueType="NO"/> <attribute name="lastSync" attributeType="Date" usesScalarValueType="NO"/>
</entity> </entity>
<fetchRequest name="FetchRequest" entity="Item" predicateString="TRUEPREDICATE"/> <fetchRequest name="FetchRequest" entity="Item" predicateString="TRUEPREDICATE"/>
<elements>
<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="194"/>
<element name="SyncState" positionX="-63" positionY="90" width="128" height="44"/>
</elements>
</model> </model>

View File

@ -75,18 +75,7 @@
argument = "-com.apple.CoreData.ConcurrencyDebug 1" argument = "-com.apple.CoreData.ConcurrencyDebug 1"
isEnabled = "YES"> isEnabled = "YES">
</CommandLineArgument> </CommandLineArgument>
<CommandLineArgument
argument = "-com.apple.CoreData.SQLDebug 1"
isEnabled = "NO">
</CommandLineArgument>
</CommandLineArguments> </CommandLineArguments>
<EnvironmentVariables>
<EnvironmentVariable
key = "SQLITE_AUTO_TRACE"
value = "1"
isEnabled = "NO">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Release" buildConfiguration = "Release"

View File

@ -104,7 +104,6 @@ class BackgroundManager: NSObject {
do { do {
try await fervorController.persistentContainer.upsertItems(items) try await fervorController.persistentContainer.upsertItems(items)
logger.info("Upserted \(items.count) items during background refresh") logger.info("Upserted \(items.count) items during background refresh")
logger.info("Most recent unread item: \(items.first(where: { $0.read == false })!.title!, privacy: .public)")
await WidgetHelper.updateWidgetData(fervorController: fervorController) await WidgetHelper.updateWidgetData(fervorController: fervorController)

View File

@ -9,7 +9,7 @@ import Foundation
import CoreData import CoreData
import Persistence import Persistence
enum ItemListType: Hashable, Equatable { enum ItemListType: Hashable {
case unread case unread
case all case all
case group(Group) case group(Group)
@ -52,9 +52,7 @@ enum ItemListType: Hashable, Equatable {
case .all: case .all:
return nil return nil
case .group(let group): case .group(let group):
// use the feed ids, because passing the NSSet of feeds into the predicate results in a multithreading violation if the request is used on a different context req.predicate = NSPredicate(format: "read = NO AND feed in %@", group.feeds!)
let feedIDs = group.feeds!.map { ($0 as! Feed).objectID }
req.predicate = NSPredicate(format: "read = NO AND feed in %@", feedIDs)
case .feed(let feed): case .feed(let feed):
req.predicate = NSPredicate(format: "read = NO AND feed = %@", feed) req.predicate = NSPredicate(format: "read = NO AND feed = %@", feed)
} }

View File

@ -45,11 +45,7 @@ class AppSplitViewController: UISplitViewController {
setViewController(sidebarNav, for: .primary) setViewController(sidebarNav, for: .primary)
secondaryNav = UINavigationController() secondaryNav = UINavigationController()
// the toggle sidebar button only appears if there's a navigation bar secondaryNav.isNavigationBarHidden = true
// so we just make always transparent, rather than disabling it
let secondaryNavBarAppearance = UINavigationBarAppearance()
secondaryNavBarAppearance.configureWithTransparentBackground()
secondaryNav.navigationBar.standardAppearance = secondaryNavBarAppearance
secondaryNav.view.backgroundColor = .appBackground secondaryNav.view.backgroundColor = .appBackground
setViewController(secondaryNav, for: .secondary) setViewController(secondaryNav, for: .secondary)

View File

@ -7,16 +7,9 @@
import UIKit import UIKit
import Fervor import Fervor
import Persistence
import OSLog
private let signposter = OSSignposter(subsystem: "net.shadowfacts.Reader", category: "HomeCollectionViewCell")
class HomeCollectionViewCell: UICollectionViewListCell { class HomeCollectionViewCell: UICollectionViewListCell {
private var currentItemCountTask: Task<Void, Never>?
private var itemCount: Int?
#if !targetEnvironment(macCatalyst) #if !targetEnvironment(macCatalyst)
override func updateConfiguration(using state: UICellConfigurationState) { override func updateConfiguration(using state: UICellConfigurationState) {
var backgroundConfig = UIBackgroundConfiguration.listGroupedCell().updated(for: state) var backgroundConfig = UIBackgroundConfiguration.listGroupedCell().updated(for: state)
@ -29,47 +22,4 @@ class HomeCollectionViewCell: UICollectionViewListCell {
} }
#endif #endif
override func prepareForReuse() {
super.prepareForReuse()
currentItemCountTask?.cancel()
itemCount = nil
}
func updateUI(item: ItemListType, persistentContainer: PersistentContainer) {
var config = UIListContentConfiguration.valueCell()
config.text = item.title
if let itemCount {
config.secondaryText = itemCount.formatted(.number)
}
config.secondaryTextProperties.color = .tintColor
self.contentConfiguration = config
currentItemCountTask = Task(priority: .userInitiated) {
let state = signposter.beginInterval("fetch count", id: signposter.makeSignpostID(), "\(String(item.hashValue, radix: 16), privacy: .public)")
if let count = await fetchCount(item: item, in: persistentContainer),
!Task.isCancelled {
self.itemCount = count
config.secondaryText = count.formatted(.number)
self.contentConfiguration = config
}
signposter.endInterval("fetch count", state)
}
}
private func fetchCount(item: ItemListType, in persistentContainer: PersistentContainer) async -> Int? {
guard let request = item.countFetchRequest else {
return nil
}
return await withCheckedContinuation({ continuation in
let state = signposter.beginInterval("waiting to perform", id: signposter.makeSignpostID(), "\(String(item.hashValue, radix: 16), privacy: .public)")
persistentContainer.performBackgroundTask { context in
signposter.endInterval("waiting to perform", state)
let state = signposter.beginInterval("count", id: signposter.makeSignpostID(), "\(String(item.hashValue, radix: 16), privacy: .public)")
let count = try? context.count(for: request)
signposter.endInterval("count", state)
continuation.resume(returning: count)
}
})
}
} }

View File

@ -126,8 +126,15 @@ class HomeViewController: UIViewController {
config.text = section.title config.text = section.title
supplementaryView.contentConfiguration = config supplementaryView.contentConfiguration = config
} }
let listCell = UICollectionView.CellRegistration<HomeCollectionViewCell, ItemListType> { [unowned self] cell, indexPath, item in let listCell = UICollectionView.CellRegistration<HomeCollectionViewCell, ItemListType> { cell, indexPath, item in
cell.updateUI(item: item, persistentContainer: self.fervorController.persistentContainer) var config = UIListContentConfiguration.valueCell()
config.text = item.title
if let req = item.countFetchRequest,
let count = try? self.fervorController.persistentContainer.viewContext.count(for: req) {
config.secondaryText = "\(count)"
config.secondaryTextProperties.color = .tintColor
}
cell.contentConfiguration = config
cell.accessories = [.disclosureIndicator()] cell.accessories = [.disclosureIndicator()]
} }

View File

@ -60,22 +60,6 @@ class ItemsViewController: UIViewController {
var configuration = UICollectionLayoutListConfiguration(appearance: .plain) var configuration = UICollectionLayoutListConfiguration(appearance: .plain)
configuration.backgroundColor = .clear configuration.backgroundColor = .clear
configuration.trailingSwipeActionsConfigurationProvider = { [unowned self] indexPath in
guard let itemID = self.dataSource.itemIdentifier(for: indexPath) else {
return nil
}
let item = fervorController.persistentContainer.viewContext.object(with: itemID) as! Item
let action = UIContextualAction(style: .normal, title: item.read ? "Mark as Unread" : "Mark as Read") { _, _, completion in
Task {
await self.fervorController.markItem(item, read: !item.read)
completion(true)
}
}
action.image = UIImage(systemName: item.read ? "checkmark.circle" : "checkmark.circle.fill")
return UISwipeActionsConfiguration(actions: [
action
])
}
let layout = UICollectionViewCompositionalLayout.list(using: configuration) let layout = UICollectionViewCompositionalLayout.list(using: configuration)
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout) collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
collectionView.delegate = self collectionView.delegate = self

View File

@ -72,10 +72,7 @@ class ReadViewController: UIViewController {
webView.isOpaque = false webView.isOpaque = false
webView.backgroundColor = .clear webView.backgroundColor = .clear
if #available(iOS 16.0, *) { if #available(iOS 16.0, *) {
// TODO: Xcode 14 RC doesn't have the macOS 13 SDK, so we can't use this
#if !targetEnvironment(macCatalyst)
webView.isFindInteractionEnabled = true webView.isFindInteractionEnabled = true
#endif
} }
if let content = itemContentHTML() { if let content = itemContentHTML() {
webView.loadHTMLString(content, baseURL: item.url) webView.loadHTMLString(content, baseURL: item.url)

View File

@ -11,7 +11,6 @@ import Persistence
import OSLog import OSLog
private let logger = Logger(subsystem: "net.shadowfacts.Reader", category: "WidgetHelper") private let logger = Logger(subsystem: "net.shadowfacts.Reader", category: "WidgetHelper")
private let signposter = OSSignposter(logger: logger)
struct WidgetHelper { struct WidgetHelper {
private init() {} private init() {}
@ -27,9 +26,7 @@ struct WidgetHelper {
req.sortDescriptors = [NSSortDescriptor(key: "published", ascending: false)] req.sortDescriptors = [NSSortDescriptor(key: "published", ascending: false)]
req.fetchLimit = 32 req.fetchLimit = 32
req.predicate = NSPredicate(format: "read = NO") req.predicate = NSPredicate(format: "read = NO")
let state = signposter.beginInterval("fetch")
var items = (try? context.fetch(req)) ?? [] var items = (try? context.fetch(req)) ?? []
signposter.endInterval("fetch", state)
var prioritizedItems: [Item] = [] var prioritizedItems: [Item] = []
@ -54,8 +51,6 @@ struct WidgetHelper {
} }
} }
logger.info("Saving widget data with first item: '\(prioritizedItems.first!.title ?? "<untitled>", privacy: .public)'")
WidgetData(recentItems: prioritizedItems).save(account: fervorController.account!) WidgetData(recentItems: prioritizedItems).save(account: fervorController.account!)
WidgetCenter.shared.reloadAllTimelines() WidgetCenter.shared.reloadAllTimelines()