[AltStore] Refactors fetch apps logic to use Source model objects

This commit is contained in:
Riley Testut
2019-07-30 17:00:04 -07:00
parent 75e398822f
commit 87ced5523e
13 changed files with 272 additions and 115 deletions

View File

@@ -109,7 +109,6 @@
BF770E5822BC3D0F002A40FE /* OperationGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5722BC3D0F002A40FE /* OperationGroup.swift */; }; BF770E5822BC3D0F002A40FE /* OperationGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5722BC3D0F002A40FE /* OperationGroup.swift */; };
BF770E6722BD57C4002A40FE /* BackgroundTaskManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E6622BD57C3002A40FE /* BackgroundTaskManager.swift */; }; BF770E6722BD57C4002A40FE /* BackgroundTaskManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E6622BD57C3002A40FE /* BackgroundTaskManager.swift */; };
BF770E6922BD57DD002A40FE /* Silence.m4a in Resources */ = {isa = PBXBuildFile; fileRef = BF770E6822BD57DD002A40FE /* Silence.m4a */; }; BF770E6922BD57DD002A40FE /* Silence.m4a in Resources */ = {isa = PBXBuildFile; fileRef = BF770E6822BD57DD002A40FE /* Silence.m4a */; };
BF7B9EF322B82B1F0042C873 /* FetchAppsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7B9EF222B82B1F0042C873 /* FetchAppsOperation.swift */; };
BF8F69C222E659F700049BA1 /* AppContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8F69C122E659F700049BA1 /* AppContentViewController.swift */; }; BF8F69C222E659F700049BA1 /* AppContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8F69C122E659F700049BA1 /* AppContentViewController.swift */; };
BF8F69C422E662D300049BA1 /* AppViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8F69C322E662D300049BA1 /* AppViewController.swift */; }; BF8F69C422E662D300049BA1 /* AppViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8F69C322E662D300049BA1 /* AppViewController.swift */; };
BF9ABA4522DCFF43008935CF /* BrowseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4422DCFF43008935CF /* BrowseViewController.swift */; }; BF9ABA4522DCFF43008935CF /* BrowseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4422DCFF43008935CF /* BrowseViewController.swift */; };
@@ -176,6 +175,8 @@
BFDB6A0B22AAEDB7007EA6D6 /* Operation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB6A0A22AAEDB7007EA6D6 /* Operation.swift */; }; BFDB6A0B22AAEDB7007EA6D6 /* Operation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB6A0A22AAEDB7007EA6D6 /* Operation.swift */; };
BFDB6A0D22AAFC1A007EA6D6 /* OperationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB6A0C22AAFC19007EA6D6 /* OperationError.swift */; }; BFDB6A0D22AAFC1A007EA6D6 /* OperationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB6A0C22AAFC19007EA6D6 /* OperationError.swift */; };
BFDB6A0F22AB2776007EA6D6 /* SendAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB6A0E22AB2776007EA6D6 /* SendAppOperation.swift */; }; BFDB6A0F22AB2776007EA6D6 /* SendAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB6A0E22AB2776007EA6D6 /* SendAppOperation.swift */; };
BFE338DD22F0E7F3002E24B9 /* Source.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE338DC22F0E7F3002E24B9 /* Source.swift */; };
BFE338DF22F0EADB002E24B9 /* FetchSourceOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE338DE22F0EADB002E24B9 /* FetchSourceOperation.swift */; };
BFE338E822F10E56002E24B9 /* LaunchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE338E722F10E56002E24B9 /* LaunchViewController.swift */; }; BFE338E822F10E56002E24B9 /* LaunchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE338E722F10E56002E24B9 /* LaunchViewController.swift */; };
BFE6325A22A83BEB00F30809 /* Authentication.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFE6325922A83BEB00F30809 /* Authentication.storyboard */; }; BFE6325A22A83BEB00F30809 /* Authentication.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFE6325922A83BEB00F30809 /* Authentication.storyboard */; };
BFE6325C22A83C0100F30809 /* AuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6325B22A83C0100F30809 /* AuthenticationViewController.swift */; }; BFE6325C22A83C0100F30809 /* AuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6325B22A83C0100F30809 /* AuthenticationViewController.swift */; };
@@ -356,7 +357,6 @@
BF770E5722BC3D0F002A40FE /* OperationGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationGroup.swift; sourceTree = "<group>"; }; BF770E5722BC3D0F002A40FE /* OperationGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationGroup.swift; sourceTree = "<group>"; };
BF770E6622BD57C3002A40FE /* BackgroundTaskManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundTaskManager.swift; sourceTree = "<group>"; }; BF770E6622BD57C3002A40FE /* BackgroundTaskManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundTaskManager.swift; sourceTree = "<group>"; };
BF770E6822BD57DD002A40FE /* Silence.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = Silence.m4a; sourceTree = "<group>"; }; BF770E6822BD57DD002A40FE /* Silence.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = Silence.m4a; sourceTree = "<group>"; };
BF7B9EF222B82B1F0042C873 /* FetchAppsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchAppsOperation.swift; sourceTree = "<group>"; };
BF8F69C122E659F700049BA1 /* AppContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppContentViewController.swift; sourceTree = "<group>"; }; BF8F69C122E659F700049BA1 /* AppContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppContentViewController.swift; sourceTree = "<group>"; };
BF8F69C322E662D300049BA1 /* AppViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppViewController.swift; sourceTree = "<group>"; }; BF8F69C322E662D300049BA1 /* AppViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppViewController.swift; sourceTree = "<group>"; };
BF9ABA4422DCFF43008935CF /* BrowseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowseViewController.swift; sourceTree = "<group>"; }; BF9ABA4422DCFF43008935CF /* BrowseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowseViewController.swift; sourceTree = "<group>"; };
@@ -427,6 +427,8 @@
BFDB6A0A22AAEDB7007EA6D6 /* Operation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Operation.swift; sourceTree = "<group>"; }; BFDB6A0A22AAEDB7007EA6D6 /* Operation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Operation.swift; sourceTree = "<group>"; };
BFDB6A0C22AAFC19007EA6D6 /* OperationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationError.swift; sourceTree = "<group>"; }; BFDB6A0C22AAFC19007EA6D6 /* OperationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationError.swift; sourceTree = "<group>"; };
BFDB6A0E22AB2776007EA6D6 /* SendAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendAppOperation.swift; sourceTree = "<group>"; }; BFDB6A0E22AB2776007EA6D6 /* SendAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendAppOperation.swift; sourceTree = "<group>"; };
BFE338DC22F0E7F3002E24B9 /* Source.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Source.swift; sourceTree = "<group>"; };
BFE338DE22F0EADB002E24B9 /* FetchSourceOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchSourceOperation.swift; sourceTree = "<group>"; };
BFE338E722F10E56002E24B9 /* LaunchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchViewController.swift; sourceTree = "<group>"; }; BFE338E722F10E56002E24B9 /* LaunchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchViewController.swift; sourceTree = "<group>"; };
BFE6325922A83BEB00F30809 /* Authentication.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Authentication.storyboard; sourceTree = "<group>"; }; BFE6325922A83BEB00F30809 /* Authentication.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Authentication.storyboard; sourceTree = "<group>"; };
BFE6325B22A83C0100F30809 /* AuthenticationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationViewController.swift; sourceTree = "<group>"; }; BFE6325B22A83C0100F30809 /* AuthenticationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationViewController.swift; sourceTree = "<group>"; };
@@ -851,6 +853,7 @@
BFBBE2DE22931F73002097FA /* App.swift */, BFBBE2DE22931F73002097FA /* App.swift */,
BF3D648722E79A3700E9056B /* AppPermission.swift */, BF3D648722E79A3700E9056B /* AppPermission.swift */,
BFBBE2E022931F81002097FA /* InstalledApp.swift */, BFBBE2E022931F81002097FA /* InstalledApp.swift */,
BFE338DC22F0E7F3002E24B9 /* Source.swift */,
BFE6326522A857C100F30809 /* Team.swift */, BFE6326522A857C100F30809 /* Team.swift */,
); );
path = Model; path = Model;
@@ -905,7 +908,7 @@
BFDB6A0722AAED73007EA6D6 /* ResignAppOperation.swift */, BFDB6A0722AAED73007EA6D6 /* ResignAppOperation.swift */,
BFDB6A0E22AB2776007EA6D6 /* SendAppOperation.swift */, BFDB6A0E22AB2776007EA6D6 /* SendAppOperation.swift */,
BF770E5022BB1CF6002A40FE /* InstallAppOperation.swift */, BF770E5022BB1CF6002A40FE /* InstallAppOperation.swift */,
BF7B9EF222B82B1F0042C873 /* FetchAppsOperation.swift */, BFE338DE22F0EADB002E24B9 /* FetchSourceOperation.swift */,
); );
path = Operations; path = Operations;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1274,7 +1277,6 @@
files = ( files = (
BFDB6A0F22AB2776007EA6D6 /* SendAppOperation.swift in Sources */, BFDB6A0F22AB2776007EA6D6 /* SendAppOperation.swift in Sources */,
BFDB6A0D22AAFC1A007EA6D6 /* OperationError.swift in Sources */, BFDB6A0D22AAFC1A007EA6D6 /* OperationError.swift in Sources */,
BF7B9EF322B82B1F0042C873 /* FetchAppsOperation.swift in Sources */,
BF3D649D22E7AC1B00E9056B /* PermissionPopoverViewController.swift in Sources */, BF3D649D22E7AC1B00E9056B /* PermissionPopoverViewController.swift in Sources */,
BFE6325E22A8497000F30809 /* SelectTeamViewController.swift in Sources */, BFE6325E22A8497000F30809 /* SelectTeamViewController.swift in Sources */,
BFD2478F2284C8F900981D42 /* Button.swift in Sources */, BFD2478F2284C8F900981D42 /* Button.swift in Sources */,
@@ -1283,11 +1285,13 @@
BFB11692229322E400BB457C /* DatabaseManager.swift in Sources */, BFB11692229322E400BB457C /* DatabaseManager.swift in Sources */,
BFC1F38D22AEE3A4003AC21A /* DownloadAppOperation.swift in Sources */, BFC1F38D22AEE3A4003AC21A /* DownloadAppOperation.swift in Sources */,
BFBBE2E122931F81002097FA /* InstalledApp.swift in Sources */, BFBBE2E122931F81002097FA /* InstalledApp.swift in Sources */,
BFE338DF22F0EADB002E24B9 /* FetchSourceOperation.swift in Sources */,
BFBBE2DF22931F73002097FA /* App.swift in Sources */, BFBBE2DF22931F73002097FA /* App.swift in Sources */,
BF9ABA4522DCFF43008935CF /* BrowseViewController.swift in Sources */, BF9ABA4522DCFF43008935CF /* BrowseViewController.swift in Sources */,
BF43002E22A714AF0051E2BC /* Keychain.swift in Sources */, BF43002E22A714AF0051E2BC /* Keychain.swift in Sources */,
BF770E5422BC044E002A40FE /* AppOperationContext.swift in Sources */, BF770E5422BC044E002A40FE /* AppOperationContext.swift in Sources */,
BFD2478C2284C4C300981D42 /* AppIconImageView.swift in Sources */, BFD2478C2284C4C300981D42 /* AppIconImageView.swift in Sources */,
BFE338DD22F0E7F3002E24B9 /* Source.swift in Sources */,
BF8F69C422E662D300049BA1 /* AppViewController.swift in Sources */, BF8F69C422E662D300049BA1 /* AppViewController.swift in Sources */,
BFE6326822A858F300F30809 /* Account.swift in Sources */, BFE6326822A858F300F30809 /* Account.swift in Sources */,
BFE6326622A857C200F30809 /* Team.swift in Sources */, BFE6326622A857C200F30809 /* Team.swift in Sources */,

View File

@@ -176,22 +176,22 @@ extension AppDelegate
return return
} }
var fetchAppsResult: Result<[App], Error>? var fetchSourceResult: Result<Source, Error>?
var serversResult: Result<Void, Error>? var serversResult: Result<Void, Error>?
let dispatchGroup = DispatchGroup() let dispatchGroup = DispatchGroup()
dispatchGroup.enter() dispatchGroup.enter()
dispatchGroup.enter() dispatchGroup.enter()
AppManager.shared.fetchApps() { (result) in AppManager.shared.fetchSource() { (result) in
fetchAppsResult = result fetchSourceResult = result
dispatchGroup.leave() dispatchGroup.leave()
do do
{ {
let apps = try result.get() let source = try result.get()
guard let context = apps.first?.managedObjectContext else { return } guard let context = source.managedObjectContext else { return }
let updatesFetchRequest = InstalledApp.updatesFetchRequest() let updatesFetchRequest = InstalledApp.updatesFetchRequest()
updatesFetchRequest.includesPendingChanges = true updatesFetchRequest.includesPendingChanges = true
@@ -230,13 +230,13 @@ extension AppDelegate
} }
dispatchGroup.notify(queue: .main) { dispatchGroup.notify(queue: .main) {
guard let fetchAppsResult = fetchAppsResult, let serversResult = serversResult else { guard let fetchSourceResult = fetchSourceResult, let serversResult = serversResult else {
completionHandler(.failed) completionHandler(.failed)
return return
} }
// Call completionHandler early to improve chances of refreshing in the background again. // Call completionHandler early to improve chances of refreshing in the background again.
switch (fetchAppsResult, serversResult) switch (fetchSourceResult, serversResult)
{ {
case (.success, .success): completionHandler(.newData) case (.success, .success): completionHandler(.newData)
case (.success, .failure(ConnectionError.serverNotFound)): completionHandler(.newData) case (.success, .failure(ConnectionError.serverNotFound)): completionHandler(.newData)

View File

@@ -36,7 +36,7 @@ class BrowseViewController: UICollectionViewController
{ {
super.viewWillAppear(animated) super.viewWillAppear(animated)
self.fetchApps() self.fetchSource()
} }
override func prepare(for segue: UIStoryboardSegue, sender: Any?) override func prepare(for segue: UIStoryboardSegue, sender: Any?)
@@ -57,10 +57,18 @@ private extension BrowseViewController
func makeDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<App, UIImage> func makeDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<App, UIImage>
{ {
let fetchRequest = App.fetchRequest() as NSFetchRequest<App> let fetchRequest = App.fetchRequest() as NSFetchRequest<App>
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \App.name, ascending: false)] fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \App.sortIndex, ascending: true), NSSortDescriptor(keyPath: \App.name, ascending: true)]
fetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(App.bundleIdentifier), App.altstoreAppID)
fetchRequest.returnsObjectsAsFaults = false fetchRequest.returnsObjectsAsFaults = false
if let source = Source.fetchAltStoreSource(in: DatabaseManager.shared.viewContext)
{
fetchRequest.predicate = NSPredicate(format: "%K != %@ AND %K == %@", #keyPath(App.bundleIdentifier), App.altstoreAppID, #keyPath(App.source), source)
}
else
{
fetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(App.bundleIdentifier), App.altstoreAppID)
}
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<App, UIImage>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext) let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<App, UIImage>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
dataSource.cellConfigurationHandler = { (cell, app, indexPath) in dataSource.cellConfigurationHandler = { (cell, app, indexPath) in
let cell = cell as! BrowseCollectionViewCell let cell = cell as! BrowseCollectionViewCell
@@ -98,13 +106,13 @@ private extension BrowseViewController
return dataSource return dataSource
} }
func fetchApps() func fetchSource()
{ {
AppManager.shared.fetchApps() { (result) in AppManager.shared.fetchSource() { (result) in
do do
{ {
let apps = try result.get() let source = try result.get()
try apps.first?.managedObjectContext?.save() try source.managedObjectContext?.save()
} }
catch catch
{ {

View File

@@ -16,7 +16,7 @@ import Roxas
extension AppManager extension AppManager
{ {
static let didFetchAppsNotification = Notification.Name("com.altstore.AppManager.didFetchApps") static let didFetchSourceNotification = Notification.Name("com.altstore.AppManager.didFetchSource")
} }
class AppManager class AppManager
@@ -86,21 +86,27 @@ extension AppManager
extension AppManager extension AppManager
{ {
func fetchApps(completionHandler: @escaping (Result<[App], Error>) -> Void) func fetchSource(completionHandler: @escaping (Result<Source, Error>) -> Void)
{ {
let fetchAppsOperation = FetchAppsOperation() DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
fetchAppsOperation.resultHandler = { (result) in guard let source = Source.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Source.identifier), Source.altStoreIdentifier), in: context) else {
return completionHandler(.failure(OperationError.noSources))
}
let fetchSourceOperation = FetchSourceOperation(sourceURL: source.sourceURL)
fetchSourceOperation.resultHandler = { (result) in
switch result switch result
{ {
case .failure(let error): case .failure(let error):
completionHandler(.failure(error)) completionHandler(.failure(error))
case .success(let apps): case .success(let source):
completionHandler(.success(apps)) completionHandler(.success(source))
NotificationCenter.default.post(name: AppManager.didFetchAppsNotification, object: self) NotificationCenter.default.post(name: AppManager.didFetchSourceNotification, object: self)
} }
} }
self.operationQueue.addOperation(fetchAppsOperation) self.operationQueue.addOperation(fetchSourceOperation)
}
} }
} }

View File

@@ -22,6 +22,7 @@
<attribute name="name" attributeType="String" syncable="YES"/> <attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="screenshotNames" attributeType="Transformable" syncable="YES"/> <attribute name="screenshotNames" attributeType="Transformable" syncable="YES"/>
<attribute name="size" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/> <attribute name="size" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="subtitle" optional="YES" attributeType="String" syncable="YES"/> <attribute name="subtitle" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="tintColor" optional="YES" attributeType="Transformable" syncable="YES"/> <attribute name="tintColor" optional="YES" attributeType="Transformable" syncable="YES"/>
<attribute name="version" attributeType="String" syncable="YES"/> <attribute name="version" attributeType="String" syncable="YES"/>
@@ -29,6 +30,7 @@
<attribute name="versionDescription" optional="YES" attributeType="String" syncable="YES"/> <attribute name="versionDescription" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="storeApp" inverseEntity="InstalledApp" syncable="YES"/> <relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="storeApp" inverseEntity="InstalledApp" syncable="YES"/>
<relationship name="permissions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission" syncable="YES"/> <relationship name="permissions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission" syncable="YES"/>
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source" syncable="YES"/>
<uniquenessConstraints> <uniquenessConstraints>
<uniquenessConstraint> <uniquenessConstraint>
<constraint value="bundleIdentifier"/> <constraint value="bundleIdentifier"/>
@@ -54,6 +56,17 @@
</uniquenessConstraint> </uniquenessConstraint>
</uniquenessConstraints> </uniquenessConstraints>
</entity> </entity>
<entity name="Source" representedClassName="Source" syncable="YES">
<attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="sourceURL" attributeType="URI" syncable="YES"/>
<relationship name="apps" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="App" inverseName="source" inverseEntity="App" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="Team" representedClassName="Team" syncable="YES"> <entity name="Team" representedClassName="Team" syncable="YES">
<attribute name="identifier" attributeType="String" syncable="YES"/> <attribute name="identifier" attributeType="String" syncable="YES"/>
<attribute name="isActiveTeam" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/> <attribute name="isActiveTeam" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
@@ -68,9 +81,10 @@
</entity> </entity>
<elements> <elements>
<element name="Account" positionX="-36" positionY="90" width="128" height="135"/> <element name="Account" positionX="-36" positionY="90" width="128" height="135"/>
<element name="App" positionX="-63" positionY="-18" width="128" height="270"/> <element name="App" positionX="-63" positionY="-18" width="128" height="300"/>
<element name="AppPermission" positionX="-45" positionY="90" width="128" height="90"/> <element name="AppPermission" positionX="-45" positionY="90" width="128" height="90"/>
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="150"/> <element name="InstalledApp" positionX="-63" positionY="0" width="128" height="150"/>
<element name="Team" positionX="-45" positionY="81" width="128" height="120"/> <element name="Team" positionX="-45" positionY="81" width="128" height="120"/>
<element name="Source" positionX="-45" positionY="99" width="128" height="105"/>
</elements> </elements>
</model> </model>

View File

@@ -39,8 +39,11 @@ class App: NSManagedObject, Decodable, Fetchable
@NSManaged private(set) var downloadURL: URL @NSManaged private(set) var downloadURL: URL
@NSManaged private(set) var tintColor: UIColor? @NSManaged private(set) var tintColor: UIColor?
@NSManaged var sortIndex: Int32
/* Relationships */ /* Relationships */
@NSManaged var installedApp: InstalledApp? @NSManaged var installedApp: InstalledApp?
@NSManaged var source: Source?
@objc(permissions) @NSManaged var _permissions: NSOrderedSet @objc(permissions) @NSManaged var _permissions: NSOrderedSet
@nonobjc var permissions: [AppPermission] { @nonobjc var permissions: [AppPermission] {

View File

@@ -118,8 +118,11 @@ private extension DatabaseManager
} }
else else
{ {
let source = Source.makeAltStoreSource(in: context)
storeApp = App.makeAltStoreApp(in: context) storeApp = App.makeAltStoreApp(in: context)
storeApp.version = localApp.version storeApp.version = localApp.version
storeApp.source = source
} }
let installedApp: InstalledApp let installedApp: InstalledApp

View File

@@ -30,6 +30,20 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
permission.managedObjectContext?.delete(permission) permission.managedObjectContext?.delete(permission)
} }
case let databaseObject as Source:
guard let conflictedObject = conflict.conflictingObjects.first as? Source else { break }
let bundleIdentifiers = Set(conflictedObject.apps.map { $0.bundleIdentifier })
for app in databaseObject.apps
{
if !bundleIdentifiers.contains(app.bundleIdentifier)
{
// No longer listed in Source, so remove it from database.
app.managedObjectContext?.delete(app)
}
}
default: break default: break
} }
} }

