Adds basic MyAppsViewController implementation

This commit is contained in:
Riley Testut
2019-05-20 21:26:01 +02:00
parent c3a8abf8dc
commit 42734f2004
9 changed files with 263 additions and 45 deletions

View File

@@ -14,9 +14,10 @@
BFB1169D22932DB100BB457C /* Apps.json in Resources */ = {isa = PBXBuildFile; fileRef = BFB1169C22932DB100BB457C /* Apps.json */; };
BFBBE2DD22931B20002097FA /* AltStore.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = BFBBE2DB22931B20002097FA /* AltStore.xcdatamodeld */; };
BFBBE2DF22931F73002097FA /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFBBE2DE22931F73002097FA /* App.swift */; };
BFBBE2E122931F81002097FA /* InstalledApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFBBE2E022931F81002097FA /* InstalledApp.swift */; };
BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2476D2284B9A500981D42 /* AppDelegate.swift */; };
BFD247702284B9A500981D42 /* AppsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2476F2284B9A500981D42 /* AppsViewController.swift */; };
BFD247722284B9A500981D42 /* SecondViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD247712284B9A500981D42 /* SecondViewController.swift */; };
BFD247722284B9A500981D42 /* MyAppsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD247712284B9A500981D42 /* MyAppsViewController.swift */; };
BFD247752284B9A500981D42 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFD247732284B9A500981D42 /* Main.storyboard */; };
BFD247772284B9A700981D42 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BFD247762284B9A700981D42 /* Assets.xcassets */; };
BFD2477A2284B9A700981D42 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFD247782284B9A700981D42 /* LaunchScreen.storyboard */; };
@@ -51,10 +52,11 @@
BFB1169C22932DB100BB457C /* Apps.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = Apps.json; sourceTree = "<group>"; };
BFBBE2DC22931B20002097FA /* AltStore.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = AltStore.xcdatamodel; sourceTree = "<group>"; };
BFBBE2DE22931F73002097FA /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
BFBBE2E022931F81002097FA /* InstalledApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledApp.swift; sourceTree = "<group>"; };
BFD2476A2284B9A500981D42 /* AltStore.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AltStore.app; sourceTree = BUILT_PRODUCTS_DIR; };
BFD2476D2284B9A500981D42 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
BFD2476F2284B9A500981D42 /* AppsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppsViewController.swift; sourceTree = "<group>"; };
BFD247712284B9A500981D42 /* SecondViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondViewController.swift; sourceTree = "<group>"; };
BFD247712284B9A500981D42 /* MyAppsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyAppsViewController.swift; sourceTree = "<group>"; };
BFD247742284B9A500981D42 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
BFD247762284B9A700981D42 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
BFD247792284B9A700981D42 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
@@ -80,6 +82,14 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
BFBBE2E2229320A2002097FA /* My Apps */ = {
isa = PBXGroup;
children = (
BFD247712284B9A500981D42 /* MyAppsViewController.swift */,
);
path = "My Apps";
sourceTree = "<group>";
};
BFD247612284B9A500981D42 = {
isa = PBXGroup;
children = (
@@ -103,10 +113,10 @@
BFD2476D2284B9A500981D42 /* AppDelegate.swift */,
BFD247732284B9A500981D42 /* Main.storyboard */,
BFD2478A2284C49000981D42 /* Apps */,
BFBBE2E2229320A2002097FA /* My Apps */,
BFD247982284D7FC00981D42 /* Model */,
BFD2478D2284C4C700981D42 /* Components */,
BFD2479D2284FBBD00981D42 /* Extensions */,
BFD247712284B9A500981D42 /* SecondViewController.swift */,
BFD247962284D7C100981D42 /* Resources */,
BFD247972284D7D800981D42 /* Supporting Files */,
);
@@ -165,6 +175,7 @@
BFBBE2DB22931B20002097FA /* AltStore.xcdatamodeld */,
BFB11691229322E400BB457C /* DatabaseManager.swift */,
BFBBE2DE22931F73002097FA /* App.swift */,
BFBBE2E022931F81002097FA /* InstalledApp.swift */,
);
path = Model;
sourceTree = "<group>";
@@ -252,8 +263,9 @@
buildActionMask = 2147483647;
files = (
BFD2478F2284C8F900981D42 /* Button.swift in Sources */,
BFD247722284B9A500981D42 /* SecondViewController.swift in Sources */,
BFD247722284B9A500981D42 /* MyAppsViewController.swift in Sources */,
BFB11692229322E400BB457C /* DatabaseManager.swift in Sources */,
BFBBE2E122931F81002097FA /* InstalledApp.swift in Sources */,
BFBBE2DF22931F73002097FA /* App.swift in Sources */,
BFD2478C2284C4C300981D42 /* AppIconImageView.swift in Sources */,
BFD2479C2284E19A00981D42 /* AppDetailViewController.swift in Sources */,

View File

@@ -48,6 +48,8 @@ class AppDetailViewController: UITableViewController
self.tableView.delegate = self
self.screenshotsCollectionView.dataSource = self.screenshotsDataSource
self.downloadButton.activityIndicatorView.style = .white
self.update()
}
@@ -95,6 +97,39 @@ private extension AppDetailViewController
}
}
private extension AppDetailViewController
{
@IBAction func downloadApp(_ sender: UIButton)
{
guard self.app.installedApp == nil else { return }
sender.isIndicatingActivity = true
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
let app = context.object(with: self.app.objectID) as! App
_ = InstalledApp(app: app,
bundleIdentifier: app.identifier,
signedDate: Date(),
expirationDate: Date().addingTimeInterval(60 * 60 * 24 * 7),
context: context)
do
{
try context.save()
}
catch
{
print("Failed to download app.", error)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
sender.isIndicatingActivity = false
}
}
}
}
extension AppDetailViewController
{
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat

View File

@@ -59,6 +59,15 @@ private extension AppsViewController
cell.developerLabel.text = app.developerName
cell.appIconImageView.image = UIImage(named: app.iconName)
if app.installedApp != nil
{
cell.button.isEnabled = false
cell.button.setTitle(NSLocalizedString("Installed", comment: ""), for: .normal)
}
else
{
cell.button.isEnabled = true
cell.button.setTitle(NSLocalizedString("Download", comment: ""), for: .normal)
}
}

