Supports new “versions” key in source JSON

Allows sources to list multiple versions of an app.

Preserves backwards compatibility by assigning legacy version values when assigning AppVersions.
This commit is contained in:
Riley Testut
2022-09-12 17:05:55 -07:00
committed by Joseph Mattello
parent 5765cb8330
commit fa160124d2
11 changed files with 198 additions and 52 deletions

View File

@@ -80,10 +80,21 @@ class AppContentViewController: UITableViewController
self.subtitleLabel.text = self.app.subtitle
self.descriptionTextView.text = self.app.localizedDescription
self.versionDescriptionTextView.text = self.app.versionDescription
self.versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), self.app.version)
self.versionDateLabel.text = Date().relativeDateString(since: self.app.versionDate, dateFormatter: self.dateFormatter)
self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: Int64(self.app.size))
if let version = self.app.latestVersion
{
self.versionDescriptionTextView.text = version.localizedDescription
self.versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), version.version)
self.versionDateLabel.text = Date().relativeDateString(since: version.date, dateFormatter: self.dateFormatter)
self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: version.size)
}
else
{
self.versionDescriptionTextView.text = nil
self.versionLabel.text = nil
self.versionDateLabel.text = nil
self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: 0)
}
self.descriptionTextView.maximumNumberOfLines = 5
self.descriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)

View File