View File

@@ -0,0 +1,92 @@
//
// Source.swift
// AltStore
//
// Created by Riley Testut on 7/30/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import CoreData
extension Source
{
static let altStoreIdentifier = "com.rileytestut.AltStore"
}
@objc(Source)
class Source: NSManagedObject, Fetchable, Decodable
{
/* Properties */
@NSManaged var name: String
@NSManaged var identifier: String
@NSManaged var sourceURL: URL
/* Relationships */
@objc(apps) @NSManaged private(set) var _apps: NSOrderedSet
@nonobjc var apps: [App] {
get {
return self._apps.array as! [App]
}
set {
self._apps = NSOrderedSet(array: newValue)
}
}
private enum CodingKeys: String, CodingKey
{
case name
case identifier
case sourceURL
case apps
}
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
{
super.init(entity: entity, insertInto: context)
}
required init(from decoder: Decoder) throws
{
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
super.init(entity: Source.entity(), insertInto: nil)
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)
self.sourceURL = try container.decode(URL.self, forKey: .sourceURL)
let apps = try container.decodeIfPresent([App].self, forKey: .apps) ?? []
for (index, app) in apps.enumerated()
{
app.sortIndex = Int32(index)
}
context.insert(self)
// Must assign after we're inserted into context.
self._apps = NSMutableOrderedSet(array: apps)
print("Downloaded Order:", self.apps.map { $0.bundleIdentifier })
}
}
extension Source
{
class func makeAltStoreSource(in context: NSManagedObjectContext) -> Source
{
let source = Source(context: context)
source.name = "AltStore"
source.identifier = Source.altStoreIdentifier
source.sourceURL = URL(string: "https://www.dropbox.com/s/6qi1vt6hsi88lv6/Apps-Dev.json?dl=1")!
return source
}
class func fetchAltStoreSource(in context: NSManagedObjectContext) -> Source?
{
let source = Source.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Source.identifier), Source.altStoreIdentifier), in: context)
return source
}
}

