Supports app versions with explicit build versions

AltStore will now consider an update available if either:

* The source’s marketing version doesn’t match installed app’s version
* The source declares a build version AND it doesn’t match the install app’s build version

The installed app matches an app version if both maketing versions match, and the build versions match (if provided by the source).
This commit is contained in:
Riley Testut
2023-05-18 14:51:26 -05:00
committed by Magesh K
parent efce9a8579
commit f7640e35d1
13 changed files with 130 additions and 46 deletions

View File

@@ -24,6 +24,7 @@ extension AnalyticsManager
case bundleIdentifier case bundleIdentifier
case developerName case developerName
case version case version
case buildVersion
case size case size
case tintColor case tintColor
case sourceIdentifier case sourceIdentifier
@@ -59,6 +60,7 @@ extension AnalyticsManager
.bundleIdentifier: app.bundleIdentifier, .bundleIdentifier: app.bundleIdentifier,
.developerName: app.storeApp?.developerName, .developerName: app.storeApp?.developerName,
.version: app.version, .version: app.version,
.buildVersion: app.buildVersion,
.size: appBundleSize?.description, .size: appBundleSize?.description,
.tintColor: app.storeApp?.tintColor?.hexString, .tintColor: app.storeApp?.tintColor?.hexString,
.sourceIdentifier: app.storeApp?.sourceIdentifier, .sourceIdentifier: app.storeApp?.sourceIdentifier,

View File

@@ -84,7 +84,7 @@ final class AppContentViewController: UITableViewController
if let version = self.app.latestAvailableVersion if let version = self.app.latestAvailableVersion
{ {
self.versionDescriptionTextView.text = version.localizedDescription self.versionDescriptionTextView.text = version.localizedDescription
self.versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), version.version) self.versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), version.localizedVersion)
self.versionDateLabel.text = Date().relativeDateString(since: version.date, dateFormatter: self.dateFormatter) self.versionDateLabel.text = Date().relativeDateString(since: version.date, dateFormatter: self.dateFormatter)
self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: version.size) self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: version.size)
} }

View File