@@ -384,10 +384,10 @@ private extension AppViewController
button.progress = progress
}
if Date() < self.app.versionDate
if let versionDate = self.app.latestVersion?.date, versionDate > Date()
{
self.bannerView.button.countdownDate = self.app.versionDate
self.navigationBarDownloadButton.countdownDate = self.app.versionDate
self.bannerView.button.countdownDate = versionDate
self.navigationBarDownloadButton.countdownDate = versionDate
}
else
{

View File

@@ -380,11 +380,11 @@ private extension AppDelegate
for update in updates
{
guard !previousUpdates.contains(where: { $0[#keyPath(InstalledApp.bundleIdentifier)] == update.bundleIdentifier }) else { continue }
guard let storeApp = update.storeApp else { continue }
guard let storeApp = update.storeApp, let version = storeApp.version else { continue }
let content = UNMutableNotificationContent()
content.title = NSLocalizedString("New Update Available", comment: "")
content.body = String(format: NSLocalizedString("%@ %@ is now available for download.", comment: ""), update.name, storeApp.version)
content.body = String(format: NSLocalizedString("%@ %@ is now available for download.", comment: ""), update.name, version)
content.sound = .default
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)

View File

@@ -113,7 +113,7 @@ private extension BrowseViewController
let progress = AppManager.shared.installationProgress(for: app)
cell.bannerView.button.progress = progress
if Date() < app.versionDate
if let versionDate = app.latestVersion?.date, versionDate > Date()
{
cell.bannerView.button.countdownDate = app.versionDate
}

View File

@@ -186,7 +186,7 @@ private extension MyAppsViewController
func makeUpdatesDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<InstalledApp, UIImage>
{
let fetchRequest = InstalledApp.updatesFetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \InstalledApp.storeApp?.versionDate, ascending: true),
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \InstalledApp.storeApp?.latestVersion?.date, ascending: true),
NSSortDescriptor(keyPath: \InstalledApp.name, ascending: true)]
fetchRequest.returnsObjectsAsFaults = false
@@ -195,7 +195,7 @@ private extension MyAppsViewController
dataSource.cellIdentifierHandler = { _ in "UpdateCell" }
dataSource.cellConfigurationHandler = { [weak self] (cell, installedApp, indexPath) in
guard let self = self else { return }
guard let app = installedApp.storeApp else { return }
guard let app = installedApp.storeApp, let latestVersion = app.latestVersion else { return }
let cell = cell as! UpdateCollectionViewCell
cell.layoutMargins.left = self.view.layoutMargins.left
@@ -209,7 +209,7 @@ private extension MyAppsViewController
cell.bannerView.configure(for: app)
let versionDate = Date().relativeDateString(since: app.versionDate, dateFormatter: self.dateFormatter)
let versionDate = Date().relativeDateString(since: latestVersion.date, dateFormatter: self.dateFormatter)
cell.bannerView.subtitleLabel.text = versionDate
let appName: String
@@ -223,7 +223,7 @@ private extension MyAppsViewController
appName = app.name
}
cell.bannerView.accessibilityLabel = String(format: NSLocalizedString("%@ %@ update. Released on %@.", comment: ""), appName, app.version, versionDate)
cell.bannerView.accessibilityLabel = String(format: NSLocalizedString("%@ %@ update. Released on %@.", comment: ""), appName, latestVersion.version, versionDate)
cell.bannerView.button.isIndicatingActivity = false
cell.bannerView.button.addTarget(self, action: #selector(MyAppsViewController.updateApp(_:)), for: .primaryActionTriggered)

View File

@@ -391,7 +391,7 @@ extension NewsViewController
let progress = AppManager.shared.installationProgress(for: storeApp)
footerView.bannerView.button.progress = progress
if Date() < storeApp.versionDate
if let versionDate = storeApp.latestVersion?.date, versionDate > Date()
{
footerView.bannerView.button.countdownDate = storeApp.versionDate
}

View File

@@ -47,7 +47,6 @@ public class AppVersion: NSManagedObject, Decodable, Fetchable
super.init(entity: entity, insertInto: context)
}
private enum CodingKeys: String, CodingKey
{
case version
@@ -55,31 +54,34 @@ public class AppVersion: NSManagedObject, Decodable, Fetchable
case localizedDescription
case downloadURL
case size
case _minOSVersion
case _maxOSVersion
case appBundleID
case sourceID
}
public required init(from decoder: Decoder) throws
{
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
super.init(entity: NewsItem.entity(), insertInto: context)
super.init(entity: AppVersion.entity(), insertInto: context)
let container = try decoder.container(keyedBy: CodingKeys.self)
self.version = try container.decode(String.self, forKey: .version)
self.date = try container.decode(Date.self, forKey: .date)
self.localizedDescription = try container.decodeIfPresent(String.self, forKey: .localizedDescription)
self.downloadURL = try container.decode(URL.self, forKey: .downloadURL)
self.size = try container.decode(Int64.self, forKey: .size)
self._minOSVersion = try container.decodeIfPresent(String.self, forKey: ._minOSVersion)
self._maxOSVersion = try container.decodeIfPresent(String.self, forKey: ._maxOSVersion)
self.appBundleID = try container.decode(String.self, forKey: .appBundleID)
self.sourceID = try container.decodeIfPresent(String.self, forKey: .sourceID)
do
{
let container = try decoder.container(keyedBy: CodingKeys.self)
self.version = try container.decode(String.self, forKey: .version)
self.date = try container.decode(Date.self, forKey: .date)
self.localizedDescription = try container.decodeIfPresent(String.self, forKey: .localizedDescription)
self.downloadURL = try container.decode(URL.self, forKey: .downloadURL)
self.size = try container.decode(Int64.self, forKey: .size)
}
catch
{
if let context = self.managedObjectContext
{
context.delete(self)
}
throw error
}
}
}
@@ -89,4 +91,26 @@ public extension AppVersion
{
return NSFetchRequest<AppVersion>(entityName: "AppVersion")
}
class func makeAppVersion(
version: String,
date: Date,
localizedDescription: String? = nil,
downloadURL: URL,
size: Int64,
appBundleID: String,
sourceID: String? = nil,
in context: NSManagedObjectContext) -> AppVersion
{
let appVersion = AppVersion(context: context)
appVersion.version = version
appVersion.date = date
appVersion.localizedDescription = localizedDescription
appVersion.downloadURL = downloadURL
appVersion.size = size
appVersion.appBundleID = appBundleID
appVersion.sourceID = sourceID
return appVersion
}
}

