2023-05-24 15:56:06 -05:00
//
// A p p D e t a i l C o l l e c t i o n V i e w C o n t r o l l e r . s w i f t
// A l t S t o r e
//
// C r e a t e d b y R i l e y T e s t u t o n 5 / 5 / 2 3 .
// C o p y r i g h t © 2 0 2 3 R i l e y T e s t u t . A l l r i g h t s r e s e r v e d .
//
import UIKit
import SwiftUI
import AltStoreCore
import Roxas
extension AppDetailCollectionViewController
{
private enum Section : Int
{
case privacy
2023-05-25 18:03:21 -05:00
case knownEntitlements
case unknownEntitlements
2023-05-24 15:56:06 -05:00
}
private enum ElementKind : String
{
case title
case button
}
@objc ( SafeAreaIgnoringCollectionView )
private class SafeAreaIgnoringCollectionView : UICollectionView
{
override var safeAreaInsets : UIEdgeInsets {
get {
// F i x e s i n c o r r e c t l a y o u t i f c o l l e c t i o n v i e w h e i g h t i s t a l l e r t h a n s a f e a r e a h e i g h t .
return . zero
}
set {
// T h e r e M U S T b e a s e t t e r f o r t h i s t o w o r k , e v e n i f i t d o e s n o t h i n g ¯ \ _ ( ツ ) _ / ¯
}
}
}
}
class AppDetailCollectionViewController : UICollectionViewController
{
let app : StoreApp
private let privacyPermissions : [ AppPermission ]
2023-05-25 18:03:21 -05:00
private let knownEntitlementPermissions : [ AppPermission ]
private let unknownEntitlementPermissions : [ AppPermission ]
2023-05-24 15:56:06 -05:00
private lazy var dataSource = self . makeDataSource ( )
private lazy var privacyDataSource = self . makePrivacyDataSource ( )
private lazy var entitlementsDataSource = self . makeEntitlementsDataSource ( )
private var headerRegistration : UICollectionView . SupplementaryRegistration < UICollectionViewListCell > !
override var collectionViewLayout : UICollectionViewCompositionalLayout {
return self . collectionView . collectionViewLayout as ! UICollectionViewCompositionalLayout
}
init ? ( app : StoreApp , coder : NSCoder )
{
self . app = app
let comparator : ( AppPermission , AppPermission ) -> Bool = { ( permissionA , permissionB ) -> Bool in
switch ( permissionA . localizedName , permissionB . localizedName )
{
case ( let nameA ? , let nameB ? ) :
// S o r t b y l o c a l i z e d N a m e , i f b o t h h a v e o n e .
return nameA . localizedStandardCompare ( nameB ) = = . orderedAscending
case ( nil , nil ) :
// S o r t b y r a w p e r m i s s i o n v a l u e a s f a l l b a c k .
return permissionA . permission . rawValue < permissionB . permission . rawValue
// S o r t " k n o w n " p e r m i s s i o n s b e f o r e " u n k n o w n " o n e s .
case ( _ ? , nil ) : return true
case ( nil , _ ? ) : return false
}
}
self . privacyPermissions = app . permissions . filter { $0 . type = = . privacy } . sorted ( by : comparator )
2023-05-25 18:03:21 -05:00
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 )
2023-05-24 15:56:06 -05:00
super . init ( coder : coder )
}
required init ? ( coder : NSCoder ) {
fatalError ( " init(coder:) has not been implemented " )
}
override func viewDidLoad ( )
{
super . viewDidLoad ( )
// A l l o w p a r e n t b a c k g r o u n d c o l o r t o s h o w t h r o u g h .
self . collectionView . backgroundColor = nil
2023-05-26 16:10:21 -05:00
// M a t c h t h e p a r e n t t a b l e v i e w m a r g i n s .
self . collectionView . directionalLayoutMargins . leading = 20
self . collectionView . directionalLayoutMargins . trailing = 20
2023-05-24 15:56:06 -05:00
let collectionViewLayout = self . makeLayout ( )
self . collectionView . collectionViewLayout = collectionViewLayout
self . collectionView . register ( UICollectionViewCell . self , forCellWithReuseIdentifier : " PrivacyCell " )
self . collectionView . register ( UICollectionViewListCell . self , forCellWithReuseIdentifier : RSTCellContentGenericCellIdentifier )
2023-05-25 18:03:21 -05:00
self . headerRegistration = UICollectionView . SupplementaryRegistration < UICollectionViewListCell > ( elementKind : UICollectionView . elementKindSectionHeader ) { [ weak self ] ( headerView , elementKind , indexPath ) in
2023-05-24 15:56:06 -05:00
var configuration = UIListContentConfiguration . plainHeader ( )
2023-05-26 16:10:21 -05:00
// M a t c h p a r e n t t a b l e v i e w s e c t i o n h e a d e r s .
configuration . textProperties . font = UIFont . systemFont ( ofSize : 22 , weight : . bold ) // . b o l d S y s t e m F o n t ( o f S i z e : ) r e t u r n s * s e m i - b o l d * c o l o r s m h .
configuration . textProperties . color = . label
2023-05-25 18:03:21 -05:00
switch Section ( rawValue : indexPath . section ) !
{
case . privacy : break
case . knownEntitlements :
2023-05-29 16:30:09 -05:00
configuration . text = nil
2023-05-25 18:03:21 -05:00
configuration . secondaryTextProperties . font = UIFont . preferredFont ( forTextStyle : . callout )
configuration . textToSecondaryTextVerticalPadding = 8
2023-05-26 16:10:21 -05:00
configuration . secondaryText = NSLocalizedString ( " Entitlements are additional permissions that grant access to certain system services, including potentially sensitive information. " , comment : " " )
2023-05-25 18:03:21 -05:00
case . unknownEntitlements :
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 ]
}
2023-05-24 15:56:06 -05:00
headerView . contentConfiguration = configuration
headerView . backgroundConfiguration = UIBackgroundConfiguration . clear ( )
}
self . dataSource . proxy = self
self . collectionView . dataSource = self . dataSource
self . collectionView . delegate = self
}
}
private extension AppDetailCollectionViewController
{
func makeLayout ( ) -> UICollectionViewCompositionalLayout
{
2023-05-26 16:10:21 -05:00
let layoutConfig = UICollectionViewCompositionalLayoutConfiguration ( )
layoutConfig . contentInsetsReference = . layoutMargins
2023-05-25 18:03:21 -05:00
let layout = UICollectionViewCompositionalLayout ( sectionProvider : { [ privacyPermissions , knownEntitlementPermissions , unknownEntitlementPermissions ] ( sectionIndex , layoutEnvironment ) -> NSCollectionLayoutSection ? in
2023-05-24 15:56:06 -05:00
guard let section = Section ( rawValue : sectionIndex ) else { return nil }
switch section
{
case . privacy :
guard ! privacyPermissions . isEmpty , #available ( iOS 16 , * ) else { return nil } // H i d e s e c t i o n p r e - i O S 1 6 .
let itemSize = NSCollectionLayoutSize ( widthDimension : . fractionalWidth ( 1.0 ) , heightDimension : . estimated ( 50 ) ) // U n d e r e s t i m a t e h e i g h t t o p r e v e n t j u m p i n g s i z e a b r u p t l y .
let item = NSCollectionLayoutItem ( layoutSize : itemSize )
let groupSize = NSCollectionLayoutSize ( widthDimension : . fractionalWidth ( 1.0 ) , heightDimension : . estimated ( 50 ) )
let group = NSCollectionLayoutGroup . horizontal ( layoutSize : groupSize , subitems : [ item ] )
let layoutSection = NSCollectionLayoutSection ( group : group )
layoutSection . interGroupSpacing = 10
return layoutSection
2023-05-25 18:03:21 -05:00
case . knownEntitlements where ! knownEntitlementPermissions . isEmpty : fallthrough
case . unknownEntitlements where ! unknownEntitlementPermissions . isEmpty :
2023-05-24 15:56:06 -05:00
var configuration = UICollectionLayoutListConfiguration ( appearance : . plain )
configuration . headerMode = . supplementary
2023-05-25 18:03:21 -05:00
configuration . showsSeparators = false
2023-05-24 15:56:06 -05:00
configuration . backgroundColor = . altBackground
let layoutSection = NSCollectionLayoutSection . list ( using : configuration , layoutEnvironment : layoutEnvironment )
2023-05-25 18:03:21 -05:00
layoutSection . contentInsets . top = 4
2023-05-24 15:56:06 -05:00
return layoutSection
2023-05-25 18:03:21 -05:00
case . knownEntitlements , . unknownEntitlements : return nil
2023-05-24 15:56:06 -05:00
}
2023-05-26 16:10:21 -05:00
} , configuration : layoutConfig )
2023-05-24 15:56:06 -05:00
return layout
}
func makeDataSource ( ) -> RSTCompositeCollectionViewDataSource < AppPermission >
{
let dataSource = RSTCompositeCollectionViewDataSource ( dataSources : [ self . privacyDataSource , self . entitlementsDataSource ] )
return dataSource
}
func makePrivacyDataSource ( ) -> RSTDynamicCollectionViewDataSource < AppPermission >
{
let dataSource = RSTDynamicCollectionViewDataSource < AppPermission > ( )
dataSource . cellIdentifierHandler = { _ in " PrivacyCell " }
dataSource . numberOfSectionsHandler = { 1 }
dataSource . cellConfigurationHandler = { [ weak self ] ( cell , _ , indexPath ) in
guard let self , #available ( iOS 16 , * ) else { return }
cell . contentConfiguration = UIHostingConfiguration {
2023-05-29 16:30:09 -05:00
AppPermissionsCard ( title : " Privacy " ,
2023-05-24 15:56:06 -05:00
description : " \( self . app . name ) may request access to the following: " ,
tintColor : Color ( uiColor : self . app . tintColor ? ? . altPrimary ) ,
permissions : self . privacyPermissions )
}
2023-05-26 16:10:21 -05:00
. margins ( . horizontal , 0 )
2023-05-24 15:56:06 -05:00
}
if #available ( iOS 16 , * )
{
dataSource . numberOfItemsHandler = { [ privacyPermissions ] _ in ! privacyPermissions . isEmpty ? 1 : 0 }
}
else
{
dataSource . numberOfItemsHandler = { _ in 0 }
}
return dataSource
}
2023-05-25 18:03:21 -05:00
func makeEntitlementsDataSource ( ) -> RSTCompositeCollectionViewDataSource < AppPermission >
2023-05-24 15:56:06 -05:00
{
2023-05-25 18:03:21 -05:00
let knownEntitlementsDataSource = RSTArrayCollectionViewDataSource ( items : self . knownEntitlementPermissions )
let unknownEntitlementsDataSource = RSTArrayCollectionViewDataSource ( items : self . unknownEntitlementPermissions )
let dataSource = RSTCompositeCollectionViewDataSource ( dataSources : [ knownEntitlementsDataSource , unknownEntitlementsDataSource ] )
dataSource . cellConfigurationHandler = { [ weak self ] ( cell , appPermission , _ ) in
2023-05-24 15:56:06 -05:00
let cell = cell as ! UICollectionViewListCell
2023-05-25 18:03:21 -05:00
let tintColor = self ? . app . tintColor ? ? . altPrimary
2023-05-24 15:56:06 -05:00
var content = cell . defaultContentConfiguration ( )
2023-05-25 18:03:21 -05:00
content . text = appPermission . localizedDisplayName
content . secondaryText = appPermission . permission . rawValue
content . secondaryTextProperties . color = . secondaryLabel
2023-05-24 15:56:06 -05:00
2023-05-25 18:03:21 -05:00
if appPermission . isKnown
2023-05-24 15:56:06 -05:00
{
2023-05-25 18:03:21 -05:00
content . image = UIImage ( systemName : appPermission . effectiveSymbolName )
content . imageProperties . tintColor = tintColor
if #available ( iOS 15.4 , * ) /* , l e t s e l f */ // C a p t u r i n g s e l f l e a d s t o s t r o n g - r e f e r e n c e c y c l e .
{
let detailAccessory = UICellAccessory . detail ( options : . init ( tintColor : tintColor ) ) {
self ? . showPermissionAlert ( for : appPermission )
}
cell . accessories = [ detailAccessory ]
}
2023-05-24 15:56:06 -05:00
}
cell . contentConfiguration = content
cell . backgroundConfiguration = UIBackgroundConfiguration . clear ( )
}
2023-05-25 18:03:21 -05:00
2023-05-24 15:56:06 -05:00
return dataSource
}
}
2023-05-25 18:03:21 -05:00
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 ( )
{
2024-12-14 18:23:33 -05:00
let alertController = UIAlertController ( title : NSLocalizedString ( " Other Entitlements " , comment : " " ) , message : NSLocalizedString ( " SideStore does not have detailed information for these entitlements. " , comment : " " ) , preferredStyle : . alert )
2023-05-25 18:03:21 -05:00
alertController . addAction ( . ok )
self . present ( alertController , animated : true )
}
}
2023-05-24 15:56:06 -05:00
extension AppDetailCollectionViewController
{
override func collectionView ( _ collectionView : UICollectionView , viewForSupplementaryElementOfKind kind : String , at indexPath : IndexPath ) -> UICollectionReusableView
{
let headerView = self . collectionView . dequeueConfiguredReusableSupplementary ( using : self . headerRegistration , for : indexPath )
return headerView
}
override func collectionView ( _ collectionView : UICollectionView , shouldHighlightItemAt indexPath : IndexPath ) -> Bool
{
return false
}
override func collectionView ( _ collectionView : UICollectionView , shouldSelectItemAt indexPath : IndexPath ) -> Bool
{
return false
}
}