mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
Improves error handling when fetching multiple sources
Fetching sources is no longer all or nothing. Now if a source cannot be fetched, it won’t prevent other sources from being updated.
This commit is contained in:
@@ -159,6 +159,7 @@
|
|||||||
BF770E6922BD57DD002A40FE /* Silence.m4a in Resources */ = {isa = PBXBuildFile; fileRef = BF770E6822BD57DD002A40FE /* Silence.m4a */; };
|
BF770E6922BD57DD002A40FE /* Silence.m4a in Resources */ = {isa = PBXBuildFile; fileRef = BF770E6822BD57DD002A40FE /* Silence.m4a */; };
|
||||||
BF7C627223DBB3B400515A2D /* AltStore2ToAltStore3.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = BF7C627123DBB3B400515A2D /* AltStore2ToAltStore3.xcmappingmodel */; };
|
BF7C627223DBB3B400515A2D /* AltStore2ToAltStore3.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = BF7C627123DBB3B400515A2D /* AltStore2ToAltStore3.xcmappingmodel */; };
|
||||||
BF7C627423DBB78C00515A2D /* InstalledAppPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7C627323DBB78C00515A2D /* InstalledAppPolicy.swift */; };
|
BF7C627423DBB78C00515A2D /* InstalledAppPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7C627323DBB78C00515A2D /* InstalledAppPolicy.swift */; };
|
||||||
|
BF88F97224F8727D00BB75DF /* AppManagerErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF88F97124F8727D00BB75DF /* AppManagerErrors.swift */; };
|
||||||
BF8CAE452489E772004D6CCE /* AnisetteDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8CAE422489E772004D6CCE /* AnisetteDataManager.swift */; };
|
BF8CAE452489E772004D6CCE /* AnisetteDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8CAE422489E772004D6CCE /* AnisetteDataManager.swift */; };
|
||||||
BF8CAE462489E772004D6CCE /* AppManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8CAE432489E772004D6CCE /* AppManager.swift */; };
|
BF8CAE462489E772004D6CCE /* AppManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8CAE432489E772004D6CCE /* AppManager.swift */; };
|
||||||
BF8CAE472489E772004D6CCE /* RequestHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8CAE442489E772004D6CCE /* RequestHandler.swift */; };
|
BF8CAE472489E772004D6CCE /* RequestHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8CAE442489E772004D6CCE /* RequestHandler.swift */; };
|
||||||
@@ -525,6 +526,7 @@
|
|||||||
BF7C627023DBB33300515A2D /* AltStore 3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 3.xcdatamodel"; sourceTree = "<group>"; };
|
BF7C627023DBB33300515A2D /* AltStore 3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 3.xcdatamodel"; sourceTree = "<group>"; };
|
||||||
BF7C627123DBB3B400515A2D /* AltStore2ToAltStore3.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore2ToAltStore3.xcmappingmodel; sourceTree = "<group>"; };
|
BF7C627123DBB3B400515A2D /* AltStore2ToAltStore3.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore2ToAltStore3.xcmappingmodel; sourceTree = "<group>"; };
|
||||||
BF7C627323DBB78C00515A2D /* InstalledAppPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledAppPolicy.swift; sourceTree = "<group>"; };
|
BF7C627323DBB78C00515A2D /* InstalledAppPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledAppPolicy.swift; sourceTree = "<group>"; };
|
||||||
|
BF88F97124F8727D00BB75DF /* AppManagerErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppManagerErrors.swift; sourceTree = "<group>"; };
|
||||||
BF8CAE422489E772004D6CCE /* AnisetteDataManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnisetteDataManager.swift; sourceTree = "<group>"; };
|
BF8CAE422489E772004D6CCE /* AnisetteDataManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnisetteDataManager.swift; sourceTree = "<group>"; };
|
||||||
BF8CAE432489E772004D6CCE /* AppManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppManager.swift; sourceTree = "<group>"; };
|
BF8CAE432489E772004D6CCE /* AppManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppManager.swift; sourceTree = "<group>"; };
|
||||||
BF8CAE442489E772004D6CCE /* RequestHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestHandler.swift; sourceTree = "<group>"; };
|
BF8CAE442489E772004D6CCE /* RequestHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestHandler.swift; sourceTree = "<group>"; };
|
||||||
@@ -1211,6 +1213,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
BF0C4EBC22A1BD8B009A2DD7 /* AppManager.swift */,
|
BF0C4EBC22A1BD8B009A2DD7 /* AppManager.swift */,
|
||||||
|
BF88F97124F8727D00BB75DF /* AppManagerErrors.swift */,
|
||||||
);
|
);
|
||||||
path = "Managing Apps";
|
path = "Managing Apps";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -2069,6 +2072,7 @@
|
|||||||
BF100C50232D7CD1006A8926 /* AltStoreToAltStore2.xcmappingmodel in Sources */,
|
BF100C50232D7CD1006A8926 /* AltStoreToAltStore2.xcmappingmodel in Sources */,
|
||||||
BF3D64B022E8D4B800E9056B /* AppContentViewControllerCells.swift in Sources */,
|
BF3D64B022E8D4B800E9056B /* AppContentViewControllerCells.swift in Sources */,
|
||||||
BFC57A6E2416FC5D00EB891E /* InstalledAppsCollectionHeaderView.swift in Sources */,
|
BFC57A6E2416FC5D00EB891E /* InstalledAppsCollectionHeaderView.swift in Sources */,
|
||||||
|
BF88F97224F8727D00BB75DF /* AppManagerErrors.swift in Sources */,
|
||||||
BF6C8FAE2429597900125131 /* BannerCollectionViewCell.swift in Sources */,
|
BF6C8FAE2429597900125131 /* BannerCollectionViewCell.swift in Sources */,
|
||||||
BFBBE2DD22931B20002097FA /* AltStore.xcdatamodeld in Sources */,
|
BFBBE2DD22931B20002097FA /* AltStore.xcdatamodeld in Sources */,
|
||||||
BF56D2AA23DF88310006506D /* AppID.swift in Sources */,
|
BF56D2AA23DF88310006506D /* AppID.swift in Sources */,
|
||||||
|
|||||||
@@ -302,13 +302,11 @@ private extension AppDelegate
|
|||||||
dispatchGroup.enter()
|
dispatchGroup.enter()
|
||||||
|
|
||||||
AppManager.shared.fetchSources() { (result) in
|
AppManager.shared.fetchSources() { (result) in
|
||||||
fetchSourcesResult = result
|
fetchSourcesResult = result.map { $0.0 }.mapError { $0 as Error }
|
||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
let sources = try result.get()
|
let (_, context) = try result.get()
|
||||||
|
|
||||||
guard let context = sources.first?.managedObjectContext else { return }
|
|
||||||
|
|
||||||
let previousUpdatesFetchRequest = InstalledApp.updatesFetchRequest() as! NSFetchRequest<NSFetchRequestResult>
|
let previousUpdatesFetchRequest = InstalledApp.updatesFetchRequest() as! NSFetchRequest<NSFetchRequestResult>
|
||||||
previousUpdatesFetchRequest.includesPendingChanges = false
|
previousUpdatesFetchRequest.includesPendingChanges = false
|
||||||
|
|||||||
@@ -178,20 +178,26 @@ private extension BrowseViewController
|
|||||||
AppManager.shared.fetchSources() { (result) in
|
AppManager.shared.fetchSources() { (result) in
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
let sources = try result.get()
|
do
|
||||||
try sources.first?.managedObjectContext?.save()
|
{
|
||||||
|
let (_, context) = try result.get()
|
||||||
DispatchQueue.main.async {
|
try context.save()
|
||||||
self.loadingState = .finished(.success(()))
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.loadingState = .finished(.success(()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch let error as AppManager.FetchSourcesError
|
||||||
|
{
|
||||||
|
try error.managedObjectContext?.save()
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch let error as NSError
|
catch
|
||||||
{
|
{
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if self.dataSource.itemCount > 0
|
if self.dataSource.itemCount > 0
|
||||||
{
|
{
|
||||||
let error = error.withLocalizedFailure(NSLocalizedString("Failed to Fetch Sources", comment: ""))
|
|
||||||
|
|
||||||
let toastView = ToastView(error: error)
|
let toastView = ToastView(error: error)
|
||||||
toastView.show(in: self)
|
toastView.show(in: self)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ class ToastView: RSTToastView
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
text = error.localizedDescription
|
text = error.localizedDescription
|
||||||
detailText = underlyingError?.localizedDescription
|
detailText = underlyingError?.localizedDescription ?? error.localizedRecoverySuggestion
|
||||||
}
|
}
|
||||||
|
|
||||||
self.init(text: text, detailText: detailText)
|
self.init(text: text, detailText: detailText)
|
||||||
|
|||||||
@@ -197,15 +197,16 @@ extension AppManager
|
|||||||
self.run([fetchSourceOperation], context: nil)
|
self.run([fetchSourceOperation], context: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchSources(completionHandler: @escaping (Result<Set<Source>, Error>) -> Void)
|
func fetchSources(completionHandler: @escaping (Result<(Set<Source>, NSManagedObjectContext), FetchSourcesError>) -> Void)
|
||||||
{
|
{
|
||||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||||
let sources = Source.all(in: context)
|
let sources = Source.all(in: context)
|
||||||
guard !sources.isEmpty else { return completionHandler(.failure(OperationError.noSources)) }
|
guard !sources.isEmpty else { return completionHandler(.failure(.init(OperationError.noSources))) }
|
||||||
|
|
||||||
let dispatchGroup = DispatchGroup()
|
let dispatchGroup = DispatchGroup()
|
||||||
var fetchedSources = Set<Source>()
|
var fetchedSources = Set<Source>()
|
||||||
var error: Error?
|
|
||||||
|
var errors = [Source: Error]()
|
||||||
|
|
||||||
let managedObjectContext = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
let managedObjectContext = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
||||||
|
|
||||||
@@ -216,8 +217,10 @@ extension AppManager
|
|||||||
fetchSourceOperation.resultHandler = { (result) in
|
fetchSourceOperation.resultHandler = { (result) in
|
||||||
switch result
|
switch result
|
||||||
{
|
{
|
||||||
case .failure(let e): error = e
|
|
||||||
case .success(let source): fetchedSources.insert(source)
|
case .success(let source): fetchedSources.insert(source)
|
||||||
|
case .failure(let error):
|
||||||
|
let source = managedObjectContext.object(with: source.objectID) as! Source
|
||||||
|
errors[source] = error
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatchGroup.leave()
|
dispatchGroup.leave()
|
||||||
@@ -227,14 +230,15 @@ extension AppManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
dispatchGroup.notify(queue: .global()) {
|
dispatchGroup.notify(queue: .global()) {
|
||||||
if let error = error
|
managedObjectContext.perform {
|
||||||
{
|
if !errors.isEmpty
|
||||||
completionHandler(.failure(error))
|
{
|
||||||
}
|
let sources = Set(sources.compactMap { managedObjectContext.object(with: $0.objectID) as? Source })
|
||||||
else
|
completionHandler(.failure(.init(sources: sources, errors: errors, context: managedObjectContext)))
|
||||||
{
|
}
|
||||||
managedObjectContext.perform {
|
else
|
||||||
completionHandler(.success(fetchedSources))
|
{
|
||||||
|
completionHandler(.success((fetchedSources, managedObjectContext)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
84
AltStore/Managing Apps/AppManagerErrors.swift
Normal file
84
AltStore/Managing Apps/AppManagerErrors.swift
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
//
|
||||||
|
// AppManagerErrors.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 8/27/20.
|
||||||
|
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
extension AppManager
|
||||||
|
{
|
||||||
|
struct FetchSourcesError: LocalizedError, CustomNSError
|
||||||
|
{
|
||||||
|
var primaryError: Error?
|
||||||
|
|
||||||
|
var sources: Set<Source>?
|
||||||
|
var errors = [Source: Error]()
|
||||||
|
|
||||||
|
var managedObjectContext: NSManagedObjectContext?
|
||||||
|
|
||||||
|
var errorDescription: String? {
|
||||||
|
if let error = self.primaryError
|
||||||
|
{
|
||||||
|
return error.localizedDescription
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var localizedDescription: String?
|
||||||
|
|
||||||
|
self.managedObjectContext?.performAndWait {
|
||||||
|
if self.sources?.count == 1
|
||||||
|
{
|
||||||
|
localizedDescription = NSLocalizedString("Could not refresh store.", comment: "")
|
||||||
|
}
|
||||||
|
else if self.errors.count == 1
|
||||||
|
{
|
||||||
|
guard let source = self.errors.keys.first else { return }
|
||||||
|
localizedDescription = String(format: NSLocalizedString("Could not refresh source “%@”.", comment: ""), source.name)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
localizedDescription = String(format: NSLocalizedString("Could not refresh %@ sources.", comment: ""), NSNumber(value: self.errors.count))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return localizedDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var recoverySuggestion: String? {
|
||||||
|
if let error = self.primaryError as NSError?
|
||||||
|
{
|
||||||
|
return error.localizedRecoverySuggestion
|
||||||
|
}
|
||||||
|
else if self.errors.count == 1
|
||||||
|
{
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return NSLocalizedString("Tap to view source errors.", comment: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var errorUserInfo: [String : Any] {
|
||||||
|
guard let error = self.errors.values.first, self.errors.count == 1 else { return [:] }
|
||||||
|
return [NSUnderlyingErrorKey: error]
|
||||||
|
}
|
||||||
|
|
||||||
|
init(_ error: Error)
|
||||||
|
{
|
||||||
|
self.primaryError = error
|
||||||
|
}
|
||||||
|
|
||||||
|
init(sources: Set<Source>, errors: [Source: Error], context: NSManagedObjectContext)
|
||||||
|
{
|
||||||
|
self.sources = sources
|
||||||
|
self.errors = errors
|
||||||
|
self.managedObjectContext = context
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -67,15 +67,25 @@ class AppPermission: NSManagedObject, Decodable, Fetchable
|
|||||||
{
|
{
|
||||||
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
|
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
|
||||||
|
|
||||||
super.init(entity: AppPermission.entity(), insertInto: nil)
|
super.init(entity: AppPermission.entity(), insertInto: context)
|
||||||
|
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
do
|
||||||
self.usageDescription = try container.decode(String.self, forKey: .usageDescription)
|
{
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
let rawType = try container.decode(String.self, forKey: .type)
|
self.usageDescription = try container.decode(String.self, forKey: .usageDescription)
|
||||||
self.type = ALTAppPermissionType(rawValue: rawType)
|
|
||||||
|
let rawType = try container.decode(String.self, forKey: .type)
|
||||||
context.insert(self)
|
self.type = ALTAppPermissionType(rawValue: rawType)
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
if let context = self.managedObjectContext
|
||||||
|
{
|
||||||
|
context.delete(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -88,52 +88,60 @@ class Source: NSManagedObject, Fetchable, Decodable
|
|||||||
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
|
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
|
||||||
guard let sourceURL = decoder.sourceURL else { preconditionFailure("Decoder must have non-nil sourceURL.") }
|
guard let sourceURL = decoder.sourceURL else { preconditionFailure("Decoder must have non-nil sourceURL.") }
|
||||||
|
|
||||||
super.init(entity: Source.entity(), insertInto: nil)
|
super.init(entity: Source.entity(), insertInto: context)
|
||||||
|
|
||||||
self.sourceURL = sourceURL
|
do
|
||||||
|
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
||||||
self.name = try container.decode(String.self, forKey: .name)
|
|
||||||
self.identifier = try container.decode(String.self, forKey: .identifier)
|
|
||||||
|
|
||||||
let userInfo = try container.decodeIfPresent([String: String].self, forKey: .userInfo)
|
|
||||||
self.userInfo = userInfo?.reduce(into: [:]) { $0[ALTSourceUserInfoKey($1.key)] = $1.value }
|
|
||||||
|
|
||||||
let apps = try container.decodeIfPresent([StoreApp].self, forKey: .apps) ?? []
|
|
||||||
let appsByID = Dictionary(apps.map { ($0.bundleIdentifier, $0) }, uniquingKeysWith: { (a, b) in return a })
|
|
||||||
|
|
||||||
for (index, app) in apps.enumerated()
|
|
||||||
{
|
{
|
||||||
app.sourceIdentifier = self.identifier
|
self.sourceURL = sourceURL
|
||||||
app.sortIndex = Int32(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
let newsItems = try container.decodeIfPresent([NewsItem].self, forKey: .news) ?? []
|
|
||||||
for (index, item) in newsItems.enumerated()
|
|
||||||
{
|
|
||||||
item.sourceIdentifier = self.identifier
|
|
||||||
item.sortIndex = Int32(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
context.insert(self)
|
|
||||||
|
|
||||||
for newsItem in newsItems
|
|
||||||
{
|
|
||||||
guard let appID = newsItem.appID else { continue }
|
|
||||||
|
|
||||||
if let storeApp = appsByID[appID]
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
self.name = try container.decode(String.self, forKey: .name)
|
||||||
|
self.identifier = try container.decode(String.self, forKey: .identifier)
|
||||||
|
|
||||||
|
let userInfo = try container.decodeIfPresent([String: String].self, forKey: .userInfo)
|
||||||
|
self.userInfo = userInfo?.reduce(into: [:]) { $0[ALTSourceUserInfoKey($1.key)] = $1.value }
|
||||||
|
|
||||||
|
let apps = try container.decodeIfPresent([StoreApp].self, forKey: .apps) ?? []
|
||||||
|
let appsByID = Dictionary(apps.map { ($0.bundleIdentifier, $0) }, uniquingKeysWith: { (a, b) in return a })
|
||||||
|
|
||||||
|
for (index, app) in apps.enumerated()
|
||||||
{
|
{
|
||||||
newsItem.storeApp = storeApp
|
app.sourceIdentifier = self.identifier
|
||||||
|
app.sortIndex = Int32(index)
|
||||||
}
|
}
|
||||||
else
|
self._apps = NSMutableOrderedSet(array: apps)
|
||||||
|
|
||||||
|
let newsItems = try container.decodeIfPresent([NewsItem].self, forKey: .news) ?? []
|
||||||
|
for (index, item) in newsItems.enumerated()
|
||||||
{
|
{
|
||||||
newsItem.storeApp = nil
|
item.sourceIdentifier = self.identifier
|
||||||
|
item.sortIndex = Int32(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for newsItem in newsItems
|
||||||
|
{
|
||||||
|
guard let appID = newsItem.appID else { continue }
|
||||||
|
|
||||||
|
if let storeApp = appsByID[appID]
|
||||||
|
{
|
||||||
|
newsItem.storeApp = storeApp
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
newsItem.storeApp = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self._newsItems = NSMutableOrderedSet(array: newsItems)
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
if let context = self.managedObjectContext
|
||||||
|
{
|
||||||
|
context.delete(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Must assign after we're inserted into context.
|
|
||||||
self._apps = NSMutableOrderedSet(array: apps)
|
|
||||||
self._newsItems = NSMutableOrderedSet(array: newsItems)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -104,43 +104,52 @@ class StoreApp: NSManagedObject, Decodable, Fetchable
|
|||||||
{
|
{
|
||||||
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
|
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
|
||||||
|
|
||||||
super.init(entity: StoreApp.entity(), insertInto: nil)
|
// Must initialize with context in order for child context saves to work correctly.
|
||||||
|
super.init(entity: StoreApp.entity(), insertInto: context)
|
||||||
|
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
do
|
||||||
self.name = try container.decode(String.self, forKey: .name)
|
|
||||||
self.bundleIdentifier = try container.decode(String.self, forKey: .bundleIdentifier)
|
|
||||||
self.developerName = try container.decode(String.self, forKey: .developerName)
|
|
||||||
self.localizedDescription = try container.decode(String.self, forKey: .localizedDescription)
|
|
||||||
|
|
||||||
self.subtitle = try container.decodeIfPresent(String.self, forKey: .subtitle)
|
|
||||||
|
|
||||||
self.version = try container.decode(String.self, forKey: .version)
|
|
||||||
self.versionDate = try container.decode(Date.self, forKey: .versionDate)
|
|
||||||
self.versionDescription = try container.decodeIfPresent(String.self, forKey: .versionDescription)
|
|
||||||
|
|
||||||
self.iconURL = try container.decode(URL.self, forKey: .iconURL)
|
|
||||||
self.screenshotURLs = try container.decodeIfPresent([URL].self, forKey: .screenshotURLs) ?? []
|
|
||||||
|
|
||||||
self.downloadURL = try container.decode(URL.self, forKey: .downloadURL)
|
|
||||||
|
|
||||||
if let tintColorHex = try container.decodeIfPresent(String.self, forKey: .tintColor)
|
|
||||||
{
|
{
|
||||||
guard let tintColor = UIColor(hexString: tintColorHex) else {
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
throw DecodingError.dataCorruptedError(forKey: .tintColor, in: container, debugDescription: "Hex code is invalid.")
|
self.name = try container.decode(String.self, forKey: .name)
|
||||||
|
self.bundleIdentifier = try container.decode(String.self, forKey: .bundleIdentifier)
|
||||||
|
self.developerName = try container.decode(String.self, forKey: .developerName)
|
||||||
|
self.localizedDescription = try container.decode(String.self, forKey: .localizedDescription)
|
||||||
|
|
||||||
|
self.subtitle = try container.decodeIfPresent(String.self, forKey: .subtitle)
|
||||||
|
|
||||||
|
self.version = try container.decode(String.self, forKey: .version)
|
||||||
|
self.versionDate = try container.decode(Date.self, forKey: .versionDate)
|
||||||
|
self.versionDescription = try container.decodeIfPresent(String.self, forKey: .versionDescription)
|
||||||
|
|
||||||
|
self.iconURL = try container.decode(URL.self, forKey: .iconURL)
|
||||||
|
self.screenshotURLs = try container.decodeIfPresent([URL].self, forKey: .screenshotURLs) ?? []
|
||||||
|
|
||||||
|
self.downloadURL = try container.decode(URL.self, forKey: .downloadURL)
|
||||||
|
|
||||||
|
if let tintColorHex = try container.decodeIfPresent(String.self, forKey: .tintColor)
|
||||||
|
{
|
||||||
|
guard let tintColor = UIColor(hexString: tintColorHex) else {
|
||||||
|
throw DecodingError.dataCorruptedError(forKey: .tintColor, in: container, debugDescription: "Hex code is invalid.")
|
||||||
|
}
|
||||||
|
|
||||||
|
self.tintColor = tintColor
|
||||||
}
|
}
|
||||||
|
|
||||||
self.tintColor = tintColor
|
self.size = try container.decode(Int32.self, forKey: .size)
|
||||||
|
self.isBeta = try container.decodeIfPresent(Bool.self, forKey: .isBeta) ?? false
|
||||||
|
|
||||||
|
let permissions = try container.decodeIfPresent([AppPermission].self, forKey: .permissions) ?? []
|
||||||
|
self._permissions = NSOrderedSet(array: permissions)
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
if let context = self.managedObjectContext
|
||||||
|
{
|
||||||
|
context.delete(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
self.size = try container.decode(Int32.self, forKey: .size)
|
|
||||||
self.isBeta = try container.decodeIfPresent(Bool.self, forKey: .isBeta) ?? false
|
|
||||||
|
|
||||||
let permissions = try container.decodeIfPresent([AppPermission].self, forKey: .permissions) ?? []
|
|
||||||
|
|
||||||
context.insert(self)
|
|
||||||
|
|
||||||
// Must assign after we're inserted into context.
|
|
||||||
self._permissions = NSOrderedSet(array: permissions)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -177,20 +177,26 @@ private extension NewsViewController
|
|||||||
AppManager.shared.fetchSources() { (result) in
|
AppManager.shared.fetchSources() { (result) in
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
let sources = try result.get()
|
do
|
||||||
try sources.first?.managedObjectContext?.save()
|
{
|
||||||
|
let (_, context) = try result.get()
|
||||||
DispatchQueue.main.async {
|
try context.save()
|
||||||
self.loadingState = .finished(.success(()))
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.loadingState = .finished(.success(()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch let error as AppManager.FetchSourcesError
|
||||||
|
{
|
||||||
|
try error.managedObjectContext?.save()
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch let error as NSError
|
catch
|
||||||
{
|
{
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if self.dataSource.itemCount > 0
|
if self.dataSource.itemCount > 0
|
||||||
{
|
{
|
||||||
let error = error.withLocalizedFailure(NSLocalizedString("Failed to Fetch Sources", comment: ""))
|
|
||||||
|
|
||||||
let toastView = ToastView(error: error)
|
let toastView = ToastView(error: error)
|
||||||
toastView.show(in: self)
|
toastView.show(in: self)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,10 @@ class FetchSourceOperation: ResultOperation<Source>
|
|||||||
super.main()
|
super.main()
|
||||||
|
|
||||||
let dataTask = self.session.dataTask(with: self.sourceURL) { (data, response, error) in
|
let dataTask = self.session.dataTask(with: self.sourceURL) { (data, response, error) in
|
||||||
self.managedObjectContext.perform {
|
|
||||||
|
let childContext = DatabaseManager.shared.persistentContainer.newBackgroundContext(withParent: self.managedObjectContext)
|
||||||
|
childContext.mergePolicy = NSOverwriteMergePolicy
|
||||||
|
childContext.perform {
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
let (data, _) = try Result((data, response), error).get()
|
let (data, _) = try Result((data, response), error).get()
|
||||||
@@ -68,21 +71,35 @@ class FetchSourceOperation: ResultOperation<Source>
|
|||||||
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Date is in invalid format.")
|
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Date is in invalid format.")
|
||||||
})
|
})
|
||||||
|
|
||||||
decoder.managedObjectContext = self.managedObjectContext
|
decoder.managedObjectContext = childContext
|
||||||
decoder.sourceURL = self.sourceURL
|
decoder.sourceURL = self.sourceURL
|
||||||
|
|
||||||
let source = try decoder.decode(Source.self, from: data)
|
let source = try decoder.decode(Source.self, from: data)
|
||||||
|
let identifier = source.identifier
|
||||||
|
|
||||||
if source.identifier == Source.altStoreIdentifier, let patreonAccessToken = source.userInfo?[.patreonAccessToken]
|
if identifier == Source.altStoreIdentifier, let patreonAccessToken = source.userInfo?[.patreonAccessToken]
|
||||||
{
|
{
|
||||||
Keychain.shared.patreonCreatorAccessToken = patreonAccessToken
|
Keychain.shared.patreonCreatorAccessToken = patreonAccessToken
|
||||||
}
|
}
|
||||||
|
|
||||||
self.finish(.success(source))
|
try childContext.save()
|
||||||
|
|
||||||
|
self.managedObjectContext.perform {
|
||||||
|
if let source = Source.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Source.identifier), identifier), in: self.managedObjectContext)
|
||||||
|
{
|
||||||
|
self.finish(.success(source))
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.finish(.failure(OperationError.noSources))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
self.finish(.failure(error))
|
self.managedObjectContext.perform {
|
||||||
|
self.finish(.failure(error))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user