@@ -346,7 +346,7 @@ private extension AppViewController
if let installedApp = self.app.installedApp if let installedApp = self.app.installedApp
{ {
if let latestVersion = self.app.latestAvailableVersion, installedApp.version != latestVersion.version if let latestVersion = self.app.latestAvailableVersion, !installedApp.matches(latestVersion)
{ {
button.setTitle(NSLocalizedString("UPDATE", comment: ""), for: .normal) button.setTitle(NSLocalizedString("UPDATE", comment: ""), for: .normal)
} }
@@ -500,7 +500,7 @@ extension AppViewController
{ {
if let installedApp = self.app.installedApp if let installedApp = self.app.installedApp
{ {
if let latestVersion = self.app.latestAvailableVersion, installedApp.version != latestVersion.version if let latestVersion = self.app.latestAvailableVersion, !installedApp.matches(latestVersion)
{ {
self.updateApp(installedApp) self.updateApp(installedApp)
} }

View File

@@ -341,7 +341,9 @@ private extension AppDelegate
let previousUpdatesFetchRequest = InstalledApp.supportedUpdatesFetchRequest() as! NSFetchRequest<NSFetchRequestResult> let previousUpdatesFetchRequest = InstalledApp.supportedUpdatesFetchRequest() as! NSFetchRequest<NSFetchRequestResult>
previousUpdatesFetchRequest.includesPendingChanges = false previousUpdatesFetchRequest.includesPendingChanges = false
previousUpdatesFetchRequest.resultType = .dictionaryResultType previousUpdatesFetchRequest.resultType = .dictionaryResultType
previousUpdatesFetchRequest.propertiesToFetch = [#keyPath(InstalledApp.bundleIdentifier), #keyPath(InstalledApp.storeApp.latestSupportedVersion.version)] previousUpdatesFetchRequest.propertiesToFetch = [#keyPath(InstalledApp.bundleIdentifier),
#keyPath(InstalledApp.storeApp.latestSupportedVersion.version),
#keyPath(InstalledApp.storeApp.latestSupportedVersion._buildVersion)]
let previousNewsItemsFetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NSFetchRequestResult> let previousNewsItemsFetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NSFetchRequestResult>
previousNewsItemsFetchRequest.includesPendingChanges = false previousNewsItemsFetchRequest.includesPendingChanges = false
@@ -365,13 +367,19 @@ private extension AppDelegate
if let previousUpdate = previousUpdates.first(where: { $0[#keyPath(InstalledApp.bundleIdentifier)] == update.bundleIdentifier }) if let previousUpdate = previousUpdates.first(where: { $0[#keyPath(InstalledApp.bundleIdentifier)] == update.bundleIdentifier })
{ {
// An update for this app was already available, so check whether the version # is different. // An update for this app was already available, so check whether the version or build version is different.
guard let version = previousUpdate[#keyPath(InstalledApp.storeApp.latestSupportedVersion.version)], version != latestSupportedVersion.version else { continue } guard let previousVersion = previousUpdate[#keyPath(InstalledApp.storeApp.latestSupportedVersion.version)] else { continue }
// previousUpdate might not contain buildVersion, but if it does then map empty string to nil to match AppVersion.
let previousBuildVersion = previousUpdate[#keyPath(InstalledApp.storeApp.latestSupportedVersion._buildVersion)].map { $0.isEmpty ? nil : "" }
// Only show notification if previous latestSupportedVersion does not _exactly_ match current latestSupportedVersion.
guard previousVersion != latestSupportedVersion.version || previousBuildVersion != latestSupportedVersion.buildVersion else { continue }
} }
let content = UNMutableNotificationContent() let content = UNMutableNotificationContent()
content.title = NSLocalizedString("New Update Available", comment: "") content.title = NSLocalizedString("New Update Available", comment: "")
content.body = String(format: NSLocalizedString("%@ %@ is now available for download.", comment: ""), update.name, latestSupportedVersion.version) content.body = String(format: NSLocalizedString("%@ %@ is now available for download.", comment: ""), update.name, latestSupportedVersion.localizedVersion)
content.sound = .default content.sound = .default
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil) let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)

View File

@@ -248,7 +248,7 @@ private extension MyAppsViewController
appName = app.name appName = app.name
} }
cell.bannerView.accessibilityLabel = String(format: NSLocalizedString("%@ %@ update. Released on %@.", comment: ""), appName, latestSupportedVersion.version, versionDate) cell.bannerView.accessibilityLabel = String(format: NSLocalizedString("%@ %@ update. Released on %@.", comment: ""), appName, latestSupportedVersion.localizedVersion, versionDate)
cell.bannerView.button.isIndicatingActivity = false cell.bannerView.button.isIndicatingActivity = false
cell.bannerView.button.addTarget(self, action: #selector(MyAppsViewController.updateApp(_:)), for: .primaryActionTriggered) cell.bannerView.button.addTarget(self, action: #selector(MyAppsViewController.updateApp(_:)), for: .primaryActionTriggered)
@@ -1049,7 +1049,7 @@ private extension MyAppsViewController
var title = storeApp.name var title = storeApp.name
if let appVersion = storeApp.latestAvailableVersion if let appVersion = storeApp.latestAvailableVersion
{ {
title += " " + appVersion.version title += " " + appVersion.localizedVersion
var osVersion: String? = nil var osVersion: String? = nil
if let minOSVersion = appVersion.minOSVersion, !ProcessInfo.processInfo.isOperatingSystemAtLeast(minOSVersion) if let minOSVersion = appVersion.minOSVersion, !ProcessInfo.processInfo.isOperatingSystemAtLeast(minOSVersion)

View File

@@ -64,22 +64,27 @@ final class DownloadAppOperation: ResultOperation<ALTApplication>
do { do {
let latestVersion = try self.verify(storeApp) let latestVersion = try self.verify(storeApp)
self.download(latestVersion) self.download(latestVersion)
} catch let error as VerificationError where error.code == .iOSVersionNotSupported { }
guard let presentingViewController = self.context.presentingViewController, catch let error as VerificationError where error.code == .iOSVersionNotSupported
let latestSupportedVersion = storeApp.latestSupportedVersion, {
case let version = latestSupportedVersion.version, guard let presentingViewController = self.context.presentingViewController, let latestSupportedVersion = storeApp.latestSupportedVersion
version != storeApp.installedApp?.version else { else { return self.finish(.failure(error)) }
return self.finish(.failure(error))
if let installedApp = storeApp.installedApp
{
guard !installedApp.matches(latestSupportedVersion) else { return self.finish(.failure(error)) }
} }
let title = NSLocalizedString("Unsupported iOS Version", comment: "") let title = NSLocalizedString("Unsupported iOS Version", comment: "")
let message = error.localizedDescription + "\n\n" + NSLocalizedString("Would you like to download the last version compatible with this device instead?", comment: "") let message = error.localizedDescription + "\n\n" + NSLocalizedString("Would you like to download the last version compatible with this device instead?", comment: "")
let localizedVersion = latestSupportedVersion.localizedVersion
DispatchQueue.main.async { DispatchQueue.main.async {
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style) { _ in alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style) { _ in
self.finish(.failure(OperationError.cancelled)) self.finish(.failure(OperationError.cancelled))
}) })
alertController.addAction(UIAlertAction(title: String(format: NSLocalizedString("Download %@ %@", comment: ""), self.appName, version), style: .default) { _ in alertController.addAction(UIAlertAction(title: String(format: NSLocalizedString("Download %@ %@", comment: ""), self.appName, localizedVersion), style: .default) { _ in
self.download(latestSupportedVersion) self.download(latestSupportedVersion)
}) })
presentingViewController.present(alertController, animated: true) presentingViewController.present(alertController, animated: true)

View File

@@ -162,8 +162,8 @@ private extension FetchSourceOperation
var versions = Set<String>() var versions = Set<String>()
for version in app.versions for version in app.versions
{ {
guard !versions.contains(version.version) else { throw SourceError.duplicateVersion(version.version, for: app, source: source) } guard !versions.contains(version.versionID) else { throw SourceError.duplicateVersion(version.localizedVersion, for: app, source: source) }
versions.insert(version.version) versions.insert(version.versionID)
} }
for permission in app.permissions for permission in app.permissions

View File

@@ -42,6 +42,7 @@
</entity> </entity>
<entity name="AppVersion" representedClassName="AppVersion" syncable="YES"> <entity name="AppVersion" representedClassName="AppVersion" syncable="YES">
<attribute name="appBundleID" attributeType="String"/> <attribute name="appBundleID" attributeType="String"/>
<attribute name="buildVersion" attributeType="String"/>
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/> <attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="downloadURL" attributeType="URI"/> <attribute name="downloadURL" attributeType="URI"/>
<attribute name="localizedDescription" optional="YES" attributeType="String"/> <attribute name="localizedDescription" optional="YES" attributeType="String"/>
@@ -57,11 +58,13 @@
<uniquenessConstraint> <uniquenessConstraint>
<constraint value="appBundleID"/> <constraint value="appBundleID"/>
<constraint value="version"/> <constraint value="version"/>
<constraint value="buildVersion"/>
<constraint value="sourceID"/> <constraint value="sourceID"/>
</uniquenessConstraint> </uniquenessConstraint>
</uniquenessConstraints> </uniquenessConstraints>
</entity> </entity>
<entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES"> <entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES">
<attribute name="buildVersion" attributeType="String"/>
<attribute name="bundleIdentifier" attributeType="String"/> <attribute name="bundleIdentifier" attributeType="String"/>
<attribute name="certificateSerialNumber" optional="YES" attributeType="String"/> <attribute name="certificateSerialNumber" optional="YES" attributeType="String"/>
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/> <attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO"/>

View File

@@ -13,9 +13,17 @@ public class AppVersion: NSManagedObject, Decodable, Fetchable
{ {
/* Properties */ /* Properties */
@NSManaged public var version: String @NSManaged public var version: String
// NULL does not work as expected with SQL Unique Constraints (because NULL != NULL),
// so we store non-optional value and provide public accessor with optional return type.
@nonobjc public var buildVersion: String? {
get { _buildVersion.isEmpty ? nil : _buildVersion }
set { _buildVersion = newValue ?? "" }
}
@NSManaged @objc(buildVersion) public private(set) var _buildVersion: String
@NSManaged public var date: Date @NSManaged public var date: Date
@NSManaged public var localizedDescription: String? @NSManaged public var localizedDescription: String?
@NSManaged public var downloadURL: URL @NSManaged public var downloadURL: URL
@NSManaged public var size: Int64 @NSManaged public var size: Int64
@NSManaged public var sha256: String? @NSManaged public var sha256: String?
@@ -51,6 +59,7 @@ public class AppVersion: NSManagedObject, Decodable, Fetchable
private enum CodingKeys: String, CodingKey private enum CodingKeys: String, CodingKey
{ {
case version case version
case buildVersion
case date case date
case localizedDescription case localizedDescription
case downloadURL case downloadURL
@@ -71,6 +80,8 @@ public class AppVersion: NSManagedObject, Decodable, Fetchable
let container = try decoder.container(keyedBy: CodingKeys.self) let container = try decoder.container(keyedBy: CodingKeys.self)
self.version = try container.decode(String.self, forKey: .version) self.version = try container.decode(String.self, forKey: .version)
self.buildVersion = try container.decodeIfPresent(String.self, forKey: .buildVersion)
self.date = try container.decode(Date.self, forKey: .date) self.date = try container.decode(Date.self, forKey: .date)
self.localizedDescription = try container.decodeIfPresent(String.self, forKey: .localizedDescription) self.localizedDescription = try container.decodeIfPresent(String.self, forKey: .localizedDescription)
@@ -94,6 +105,26 @@ public class AppVersion: NSManagedObject, Decodable, Fetchable
} }
} }
public extension AppVersion
{
var localizedVersion: String {
guard let buildVersion else { return self.version }
let localizedVersion = "\(self.version) (\(buildVersion))"
return localizedVersion
}
var versionID: String {
// Use `nil` as fallback to prevent collisions between versions with builds and versions without.
// 1.5 (4) -> "1.5|4"
// 1.5.4 (no build) -> "1.5.4|nil"
let buildVersion = self.buildVersion ?? "nil"
let versionID = "\(self.version)|\(buildVersion)"
return versionID
}
}
public extension AppVersion public extension AppVersion
{ {
@nonobjc class func fetchRequest() -> NSFetchRequest<AppVersion> @nonobjc class func fetchRequest() -> NSFetchRequest<AppVersion>
@@ -103,6 +134,7 @@ public extension AppVersion
class func makeAppVersion( class func makeAppVersion(
version: String, version: String,
buildVersion: String?,
date: Date, date: Date,
localizedDescription: String? = nil, localizedDescription: String? = nil,
downloadURL: URL, downloadURL: URL,
@@ -113,6 +145,7 @@ public extension AppVersion
{ {
let appVersion = AppVersion(context: context) let appVersion = AppVersion(context: context)
appVersion.version = version appVersion.version = version
appVersion.buildVersion = buildVersion
appVersion.date = date appVersion.date = date
appVersion.localizedDescription = localizedDescription appVersion.localizedDescription = localizedDescription
appVersion.downloadURL = downloadURL appVersion.downloadURL = downloadURL

View File

@@ -229,8 +229,7 @@ private extension DatabaseManager
} }
else else
{ {
storeApp = StoreApp.makeAltStoreApp(in: context) storeApp = StoreApp.makeAltStoreApp(version: localApp.version, buildVersion: localApp.buildVersion, in: context)
storeApp.latestSupportedVersion?.version = localApp.version
storeApp.source = altStoreSource storeApp.source = altStoreSource
} }
@@ -278,7 +277,7 @@ private extension DatabaseManager
#if DEBUG #if DEBUG
let replaceCachedApp = true let replaceCachedApp = true
#else #else
let replaceCachedApp = !FileManager.default.fileExists(atPath: fileURL.path) || installedApp.version != localApp.version let replaceCachedApp = !FileManager.default.fileExists(atPath: fileURL.path) || installedApp.version != localApp.version || installedApp.buildVersion != localApp.buildVersion
#endif #endif
if replaceCachedApp if replaceCachedApp

View File

@@ -49,6 +49,7 @@ public class InstalledApp: NSManagedObject, InstalledAppProtocol
@NSManaged public var bundleIdentifier: String @NSManaged public var bundleIdentifier: String
@NSManaged public var resignedBundleIdentifier: String @NSManaged public var resignedBundleIdentifier: String
@NSManaged public var version: String @NSManaged public var version: String
@NSManaged public var buildVersion: String
@NSManaged public var refreshedDate: Date @NSManaged public var refreshedDate: Date
@NSManaged public var expirationDate: Date @NSManaged public var expirationDate: Date
@@ -118,13 +119,22 @@ public class InstalledApp: NSManagedObject, InstalledAppProtocol
self.update(resignedApp: resignedApp, certificateSerialNumber: certificateSerialNumber) self.update(resignedApp: resignedApp, certificateSerialNumber: certificateSerialNumber)
} }
}
public func update(resignedApp: ALTApplication, certificateSerialNumber: String?) public extension InstalledApp
{
var localizedVersion: String {
let localizedVersion = "\(self.version) (\(self.buildVersion))"
return localizedVersion
}
func update(resignedApp: ALTApplication, certificateSerialNumber: String?)
{ {
self.name = resignedApp.name self.name = resignedApp.name
self.resignedBundleIdentifier = resignedApp.bundleIdentifier self.resignedBundleIdentifier = resignedApp.bundleIdentifier
self.version = resignedApp.version self.version = resignedApp.version
self.buildVersion = resignedApp.buildVersion
self.certificateSerialNumber = certificateSerialNumber self.certificateSerialNumber = certificateSerialNumber
@@ -134,13 +144,13 @@ public class InstalledApp: NSManagedObject, InstalledAppProtocol
} }
} }
public func update(provisioningProfile: ALTProvisioningProfile) func update(provisioningProfile: ALTProvisioningProfile)
{ {
self.refreshedDate = provisioningProfile.creationDate self.refreshedDate = provisioningProfile.creationDate
self.expirationDate = provisioningProfile.expirationDate self.expirationDate = provisioningProfile.expirationDate
} }
public func loadIcon(completion: @escaping (Result<UIImage?, Error>) -> Void) func loadIcon(completion: @escaping (Result<UIImage?, Error>) -> Void)
{ {
let hasAlternateIcon = self.hasAlternateIcon let hasAlternateIcon = self.hasAlternateIcon
let alternateIconURL = self.alternateIconURL let alternateIconURL = self.alternateIconURL
@@ -165,6 +175,19 @@ public class InstalledApp: NSManagedObject, InstalledAppProtocol
} }
} }
} }
func matches(_ appVersion: AppVersion) -> Bool
{
// Versions MUST match exactly.
guard self.version == appVersion.version else { return false }
// If buildVersion is nil, return true because versions match.
guard let buildVersion = appVersion.buildVersion else { return true }
// If buildVersion != nil, compare buildVersions and return result.
let matchesAppVersion = (self.buildVersion == buildVersion)
return matchesAppVersion
}
} }
public extension InstalledApp public extension InstalledApp
@@ -186,13 +209,22 @@ public extension InstalledApp
class func supportedUpdatesFetchRequest() -> NSFetchRequest<InstalledApp> class func supportedUpdatesFetchRequest() -> NSFetchRequest<InstalledApp>
{ {
let predicate = NSPredicate(format: "%K != nil AND %K != %K", let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp>
#keyPath(InstalledApp.storeApp.latestSupportedVersion),
#keyPath(InstalledApp.version), #keyPath(InstalledApp.storeApp.latestSupportedVersion.version))
let fetchRequest = InstalledApp.updatesFetchRequest() let predicateFormat = [
fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [fetchRequest.predicate, predicate].compactMap { $0 }) // isActive && storeApp != nil && latestSupportedVersion != nil
"%K == YES AND %K != nil AND %K != nil",
"AND",
// (latestSupportedVersion.version != installedApp.version || (latestSupportedVersion.buildVersion != nil && latestSupportedVersion.buildVersion != installedApp.buildVersion))
"(%K != %K OR (%K != '' AND %K != %K))",
].joined(separator: " ")
fetchRequest.predicate = NSPredicate(format: predicateFormat,
#keyPath(InstalledApp.isActive), #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.storeApp.latestSupportedVersion),
#keyPath(InstalledApp.storeApp.latestSupportedVersion.version), #keyPath(InstalledApp.version),
#keyPath(InstalledApp.storeApp.latestSupportedVersion._buildVersion), #keyPath(InstalledApp.storeApp.latestSupportedVersion._buildVersion), #keyPath(InstalledApp.buildVersion))
return fetchRequest return fetchRequest
} }

View File

@@ -181,7 +181,7 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
return return
} }
var sortedVersionsByGlobalAppID = [String: NSOrderedSet]() var sortedVersionIDsByGlobalAppID = [String: NSOrderedSet]()
var permissionsByGlobalAppID = [String: Set<AnyHashable>]() var permissionsByGlobalAppID = [String: Set<AnyHashable>]()
var featuredAppIDsBySourceID = [String: [String]]() var featuredAppIDsBySourceID = [String: [String]]()
@@ -202,8 +202,8 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
} }
// Versions // Versions
let contextVersions = NSOrderedSet(array: contextApp._versions.lazy.compactMap { $0 as? AppVersion }.map { $0.version }) let contextVersionIDs = NSOrderedSet(array: contextApp._versions.lazy.compactMap { $0 as? AppVersion }.map { $0.versionID })
for case let databaseVersion as AppVersion in databaseObject._versions where !contextVersions.contains(databaseVersion.version) for case let databaseVersion as AppVersion in databaseObject._versions where !contextVersionIDs.contains(databaseVersion.versionID)
{ {
// Version # does NOT exist in context, so delete existing databaseVersion. // Version # does NOT exist in context, so delete existing databaseVersion.
databaseVersion.managedObjectContext?.delete(databaseVersion) databaseVersion.managedObjectContext?.delete(databaseVersion)
@@ -212,7 +212,7 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
if let globallyUniqueID = contextApp.globallyUniqueID if let globallyUniqueID = contextApp.globallyUniqueID
{ {
permissionsByGlobalAppID[globallyUniqueID] = contextPermissions permissionsByGlobalAppID[globallyUniqueID] = contextPermissions
sortedVersionsByGlobalAppID[globallyUniqueID] = contextVersions sortedVersionIDsByGlobalAppID[globallyUniqueID] = contextVersionIDs
} }
case let databaseObject as Source: case let databaseObject as Source:
@@ -271,21 +271,21 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
} }
// App versions // App versions
if let sortedAppVersions = sortedVersionsByGlobalAppID[globallyUniqueID], if let sortedAppVersionIDs = sortedVersionIDsByGlobalAppID[globallyUniqueID],
let sortedAppVersionsArray = sortedAppVersions.array as? [String], let sortedAppVersionsIDsArray = sortedAppVersionIDs.array as? [String],
case let databaseVersions = databaseObject.versions.map({ $0.version }), case let databaseVersionIDs = databaseObject.versions.map({ $0.versionID }),
databaseVersions != sortedAppVersionsArray databaseVersionIDs != sortedAppVersionsIDsArray
{ {
// databaseObject.versions post-merge doesn't match contextApp.versions pre-merge, so attempt to fix by re-sorting. // databaseObject.versions post-merge doesn't match contextApp.versions pre-merge, so attempt to fix by re-sorting.
let fixedAppVersions = databaseObject.versions.sorted { (versionA, versionB) in let fixedAppVersions = databaseObject.versions.sorted { (versionA, versionB) in
let indexA = sortedAppVersions.index(of: versionA.version) let indexA = sortedAppVersionIDs.index(of: versionA.versionID)
let indexB = sortedAppVersions.index(of: versionB.version) let indexB = sortedAppVersionIDs.index(of: versionB.versionID)
return indexA < indexB return indexA < indexB
} }
let appVersionValues = fixedAppVersions.map { $0.version } let appVersionIDs = fixedAppVersions.map { $0.versionID }
guard appVersionValues == sortedAppVersionsArray else { guard appVersionIDs == sortedAppVersionsIDsArray else {
// fixedAppVersions still doesn't match source's versions, so throw MergeError. // fixedAppVersions still doesn't match source's versions, so throw MergeError.
throw MergeError.incorrectVersionOrder(for: databaseObject) throw MergeError.incorrectVersionOrder(for: databaseObject)
} }

View File

@@ -316,6 +316,7 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
let size = try container.decode(Int32.self, forKey: .size) let size = try container.decode(Int32.self, forKey: .size)
let appVersion = AppVersion.makeAppVersion(version: version, let appVersion = AppVersion.makeAppVersion(version: version,
buildVersion: nil,
date: versionDate, date: versionDate,
localizedDescription: versionDescription, localizedDescription: versionDescription,
downloadURL: downloadURL, downloadURL: downloadURL,
@@ -407,7 +408,7 @@ public extension StoreApp
return NSFetchRequest<StoreApp>(entityName: "StoreApp") return NSFetchRequest<StoreApp>(entityName: "StoreApp")
} }
class func makeAltStoreApp(in context: NSManagedObjectContext) -> StoreApp class func makeAltStoreApp(version: String, buildVersion: String?, in context: NSManagedObjectContext) -> StoreApp
{ {
let app = StoreApp(context: context) let app = StoreApp(context: context)
app.name = "SideStore" app.name = "SideStore"
@@ -418,7 +419,8 @@ public extension StoreApp
app.screenshotURLs = [] app.screenshotURLs = []
app.sourceIdentifier = Source.altStoreIdentifier app.sourceIdentifier = Source.altStoreIdentifier
let appVersion = AppVersion.makeAppVersion(version: "0.3.0", let appVersion = AppVersion.makeAppVersion(version: version,
buildVersion: buildVersion,
date: Date(), date: Date(),
downloadURL: URL(string: "http://rileytestut.com")!, downloadURL: URL(string: "http://rileytestut.com")!,
size: 0, size: 0,