mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-19 11:43:24 +01:00
Revises Entitlements UI on app detail page
This commit is contained in:
@@ -116,6 +116,12 @@ class AppContentViewController: UITableViewController
|
|||||||
if self.appDetailCollectionViewHeightConstraint.constant != permissionsHeight && permissionsHeight > 0
|
if self.appDetailCollectionViewHeightConstraint.constant != permissionsHeight && permissionsHeight > 0
|
||||||
{
|
{
|
||||||
self.appDetailCollectionViewHeightConstraint.constant = permissionsHeight
|
self.appDetailCollectionViewHeightConstraint.constant = permissionsHeight
|
||||||
|
|
||||||
|
UIView.performWithoutAnimation {
|
||||||
|
// Update row height without animation.
|
||||||
|
self.tableView.beginUpdates()
|
||||||
|
self.tableView.endUpdates()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ extension AppDetailCollectionViewController
|
|||||||
private enum Section: Int
|
private enum Section: Int
|
||||||
{
|
{
|
||||||
case privacy
|
case privacy
|
||||||
case entitlements
|
case knownEntitlements
|
||||||
|
case unknownEntitlements
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum ElementKind: String
|
private enum ElementKind: String
|
||||||
@@ -45,7 +46,8 @@ class AppDetailCollectionViewController: UICollectionViewController
|
|||||||
{
|
{
|
||||||
let app: StoreApp
|
let app: StoreApp
|
||||||
private let privacyPermissions: [AppPermission]
|
private let privacyPermissions: [AppPermission]
|
||||||
private let entitlementPermissions: [AppPermission]
|
private let knownEntitlementPermissions: [AppPermission]
|
||||||
|
private let unknownEntitlementPermissions: [AppPermission]
|
||||||
|
|
||||||
private lazy var dataSource = self.makeDataSource()
|
private lazy var dataSource = self.makeDataSource()
|
||||||
private lazy var privacyDataSource = self.makePrivacyDataSource()
|
private lazy var privacyDataSource = self.makePrivacyDataSource()
|
||||||
@@ -79,7 +81,10 @@ class AppDetailCollectionViewController: UICollectionViewController
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.privacyPermissions = app.permissions.filter { $0.type == .privacy }.sorted(by: comparator)
|
self.privacyPermissions = app.permissions.filter { $0.type == .privacy }.sorted(by: comparator)
|
||||||
self.entitlementPermissions = app.permissions.filter { $0.type == .entitlement }.sorted(by: comparator)
|
|
||||||
|
let entitlementPermissions = app.permissions.lazy.filter { $0.type == .entitlement }
|
||||||
|
self.knownEntitlementPermissions = entitlementPermissions.filter { $0.isKnown }.sorted(by: comparator)
|
||||||
|
self.unknownEntitlementPermissions = entitlementPermissions.filter { !$0.isKnown }.sorted(by: comparator)
|
||||||
|
|
||||||
super.init(coder: coder)
|
super.init(coder: coder)
|
||||||
}
|
}
|
||||||
@@ -101,11 +106,35 @@ class AppDetailCollectionViewController: UICollectionViewController
|
|||||||
self.collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "PrivacyCell")
|
self.collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "PrivacyCell")
|
||||||
self.collectionView.register(UICollectionViewListCell.self, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier)
|
self.collectionView.register(UICollectionViewListCell.self, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier)
|
||||||
|
|
||||||
self.headerRegistration = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionHeader) { (headerView, elementKind, indexPath) in
|
self.headerRegistration = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionHeader) { [weak self] (headerView, elementKind, indexPath) in
|
||||||
var configuration = UIListContentConfiguration.plainHeader()
|
var configuration = UIListContentConfiguration.plainHeader()
|
||||||
configuration.text = NSLocalizedString("Entitlements", comment: "")
|
|
||||||
configuration.directionalLayoutMargins.bottom = 15
|
|
||||||
|
|
||||||
|
switch Section(rawValue: indexPath.section)!
|
||||||
|
{
|
||||||
|
case .privacy: break
|
||||||
|
case .knownEntitlements:
|
||||||
|
let fontDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .title3).withSymbolicTraits(.traitBold) ?? UIFontDescriptor.preferredFontDescriptor(withTextStyle: .title3)
|
||||||
|
configuration.textProperties.font = UIFont(descriptor: fontDescriptor, size: 0.0)
|
||||||
|
configuration.text = NSLocalizedString("Entitlements", comment: "")
|
||||||
|
|
||||||
|
configuration.secondaryTextProperties.font = UIFont.preferredFont(forTextStyle: .callout)
|
||||||
|
configuration.textToSecondaryTextVerticalPadding = 8
|
||||||
|
configuration.secondaryText = NSLocalizedString("Entitlements are additional permissions that grant access to certain system services, including potentially sensitive information. We recommend reviewing these before sideloading.", comment: "")
|
||||||
|
|
||||||
|
case .unknownEntitlements:
|
||||||
|
let fontDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body).withSymbolicTraits(.traitBold) ?? UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body)
|
||||||
|
configuration.textProperties.font = UIFont(descriptor: fontDescriptor, size: 0.0)
|
||||||
|
configuration.text = NSLocalizedString("Other Entitlements", comment: "")
|
||||||
|
|
||||||
|
let action = UIAction(image: UIImage(systemName: "questionmark.circle")) { _ in
|
||||||
|
self?.showUnknownEntitlementsAlert()
|
||||||
|
}
|
||||||
|
|
||||||
|
let helpButton = UIButton(primaryAction: action)
|
||||||
|
let customAccessory = UICellAccessory.customView(configuration: .init(customView: helpButton, placement: .trailing(), tintColor: self?.app.tintColor ?? .altPrimary))
|
||||||
|
headerView.accessories = [customAccessory]
|
||||||
|
}
|
||||||
|
|
||||||
headerView.contentConfiguration = configuration
|
headerView.contentConfiguration = configuration
|
||||||
headerView.backgroundConfiguration = UIBackgroundConfiguration.clear()
|
headerView.backgroundConfiguration = UIBackgroundConfiguration.clear()
|
||||||
}
|
}
|
||||||
@@ -120,7 +149,7 @@ private extension AppDetailCollectionViewController
|
|||||||
{
|
{
|
||||||
func makeLayout() -> UICollectionViewCompositionalLayout
|
func makeLayout() -> UICollectionViewCompositionalLayout
|
||||||
{
|
{
|
||||||
let layout = UICollectionViewCompositionalLayout(sectionProvider: { [privacyPermissions, entitlementPermissions] (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
|
let layout = UICollectionViewCompositionalLayout(sectionProvider: { [privacyPermissions, knownEntitlementPermissions, unknownEntitlementPermissions] (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
|
||||||
guard let section = Section(rawValue: sectionIndex) else { return nil }
|
guard let section = Section(rawValue: sectionIndex) else { return nil }
|
||||||
switch section
|
switch section
|
||||||
{
|
{
|
||||||
@@ -137,15 +166,18 @@ private extension AppDetailCollectionViewController
|
|||||||
layoutSection.interGroupSpacing = 10
|
layoutSection.interGroupSpacing = 10
|
||||||
return layoutSection
|
return layoutSection
|
||||||
|
|
||||||
case .entitlements:
|
case .knownEntitlements where !knownEntitlementPermissions.isEmpty: fallthrough
|
||||||
guard !entitlementPermissions.isEmpty else { return nil }
|
case .unknownEntitlements where !unknownEntitlementPermissions.isEmpty:
|
||||||
|
|
||||||
var configuration = UICollectionLayoutListConfiguration(appearance: .plain)
|
var configuration = UICollectionLayoutListConfiguration(appearance: .plain)
|
||||||
configuration.headerMode = .supplementary
|
configuration.headerMode = .supplementary
|
||||||
|
configuration.showsSeparators = false
|
||||||
configuration.backgroundColor = .altBackground
|
configuration.backgroundColor = .altBackground
|
||||||
|
|
||||||
let layoutSection = NSCollectionLayoutSection.list(using: configuration, layoutEnvironment: layoutEnvironment)
|
let layoutSection = NSCollectionLayoutSection.list(using: configuration, layoutEnvironment: layoutEnvironment)
|
||||||
|
layoutSection.contentInsets.top = 4
|
||||||
return layoutSection
|
return layoutSection
|
||||||
|
|
||||||
|
case .knownEntitlements, .unknownEntitlements: return nil
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -186,49 +218,61 @@ private extension AppDetailCollectionViewController
|
|||||||
|
|
||||||
return dataSource
|
return dataSource
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeEntitlementsDataSource() -> RSTArrayCollectionViewDataSource<AppPermission>
|
func makeEntitlementsDataSource() -> RSTCompositeCollectionViewDataSource<AppPermission>
|
||||||
{
|
{
|
||||||
let dataSource = RSTArrayCollectionViewDataSource(items: self.entitlementPermissions)
|
let knownEntitlementsDataSource = RSTArrayCollectionViewDataSource(items: self.knownEntitlementPermissions)
|
||||||
dataSource.cellConfigurationHandler = { [weak self] (cell, appPermission, indexPath) in
|
let unknownEntitlementsDataSource = RSTArrayCollectionViewDataSource(items: self.unknownEntitlementPermissions)
|
||||||
|
|
||||||
|
let dataSource = RSTCompositeCollectionViewDataSource(dataSources: [knownEntitlementsDataSource, unknownEntitlementsDataSource])
|
||||||
|
dataSource.cellConfigurationHandler = { [weak self] (cell, appPermission, _) in
|
||||||
let cell = cell as! UICollectionViewListCell
|
let cell = cell as! UICollectionViewListCell
|
||||||
|
let tintColor = self?.app.tintColor ?? .altPrimary
|
||||||
|
|
||||||
var content = cell.defaultContentConfiguration()
|
var content = cell.defaultContentConfiguration()
|
||||||
content.image = UIImage(systemName: appPermission.effectiveSymbolName)
|
content.text = appPermission.localizedDisplayName
|
||||||
|
content.secondaryText = appPermission.permission.rawValue
|
||||||
|
content.secondaryTextProperties.color = .secondaryLabel
|
||||||
|
|
||||||
let tintColor = self?.app.tintColor ?? .altPrimary
|
if appPermission.isKnown
|
||||||
content.imageProperties.tintColor = tintColor
|
|
||||||
|
|
||||||
if let name = appPermission.localizedName
|
|
||||||
{
|
{
|
||||||
content.text = name
|
content.image = UIImage(systemName: appPermission.effectiveSymbolName)
|
||||||
content.secondaryText = appPermission.permission.rawValue
|
content.imageProperties.tintColor = tintColor
|
||||||
content.secondaryTextProperties.color = UIColor.secondaryLabel
|
|
||||||
}
|
if #available(iOS 15.4, *) /*, let self */ // Capturing self leads to strong-reference cycle.
|
||||||
else
|
{
|
||||||
{
|
let detailAccessory = UICellAccessory.detail(options: .init(tintColor: tintColor)) {
|
||||||
content.text = appPermission.permission.rawValue
|
self?.showPermissionAlert(for: appPermission)
|
||||||
|
}
|
||||||
|
cell.accessories = [detailAccessory]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cell.contentConfiguration = content
|
cell.contentConfiguration = content
|
||||||
cell.backgroundConfiguration = UIBackgroundConfiguration.clear()
|
cell.backgroundConfiguration = UIBackgroundConfiguration.clear()
|
||||||
|
|
||||||
if #available(iOS 15.4, *) /*, let self */ // Capturing self leads to strong-reference cycle.
|
|
||||||
{
|
|
||||||
let detailAccessory = UICellAccessory.detail(displayed: .always, options: .init(tintColor: tintColor)) {
|
|
||||||
let alertController = UIAlertController(title: appPermission.localizedDisplayName, message: appPermission.localizedDescription, preferredStyle: .alert)
|
|
||||||
alertController.addAction(.ok)
|
|
||||||
self?.present(alertController, animated: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
cell.accessories = [detailAccessory]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return dataSource
|
return dataSource
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extension AppDetailCollectionViewController
|
||||||
|
{
|
||||||
|
func showPermissionAlert(for permission: AppPermission)
|
||||||
|
{
|
||||||
|
let alertController = UIAlertController(title: permission.localizedDisplayName, message: permission.localizedDescription, preferredStyle: .alert)
|
||||||
|
alertController.addAction(.ok)
|
||||||
|
self.present(alertController, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func showUnknownEntitlementsAlert()
|
||||||
|
{
|
||||||
|
let alertController = UIAlertController(title: NSLocalizedString("Other Entitlements", comment: ""), message: NSLocalizedString("AltStore does not have detailed information for these entitlements.", comment: ""), preferredStyle: .alert)
|
||||||
|
alertController.addAction(.ok)
|
||||||
|
self.present(alertController, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension AppDetailCollectionViewController
|
extension AppDetailCollectionViewController
|
||||||
{
|
{
|
||||||
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView
|
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView
|
||||||
|
|||||||
@@ -28,21 +28,33 @@ public protocol ALTAppPermission: RawRepresentable<String>, Hashable
|
|||||||
var symbolName: String? { get }
|
var symbolName: String? { get }
|
||||||
|
|
||||||
var localizedName: String? { get }
|
var localizedName: String? { get }
|
||||||
|
var synthesizedName: String? { get } // Kupo!
|
||||||
|
|
||||||
var localizedDescription: String? { get }
|
var localizedDescription: String? { get }
|
||||||
|
|
||||||
// Default implementations
|
// Convenience properties with default implementations.
|
||||||
|
// Would normally just be in extension, except that crashes Swift 5.8 compiler ¯\_(ツ)_/¯
|
||||||
|
var isKnown: Bool { get }
|
||||||
var effectiveSymbolName: String { get }
|
var effectiveSymbolName: String { get }
|
||||||
var localizedDisplayName: String { get }
|
var localizedDisplayName: String { get }
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension ALTAppPermission
|
public extension ALTAppPermission
|
||||||
{
|
{
|
||||||
|
var isKnown: Bool {
|
||||||
|
// Assume all known permissions have non-nil localizedDescriptions.
|
||||||
|
return self.localizedDescription != nil
|
||||||
|
}
|
||||||
|
|
||||||
var effectiveSymbolName: String { self.symbolName ?? "lock" }
|
var effectiveSymbolName: String { self.symbolName ?? "lock" }
|
||||||
|
|
||||||
var localizedDisplayName: String {
|
var localizedDisplayName: String {
|
||||||
return self.localizedName ?? self.rawValue
|
return self.localizedName ?? self.synthesizedName ?? self.rawValue
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension ALTAppPermission
|
||||||
|
{
|
||||||
func isEqual(_ permission: any ALTAppPermission) -> Bool
|
func isEqual(_ permission: any ALTAppPermission) -> Bool
|
||||||
{
|
{
|
||||||
guard let permission = permission as? Self else { return false }
|
guard let permission = permission as? Self else { return false }
|
||||||
@@ -61,6 +73,8 @@ public struct UnknownAppPermission: ALTAppPermission
|
|||||||
public var symbolName: String? { nil }
|
public var symbolName: String? { nil }
|
||||||
|
|
||||||
public var localizedName: String? { nil }
|
public var localizedName: String? { nil }
|
||||||
|
public var synthesizedName: String? { nil }
|
||||||
|
|
||||||
public var localizedDescription: String? { nil }
|
public var localizedDescription: String? { nil }
|
||||||
|
|
||||||
public var rawValue: String
|
public var rawValue: String
|
||||||
@@ -77,6 +91,25 @@ extension ALTEntitlement: ALTAppPermission
|
|||||||
public var symbolName: String? { nil }
|
public var symbolName: String? { nil }
|
||||||
|
|
||||||
public var localizedName: String? { nil }
|
public var localizedName: String? { nil }
|
||||||
|
|
||||||
|
public var synthesizedName: String? {
|
||||||
|
// Attempt to convert last component of entitlement to human-readable string.
|
||||||
|
// e.g. com.apple.developer.kernel.increased-memory-limit -> "Increased Memory Limit"
|
||||||
|
let components = self.rawValue.components(separatedBy: ".")
|
||||||
|
guard let rawName = components.last else { return nil }
|
||||||
|
|
||||||
|
let words = rawName.components(separatedBy: "-").map { word in
|
||||||
|
switch word.lowercased()
|
||||||
|
{
|
||||||
|
case "carplay": return NSLocalizedString("CarPlay", comment: "")
|
||||||
|
default: return word.localizedCapitalized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let synthesizedName = words.joined(separator: " ")
|
||||||
|
return synthesizedName
|
||||||
|
}
|
||||||
|
|
||||||
public var localizedDescription: String? { nil }
|
public var localizedDescription: String? { nil }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,6 +132,7 @@ extension ALTAppPrivacyPermission: ALTAppPermission
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var synthesizedName: String? { nil }
|
||||||
public var localizedDescription: String? { nil }
|
public var localizedDescription: String? { nil }
|
||||||
|
|
||||||
public var symbolName: String? {
|
public var symbolName: String? {
|
||||||
@@ -123,5 +157,7 @@ extension ALTAppBackgroundMode: ALTAppPermission
|
|||||||
public var symbolName: String? { nil }
|
public var symbolName: String? { nil }
|
||||||
|
|
||||||
public var localizedName: String? { nil }
|
public var localizedName: String? { nil }
|
||||||
|
public var synthesizedName: String? { nil }
|
||||||
|
|
||||||
public var localizedDescription: String? { nil }
|
public var localizedDescription: String? { nil }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user