View File

@@ -211,7 +211,7 @@ private extension DatabaseManager
else
{
storeApp = StoreApp.makeAltStoreApp(in: context)
storeApp.version = localApp.version
storeApp.latestVersion?.version = localApp.version
storeApp.source = altStoreSource
}

View File

@@ -146,7 +146,7 @@ public extension InstalledApp
{
let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp>
fetchRequest.predicate = NSPredicate(format: "%K == YES AND %K != nil AND %K != %K",
#keyPath(InstalledApp.isActive), #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.version), #keyPath(InstalledApp.storeApp.version))
#keyPath(InstalledApp.isActive), #keyPath(InstalledApp.storeApp), #keyPath(InstalledApp.version), #keyPath(InstalledApp.storeApp.latestVersion.version))
return fetchRequest
}

View File

@@ -31,6 +31,12 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
{
permission.managedObjectContext?.delete(permission)
}
// Delete previous versions (different than below).
for case let appVersion as AppVersion in previousApp._versions where appVersion.app == nil
{
appVersion.managedObjectContext?.delete(appVersion)
}
}
default:
@@ -55,6 +61,16 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
permission.managedObjectContext?.delete(permission)
}
if let contextApp = conflict.conflictingObjects.first as? StoreApp
{
let contextVersions = Set(contextApp._versions.lazy.compactMap { $0 as? AppVersion }.map { $0.version })
for case let appVersion as AppVersion in databaseObject._versions where !contextVersions.contains(appVersion.version)
{
print("[ALTLog] Deleting cached app version: \(appVersion.appBundleID + "_" + appVersion.version), not in:", contextApp.versions.map { $0.appBundleID + "_" + $0.version })
appVersion.managedObjectContext?.delete(appVersion)
}
}
case let databaseObject as Source:
guard let conflictedObject = conflict.conflictingObjects.first as? Source else { break }

View File