View File

@@ -6,47 +6,9 @@
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Second-->
<scene sceneID="wg7-f3-ORb">
<objects>
<viewController id="8rJ-Kc-sve" customClass="SecondViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="QS5-Rx-YEW">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" text="Second View" textAlignment="center" lineBreakMode="tailTruncation" minimumFontSize="10" translatesAutoresizingMaskIntoConstraints="NO" id="zEq-FU-wV5">
<rect key="frame" x="87" y="312" width="201.5" height="43"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" type="system" pointSize="36"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Loaded by SecondViewController" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="NDk-cv-Gan">
<rect key="frame" x="80" y="363" width="215" height="17"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="NDk-cv-Gan" firstAttribute="top" secondItem="zEq-FU-wV5" secondAttribute="bottom" constant="8" symbolic="YES" id="Day-4N-Vmt"/>
<constraint firstItem="NDk-cv-Gan" firstAttribute="centerX" secondItem="zEq-FU-wV5" secondAttribute="centerX" id="JgO-Fn-dHn"/>
<constraint firstAttribute="centerX" secondItem="zEq-FU-wV5" secondAttribute="centerX" id="qqM-NS-xev"/>
<constraint firstAttribute="centerY" secondItem="zEq-FU-wV5" secondAttribute="centerY" id="qzY-Ky-pLD"/>
</constraints>
<viewLayoutGuide key="safeArea" id="O1u-W8-tvY"/>
</view>
<tabBarItem key="tabBarItem" title="Second" image="second" id="cPa-gy-q4n"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="4Nw-L8-lE0" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="750" y="360"/>
</scene>
<!--Tab Bar Controller-->
<scene sceneID="yl2-sM-qoP">
<objects>
@@ -58,13 +20,58 @@
</tabBar>
<connections>
<segue destination="XF0-gk-CxQ" kind="relationship" relationship="viewControllers" id="uf8-vX-M3B"/>
<segue destination="8rJ-Kc-sve" kind="relationship" relationship="viewControllers" id="0YU-bO-QiZ"/>
<segue destination="v8c-d9-T9x" kind="relationship" relationship="viewControllers" id="fqx-5p-YCD"/>
</connections>
</tabBarController>
<placeholder placeholderIdentifier="IBFirstResponder" id="HuB-VB-40B" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="0.0" y="0.0"/>
</scene>
<!--My Apps-->
<scene sceneID="eRf-VD-MSX">
<objects>
<tableViewController id="Xf1-LZ-1vU" customClass="MyAppsViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="hkn-c3-iKp">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" textLabel="kY8-9c-qLE" detailTextLabel="XWn-JG-SYe" style="IBUITableViewCellStyleSubtitle" id="Lwi-IB-I9S">
<rect key="frame" x="0.0" y="28" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Lwi-IB-I9S" id="4Vg-jI-f8V">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="kY8-9c-qLE">
<rect key="frame" x="16" y="5" width="33.5" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Subtitle" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="XWn-JG-SYe">
<rect key="frame" x="16" y="25.5" width="44" height="14.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="12"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
</prototypes>
<connections>
<outlet property="dataSource" destination="Xf1-LZ-1vU" id="Lsq-Jy-ugM"/>
<outlet property="delegate" destination="Xf1-LZ-1vU" id="0YS-oa-D9d"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="My Apps" id="dz9-0e-LKa"/>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="nb5-5T-hHT" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1518" y="420"/>
</scene>
<!--Apps-->
<scene sceneID="JlP-x7-lBT">
<objects>
@@ -219,6 +226,9 @@
<rect key="frame" x="0.0" y="129" width="343" height="34"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="18"/>
<state key="normal" title="Download"/>
<connections>
<action selector="downloadApp:" destination="hR3-go-2DG" eventType="primaryActionTriggered" id="TZ5-aD-2Bp"/>
</connections>
</button>
</subviews>
</stackView>
@@ -376,6 +386,25 @@
</objects>
<point key="canvasLocation" x="749.60000000000002" y="-318.89055472263868"/>
</scene>
<!--My Apps-->
<scene sceneID="Edw-HT-TTT">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="v8c-d9-T9x" sceneMemberID="viewController">
<tabBarItem key="tabBarItem" title="My Apps" image="second" id="Pld-qe-pas"/>
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="PKd-0h-fii">
<rect key="frame" x="0.0" y="20" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<nil name="viewControllers"/>
<connections>
<segue destination="Xf1-LZ-1vU" kind="relationship" relationship="rootViewController" id="uaQ-4h-SJu"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="G7O-77-OQq" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="749.60000000000002" y="420.53973013493254"/>
</scene>
</scenes>
<resources>
<image name="DeltaIcon" width="512" height="512"/>