View File

@@ -57,7 +57,7 @@ class MyAppsViewController: UICollectionViewController
{ {
super.init(coder: aDecoder) super.init(coder: aDecoder)
NotificationCenter.default.addObserver(self, selector: #selector(MyAppsViewController.didFetchApps(_:)), name: AppManager.didFetchAppsNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(MyAppsViewController.didFetchSource(_:)), name: AppManager.didFetchSourceNotification, object: nil)
} }
override func viewDidLoad() override func viewDidLoad()
@@ -522,7 +522,7 @@ private extension MyAppsViewController
private extension MyAppsViewController private extension MyAppsViewController
{ {
@objc func didFetchApps(_ notification: Notification) @objc func didFetchSource(_ notification: Notification)
{ {
DispatchQueue.main.async { DispatchQueue.main.async {
if self.updatesDataSource.fetchedResultsController.fetchedObjects == nil if self.updatesDataSource.fetchedResultsController.fetchedObjects == nil

View File

@@ -1,17 +1,19 @@
// //
// FetchAppsOperation.swift // FetchSourceOperation.swift
// AltStore // AltStore
// //
// Created by Riley Testut on 6/17/19. // Created by Riley Testut on 7/30/19.
// Copyright © 2019 Riley Testut. All rights reserved. // Copyright © 2019 Riley Testut. All rights reserved.
// //
import Foundation import Foundation
import Roxas import Roxas
@objc(FetchAppsOperation) @objc(FetchSourceOperation)
class FetchAppsOperation: ResultOperation<[App]> class FetchSourceOperation: ResultOperation<Source>
{ {
let sourceURL: URL
private let session = URLSession(configuration: .default) private let session = URLSession(configuration: .default)
private lazy var dateFormatter: DateFormatter = { private lazy var dateFormatter: DateFormatter = {
@@ -20,13 +22,16 @@ class FetchAppsOperation: ResultOperation<[App]>
return dateFormatter return dateFormatter
}() }()
init(sourceURL: URL)
{
self.sourceURL = sourceURL
}
override func main() override func main()
{ {
super.main() super.main()
let appsURL = URL(string: "https://www.dropbox.com/s/6qi1vt6hsi88lv6/Apps-Dev.json?dl=1")! let dataTask = self.session.dataTask(with: self.sourceURL) { (data, response, error) in
let dataTask = self.session.dataTask(with: appsURL) { (data, response, error) in
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
do do
{ {
@@ -36,8 +41,8 @@ class FetchAppsOperation: ResultOperation<[App]>
decoder.dateDecodingStrategy = .formatted(self.dateFormatter) decoder.dateDecodingStrategy = .formatted(self.dateFormatter)
decoder.managedObjectContext = context decoder.managedObjectContext = context
let apps = try decoder.decode([App].self, from: data) let source = try decoder.decode(Source.self, from: data)
self.finish(.success(apps)) self.finish(.success(source))
} }
catch catch
{ {

View File

@@ -25,6 +25,8 @@ enum OperationError: LocalizedError
case iOSVersionNotSupported(ALTApplication) case iOSVersionNotSupported(ALTApplication)
case noSources
var errorDescription: String? { var errorDescription: String? {
switch self { switch self {
case .unknown: return NSLocalizedString("An unknown error occured.", comment: "") case .unknown: return NSLocalizedString("An unknown error occured.", comment: "")
@@ -35,6 +37,7 @@ enum OperationError: LocalizedError
case .unknownUDID: return NSLocalizedString("Unknown device UDID.", comment: "") case .unknownUDID: return NSLocalizedString("Unknown device UDID.", comment: "")
case .invalidApp: return NSLocalizedString("The app is invalid.", comment: "") case .invalidApp: return NSLocalizedString("The app is invalid.", comment: "")
case .invalidParameters: return NSLocalizedString("Invalid parameters.", comment: "") case .invalidParameters: return NSLocalizedString("Invalid parameters.", comment: "")
case .noSources: return NSLocalizedString("There are no AltStore sources.", comment: "")
case .iOSVersionNotSupported(let app): case .iOSVersionNotSupported(let app):
let name = app.name let name = app.name

View File

@@ -1,4 +1,8 @@
[ {
"name": "AltStore",
"identifier": "com.rileytestut.AltStore",
"sourceURL": "https://www.dropbox.com/s/6qi1vt6hsi88lv6/Apps-Dev.json?dl=1",
"apps": [
{ {
"name": "AltStore", "name": "AltStore",
"bundleIdentifier": "com.rileytestut.AltStore", "bundleIdentifier": "com.rileytestut.AltStore",
@@ -43,7 +47,7 @@
"screenshotNames": [ "screenshotNames": [
"Delta1", "Delta1",
"Delta2", "Delta2",
"Delta3", "Delta3"
] ]
}, },
{ {
@@ -67,7 +71,8 @@
], ],
"screenshotNames": [ "screenshotNames": [
"Clip1", "Clip1",
"Clip2", "Clip2"
] ]
} }
] ]
}