@@ -103,22 +103,41 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
@NSManaged public private(set) var developerName: String
@NSManaged public private(set) var localizedDescription: String
@NSManaged public private(set) var size: Int32
@NSManaged @objc(size) private var _size: Int32
@NSManaged public private(set) var iconURL: URL
@NSManaged public private(set) var screenshotURLs: [URL]
@NSManaged public var version: String
@NSManaged public private(set) var versionDate: Date
@NSManaged public private(set) var versionDescription: String?
@NSManaged @objc(version) private var _version: String
@NSManaged @objc(versionDate) private var _versionDate: Date
@NSManaged @objc(versionDescription) private var _versionDescription: String?
@NSManaged public private(set) var downloadURL: URL
@NSManaged @objc(downloadURL) private var _downloadURL: URL
@NSManaged public private(set) var platformURLs: PlatformURLs?
@NSManaged public private(set) var tintColor: UIColor?
@NSManaged public private(set) var isBeta: Bool
@NSManaged public var sourceIdentifier: String?
@objc public internal(set) var sourceIdentifier: String? {
get {
self.willAccessValue(forKey: #keyPath(sourceIdentifier))
defer { self.didAccessValue(forKey: #keyPath(sourceIdentifier)) }
let sourceIdentifier = self.primitiveSourceIdentifier
return sourceIdentifier
}
set {
self.willChangeValue(forKey: #keyPath(sourceIdentifier))
self.primitiveSourceIdentifier = newValue
self.didChangeValue(forKey: #keyPath(sourceIdentifier))
for version in self.versions
{
version.sourceID = newValue
}
}
}
@NSManaged private var primitiveSourceIdentifier: String?
@NSManaged public var sortIndex: Int32
@@ -152,6 +171,31 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
return self._versions.array as! [AppVersion]
}
@nonobjc public var size: Int64? {
guard let version = self.latestVersion else { return nil }
return version.size
}
@nonobjc public var version: String? {
guard let version = self.latestVersion else { return nil }
return version.version
}
@nonobjc public var versionDescription: String? {
guard let version = self.latestVersion else { return nil }
return version.localizedDescription
}
@nonobjc public var versionDate: Date? {
guard let version = self.latestVersion else { return nil }
return version.date
}
@nonobjc public var downloadURL: URL? {
guard let version = self.latestVersion else { return nil }
return version.downloadURL
}
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
{
super.init(entity: entity, insertInto: context)
@@ -175,6 +219,7 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
case permissions
case size
case isBeta = "beta"
case versions
}
public required init(from decoder: Decoder) throws
@@ -194,10 +239,6 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
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) ?? []
@@ -207,14 +248,14 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
self.platformURLs = platformURLs
// Backwards compatibility, use the fiirst (iOS will be first since sorted that way)
if let first = platformURLs.sorted().first {
self.downloadURL = first.downloadURL
self._downloadURL = first.downloadURL
} else {
throw DecodingError.dataCorruptedError(forKey: .platformURLs, in: container, debugDescription: "platformURLs has no entries")
}
} else if let downloadURL = downloadURL {
self.downloadURL = downloadURL
self._downloadURL = downloadURL
} else {
throw DecodingError.dataCorruptedError(forKey: .downloadURL, in: container, debugDescription: "E downloadURL:String or downloadURLs:[[Platform:URL]] key required.")
}
@@ -228,11 +269,40 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
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)
if let versions = try container.decodeIfPresent([AppVersion].self, forKey: .versions)
{
//TODO: Throw error if there isn't at least one version.
for version in versions
{
version.appBundleID = self.bundleIdentifier
}
self.setVersions(versions)
}
else
{
let version = try container.decode(String.self, forKey: .version)
let versionDate = try container.decode(Date.self, forKey: .versionDate)
let versionDescription = try container.decodeIfPresent(String.self, forKey: .versionDescription)
let downloadURL = try container.decode(URL.self, forKey: .downloadURL)
let size = try container.decode(Int32.self, forKey: .size)
let appVersion = AppVersion.makeAppVersion(version: version,
date: versionDate,
localizedDescription: versionDescription,
downloadURL: downloadURL,
size: Int64(size),
appBundleID: self.bundleIdentifier,
in: context)
self.setVersions([appVersion])
}
}
catch
{
@@ -246,6 +316,24 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
}
}
private extension StoreApp
{
func setVersions(_ versions: [AppVersion])
{
guard let latestVersion = versions.first else { preconditionFailure("StoreApp must have at least one AppVersion.") }
self.latestVersion = latestVersion
self._versions = NSOrderedSet(array: versions)
// Preserve backwards compatibility by assigning legacy property values.
self._version = latestVersion.version
self._versionDate = latestVersion.date
self._versionDescription = latestVersion.localizedDescription
self._downloadURL = latestVersion.downloadURL
self._size = Int32(latestVersion.size)
}
}
public extension StoreApp
{
@nonobjc class func fetchRequest() -> NSFetchRequest<StoreApp>
@@ -262,9 +350,16 @@ public extension StoreApp
app.localizedDescription = "SideStore is an alternative App Store."
app.iconURL = URL(string: "https://user-images.githubusercontent.com/705880/63392210-540c5980-c37b-11e9-968c-8742fc68ab2e.png")!
app.screenshotURLs = []
app.version = "1.0"
app.versionDate = Date()
app.downloadURL = URL(string: "http://rileytestut.com")!
app.sourceIdentifier = Source.altStoreIdentifier
let appVersion = AppVersion.makeAppVersion(version: "1.0",
date: Date(),
downloadURL: URL(string: "http://rileytestut.com")!,
size: 0,
appBundleID: app.bundleIdentifier,
sourceID: Source.altStoreIdentifier,
in: context)
app.setVersions([appVersion])
#if BETA
app.isBeta = true