View File

@@ -41,12 +41,25 @@ class Button: UIButton
self.update()
}
}
override var isEnabled: Bool {
didSet {
self.update()
}
}
}
private extension Button
{
func update()
{
if self.isEnabled
{
self.backgroundColor = self.tintColor
}
else
{
self.backgroundColor = .lightGray
}
}
}

View File

@@ -7,13 +7,22 @@
<attribute name="localizedDescription" attributeType="String" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="screenshotNames" attributeType="Transformable" syncable="YES"/>
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="app" inverseEntity="InstalledApp" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String" syncable="YES"/>
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="isBeta" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="signedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="App" inverseName="installedApp" inverseEntity="App" syncable="YES"/>
</entity>
<elements>
<element name="App" positionX="-63" positionY="-18" width="128" height="135"/>
<element name="App" positionX="-63" positionY="-18" width="128" height="150"/>
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="120"/>
</elements>
</model>

View File

@@ -22,6 +22,9 @@ class App: NSManagedObject, Decodable
@NSManaged private(set) var iconName: String
@NSManaged private(set) var screenshotNames: [String]
/* Relationships */
@NSManaged private(set) var installedApp: InstalledApp?
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
{
super.init(entity: entity, insertInto: context)

View File

@@ -0,0 +1,50 @@
//
// InstalledApp.swift
// AltStore
//
// Created by Riley Testut on 5/20/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import Foundation
import CoreData
@objc(InstalledApp)
class InstalledApp: NSManagedObject
{
/* Properties */
@NSManaged var bundleIdentifier: String
@NSManaged var signedDate: Date
@NSManaged var expirationDate: Date
@NSManaged var isBeta: Bool
/* Relationships */
@NSManaged private(set) var app: App?
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
{
super.init(entity: entity, insertInto: context)
}
init(app: App, bundleIdentifier: String, signedDate: Date, expirationDate: Date, context: NSManagedObjectContext)
{
super.init(entity: InstalledApp.entity(), insertInto: context)
let app = context.object(with: app.objectID) as! App
self.app = app
self.bundleIdentifier = bundleIdentifier
self.signedDate = signedDate
self.expirationDate = expirationDate
}
}
extension InstalledApp
{
@nonobjc class func fetchRequest() -> NSFetchRequest<InstalledApp>
{
return NSFetchRequest<InstalledApp>(entityName: "InstalledApp")
}
}

View File

@@ -0,0 +1,58 @@
//
// MyAppsViewController.swift
// AltStore
//
// Created by Riley Testut on 5/9/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
import Roxas
class MyAppsViewController: UITableViewController
{
private lazy var dataSource = self.makeDataSource()
private lazy var dateFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .short
dateFormatter.timeStyle = .none
return dateFormatter
}()
override func viewDidLoad()
{
super.viewDidLoad()
self.tableView.dataSource = self.dataSource
}
}
private extension MyAppsViewController
{
func makeDataSource() -> RSTFetchedResultsTableViewDataSource<InstalledApp>
{
let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp>
fetchRequest.relationshipKeyPathsForPrefetching = [#keyPath(InstalledApp.app)]
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \InstalledApp.app?.name, ascending: true)]
fetchRequest.returnsObjectsAsFaults = false
let dataSource = RSTFetchedResultsTableViewDataSource(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
dataSource.cellConfigurationHandler = { (cell, installedApp, indexPath) in
guard let app = installedApp.app else { return }
cell.textLabel?.text = app.name
let detailText =
"""
Signed: \(self.dateFormatter.string(from: installedApp.signedDate))
Expires: \(self.dateFormatter.string(from: installedApp.expirationDate))
"""
cell.detailTextLabel?.numberOfLines = 2
cell.detailTextLabel?.text = detailText
}
return dataSource
}
}