[AltStore] Loads images remotely rather than including them in app bundle
@@ -122,7 +122,7 @@
|
||||
BF9ABA4F22DD41A9008935CF /* UIColor+Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4E22DD41A9008935CF /* UIColor+Hex.swift */; };
|
||||
BFB11692229322E400BB457C /* DatabaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB11691229322E400BB457C /* DatabaseManager.swift */; };
|
||||
BFB1169B2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB1169A2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift */; };
|
||||
BFB1169D22932DB100BB457C /* Apps-Dev.json in Resources */ = {isa = PBXBuildFile; fileRef = BFB1169C22932DB100BB457C /* Apps-Dev.json */; };
|
||||
BFB1169D22932DB100BB457C /* Apps-Staging.json in Resources */ = {isa = PBXBuildFile; fileRef = BFB1169C22932DB100BB457C /* Apps-Staging.json */; };
|
||||
BFB4323F22DE852000B7F8BC /* UpdateCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BFB4323E22DE852000B7F8BC /* UpdateCollectionViewCell.xib */; };
|
||||
BFBBE2DD22931B20002097FA /* AltStore.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = BFBBE2DB22931B20002097FA /* AltStore.xcdatamodeld */; };
|
||||
BFBBE2DF22931F73002097FA /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFBBE2DE22931F73002097FA /* App.swift */; };
|
||||
@@ -374,7 +374,7 @@
|
||||
BF9B63C5229DD44D002F0A62 /* AltSign.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AltSign.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BFB11691229322E400BB457C /* DatabaseManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseManager.swift; sourceTree = "<group>"; };
|
||||
BFB1169A2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "JSONDecoder+ManagedObjectContext.swift"; sourceTree = "<group>"; };
|
||||
BFB1169C22932DB100BB457C /* Apps-Dev.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "Apps-Dev.json"; sourceTree = "<group>"; };
|
||||
BFB1169C22932DB100BB457C /* Apps-Staging.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "Apps-Staging.json"; sourceTree = "<group>"; };
|
||||
BFB4323E22DE852000B7F8BC /* UpdateCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = UpdateCollectionViewCell.xib; sourceTree = "<group>"; };
|
||||
BFBAC8852295C90300587369 /* Result+Conveniences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Result+Conveniences.swift"; sourceTree = "<group>"; };
|
||||
BFBBE2DC22931B20002097FA /* AltStore.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = AltStore.xcdatamodel; sourceTree = "<group>"; };
|
||||
@@ -832,7 +832,7 @@
|
||||
BFD247962284D7C100981D42 /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BFB1169C22932DB100BB457C /* Apps-Dev.json */,
|
||||
BFB1169C22932DB100BB457C /* Apps-Staging.json */,
|
||||
BFD247762284B9A700981D42 /* Assets.xcassets */,
|
||||
BF770E6822BD57DD002A40FE /* Silence.m4a */,
|
||||
);
|
||||
@@ -1136,7 +1136,7 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
BFB1169D22932DB100BB457C /* Apps-Dev.json in Resources */,
|
||||
BFB1169D22932DB100BB457C /* Apps-Staging.json in Resources */,
|
||||
BFB4323F22DE852000B7F8BC /* UpdateCollectionViewCell.xib in Resources */,
|
||||
BFD2477A2284B9A700981D42 /* LaunchScreen.storyboard in Resources */,
|
||||
BF770E6922BD57DD002A40FE /* Silence.m4a in Resources */,
|
||||
@@ -1160,12 +1160,14 @@
|
||||
inputPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-AltStore/Pods-AltStore-frameworks.sh",
|
||||
"${BUILT_PRODUCTS_DIR}/KeychainAccess/KeychainAccess.framework",
|
||||
"${BUILT_PRODUCTS_DIR}/Nuke/Nuke.framework",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/KeychainAccess.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Nuke.framework",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
|
||||
@@ -10,6 +10,8 @@ import UIKit
|
||||
|
||||
import Roxas
|
||||
|
||||
import Nuke
|
||||
|
||||
extension AppContentViewController
|
||||
{
|
||||
private enum Row: Int, CaseIterable
|
||||
@@ -51,12 +53,10 @@ class AppContentViewController: UITableViewController
|
||||
@IBOutlet private var screenshotsCollectionView: UICollectionView!
|
||||
@IBOutlet private var permissionsCollectionView: UICollectionView!
|
||||
|
||||
var preferredScreenshotSize: CGSize? {
|
||||
guard let image = self.screenshotsDataSource.items.first else { return nil }
|
||||
|
||||
var preferredScreenshotSize: CGSize? {
|
||||
let layout = self.screenshotsCollectionView.collectionViewLayout as! UICollectionViewFlowLayout
|
||||
|
||||
let aspectRatio = image.size.height / image.size.width
|
||||
let aspectRatio: CGFloat = 16.0 / 9.0 // Hardcoded for now.
|
||||
|
||||
let width = self.screenshotsCollectionView.bounds.width - (layout.minimumInteritemSpacing * 2)
|
||||
|
||||
@@ -125,14 +125,39 @@ class AppContentViewController: UITableViewController
|
||||
|
||||
private extension AppContentViewController
|
||||
{
|
||||
func makeScreenshotsDataSource() -> RSTArrayCollectionViewDataSource<UIImage>
|
||||
func makeScreenshotsDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>
|
||||
{
|
||||
let screenshots = self.app.screenshotNames.compactMap(UIImage.init(named:))
|
||||
|
||||
let dataSource = RSTArrayCollectionViewDataSource(items: screenshots)
|
||||
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>(items: self.app.screenshotURLs as [NSURL])
|
||||
dataSource.cellConfigurationHandler = { (cell, screenshot, indexPath) in
|
||||
let cell = cell as! ScreenshotCollectionViewCell
|
||||
cell.imageView.image = screenshot
|
||||
cell.imageView.image = nil
|
||||
cell.imageView.isIndicatingActivity = true
|
||||
}
|
||||
dataSource.prefetchHandler = { (imageURL, indexPath, completionHandler) in
|
||||
return RSTAsyncBlockOperation() { (operation) in
|
||||
ImagePipeline.shared.loadImage(with: imageURL as URL, progress: nil, completion: { (response, error) in
|
||||
guard !operation.isCancelled else { return }
|
||||
|
||||
if let image = response?.image
|
||||
{
|
||||
completionHandler(image, nil)
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
||||
let cell = cell as! ScreenshotCollectionViewCell
|
||||
cell.imageView.isIndicatingActivity = false
|
||||
cell.imageView.image = image
|
||||
|
||||
if let error = error
|
||||
{
|
||||
print("Error loading image:", error)
|
||||
}
|
||||
}
|
||||
|
||||
return dataSource
|
||||
|
||||
@@ -10,6 +10,8 @@ import UIKit
|
||||
|
||||
import Roxas
|
||||
|
||||
import Nuke
|
||||
|
||||
class AppViewController: UIViewController
|
||||
{
|
||||
var app: StoreApp!
|
||||
@@ -83,17 +85,15 @@ class AppViewController: UIViewController
|
||||
self.nameLabel.text = self.app.name
|
||||
self.developerLabel.text = self.app.developerName
|
||||
self.developerLabel.textColor = self.app.tintColor
|
||||
self.appIconImageView.image = UIImage(named: self.app.iconName)
|
||||
self.appIconImageView.image = nil
|
||||
self.appIconImageView.tintColor = self.app.tintColor
|
||||
self.downloadButton.tintColor = self.app.tintColor
|
||||
self.backgroundAppIconImageView.image = UIImage(named: self.app.iconName)
|
||||
|
||||
self.backButtonContainerView.tintColor = self.app.tintColor
|
||||
|
||||
self.navigationController?.navigationBar.tintColor = self.app.tintColor
|
||||
self.navigationBarDownloadButton.tintColor = self.app.tintColor
|
||||
self.navigationBarAppNameLabel.text = self.app.name
|
||||
self.navigationBarAppIconImageView.image = UIImage(named: self.app.iconName)
|
||||
self.navigationBarAppIconImageView.tintColor = self.app.tintColor
|
||||
|
||||
self.contentSizeObservation = self.contentViewController.tableView.observe(\.contentSize) { [weak self] (tableView, change) in
|
||||
@@ -108,6 +108,19 @@ class AppViewController: UIViewController
|
||||
|
||||
self._backgroundBlurEffect = self.backgroundBlurView.effect as? UIBlurEffect
|
||||
self._backgroundBlurTintColor = self.backgroundBlurView.contentView.backgroundColor
|
||||
|
||||
// Load Images
|
||||
for imageView in [self.appIconImageView!, self.backgroundAppIconImageView!, self.navigationBarAppIconImageView!]
|
||||
{
|
||||
imageView.isIndicatingActivity = true
|
||||
|
||||
Nuke.loadImage(with: self.app.iconURL, options: .shared, into: imageView, progress: nil) { [weak imageView] (response, error) in
|
||||
if response?.image != nil
|
||||
{
|
||||
imageView?.isIndicatingActivity = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool)
|
||||
|
||||
@@ -10,11 +10,13 @@ import UIKit
|
||||
|
||||
import Roxas
|
||||
|
||||
import Nuke
|
||||
|
||||
@objc class BrowseCollectionViewCell: UICollectionViewCell
|
||||
{
|
||||
var imageNames: [String] = [] {
|
||||
var imageURLs: [URL] = [] {
|
||||
didSet {
|
||||
self.dataSource.items = self.imageNames.map { $0 as NSString }
|
||||
self.dataSource.items = self.imageURLs as [NSURL]
|
||||
}
|
||||
}
|
||||
private lazy var dataSource = self.makeDataSource()
|
||||
@@ -56,24 +58,39 @@ import Roxas
|
||||
|
||||
private extension BrowseCollectionViewCell
|
||||
{
|
||||
func makeDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<NSString, UIImage>
|
||||
func makeDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>
|
||||
{
|
||||
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<NSString, UIImage>(items: [])
|
||||
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>(items: [])
|
||||
dataSource.cellConfigurationHandler = { (cell, screenshot, indexPath) in
|
||||
let cell = cell as! ScreenshotCollectionViewCell
|
||||
cell.imageView.image = nil
|
||||
cell.imageView.isIndicatingActivity = true
|
||||
}
|
||||
dataSource.prefetchHandler = { (imageName, indexPath, completion) in
|
||||
return BlockOperation {
|
||||
let image = UIImage(named: imageName as String)
|
||||
completion(image, nil)
|
||||
dataSource.prefetchHandler = { (imageURL, indexPath, completionHandler) in
|
||||
return RSTAsyncBlockOperation() { (operation) in
|
||||
ImagePipeline.shared.loadImage(with: imageURL as URL, progress: nil, completion: { (response, error) in
|
||||
guard !operation.isCancelled else { return }
|
||||
|
||||
if let image = response?.image
|
||||
{
|
||||
completionHandler(image, nil)
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
||||
let cell = cell as! ScreenshotCollectionViewCell
|
||||
cell.imageView.isIndicatingActivity = false
|
||||
cell.imageView.image = image
|
||||
|
||||
if let error = error
|
||||
{
|
||||
print("Error loading image:", error)
|
||||
}
|
||||
}
|
||||
|
||||
return dataSource
|
||||
|
||||
@@ -10,6 +10,8 @@ import UIKit
|
||||
|
||||
import Roxas
|
||||
|
||||
import Nuke
|
||||
|
||||
class BrowseViewController: UICollectionViewController
|
||||
{
|
||||
private lazy var dataSource = self.makeDataSource()
|
||||
@@ -75,8 +77,9 @@ private extension BrowseViewController
|
||||
cell.nameLabel.text = app.name
|
||||
cell.developerLabel.text = app.developerName
|
||||
cell.subtitleLabel.text = app.subtitle
|
||||
cell.imageNames = Array(app.screenshotNames.prefix(3))
|
||||
cell.appIconImageView.image = UIImage(named: app.iconName)
|
||||
cell.imageURLs = Array(app.screenshotURLs.prefix(2))
|
||||
cell.appIconImageView.image = nil
|
||||
cell.appIconImageView.isIndicatingActivity = true
|
||||
|
||||
cell.actionButton.addTarget(self, action: #selector(BrowseViewController.performAppAction(_:)), for: .primaryActionTriggered)
|
||||
cell.actionButton.activityIndicatorView.style = .white
|
||||
@@ -103,6 +106,34 @@ private extension BrowseViewController
|
||||
cell.actionButton.isInverted = true
|
||||
}
|
||||
}
|
||||
dataSource.prefetchHandler = { (storeApp, indexPath, completionHandler) -> Foundation.Operation? in
|
||||
let iconURL = storeApp.iconURL
|
||||
|
||||
return RSTAsyncBlockOperation() { (operation) in
|
||||
ImagePipeline.shared.loadImage(with: iconURL, progress: nil, completion: { (response, error) in
|
||||
guard !operation.isCancelled else { return }
|
||||
|
||||
if let image = response?.image
|
||||
{
|
||||
completionHandler(image, nil)
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
||||
let cell = cell as! BrowseCollectionViewCell
|
||||
cell.appIconImageView.isIndicatingActivity = false
|
||||
cell.appIconImageView.image = image
|
||||
|
||||
if let error = error
|
||||
{
|
||||
print("Error loading image:", error)
|
||||
}
|
||||
}
|
||||
|
||||
return dataSource
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14490.99" systemVersion="18F203" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14492.1" systemVersion="18F203" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Account" representedClassName="Account" syncable="YES">
|
||||
<attribute name="appleID" attributeType="String" syncable="YES"/>
|
||||
<attribute name="firstName" attributeType="String" syncable="YES"/>
|
||||
@@ -58,10 +58,10 @@
|
||||
<attribute name="bundleIdentifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="developerName" attributeType="String" syncable="YES"/>
|
||||
<attribute name="downloadURL" attributeType="URI" syncable="YES"/>
|
||||
<attribute name="iconName" attributeType="String" syncable="YES"/>
|
||||
<attribute name="iconURL" attributeType="URI" syncable="YES"/>
|
||||
<attribute name="localizedDescription" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="screenshotNames" attributeType="Transformable" syncable="YES"/>
|
||||
<attribute name="screenshotURLs" attributeType="Transformable" syncable="YES"/>
|
||||
<attribute name="size" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="subtitle" optional="YES" attributeType="String" syncable="YES"/>
|
||||
@@ -92,11 +92,11 @@
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="Account" positionX="-36" positionY="90" width="128" height="135"/>
|
||||
<element name="StoreApp" positionX="-63" positionY="-18" width="128" height="300"/>
|
||||
<element name="AppPermission" positionX="-45" positionY="90" width="128" height="90"/>
|
||||
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="150"/>
|
||||
<element name="Source" positionX="-45" positionY="99" width="128" height="105"/>
|
||||
<element name="Team" positionX="-45" positionY="81" width="128" height="120"/>
|
||||
<element name="RefreshAttempt" positionX="-45" positionY="117" width="128" height="105"/>
|
||||
<element name="Source" positionX="-45" positionY="99" width="128" height="105"/>
|
||||
<element name="StoreApp" positionX="-63" positionY="-18" width="128" height="300"/>
|
||||
<element name="Team" positionX="-45" positionY="81" width="128" height="120"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -29,8 +29,8 @@ class StoreApp: NSManagedObject, Decodable, Fetchable
|
||||
@NSManaged private(set) var localizedDescription: String
|
||||
@NSManaged private(set) var size: Int32
|
||||
|
||||
@NSManaged private(set) var iconName: String
|
||||
@NSManaged private(set) var screenshotNames: [String]
|
||||
@NSManaged private(set) var iconURL: URL
|
||||
@NSManaged private(set) var screenshotURLs: [URL]
|
||||
|
||||
@NSManaged var version: String
|
||||
@NSManaged private(set) var versionDate: Date
|
||||
@@ -64,8 +64,8 @@ class StoreApp: NSManagedObject, Decodable, Fetchable
|
||||
case version
|
||||
case versionDescription
|
||||
case versionDate
|
||||
case iconName
|
||||
case screenshotNames
|
||||
case iconURL
|
||||
case screenshotURLs
|
||||
case downloadURL
|
||||
case tintColor
|
||||
case subtitle
|
||||
@@ -91,8 +91,8 @@ class StoreApp: NSManagedObject, Decodable, Fetchable
|
||||
self.versionDate = try container.decode(Date.self, forKey: .versionDate)
|
||||
self.versionDescription = try container.decodeIfPresent(String.self, forKey: .versionDescription)
|
||||
|
||||
self.iconName = try container.decode(String.self, forKey: .iconName)
|
||||
self.screenshotNames = try container.decodeIfPresent([String].self, forKey: .screenshotNames) ?? []
|
||||
self.iconURL = try container.decode(URL.self, forKey: .iconURL)
|
||||
self.screenshotURLs = try container.decodeIfPresent([URL].self, forKey: .screenshotURLs) ?? []
|
||||
|
||||
self.downloadURL = try container.decode(URL.self, forKey: .downloadURL)
|
||||
|
||||
@@ -130,8 +130,8 @@ extension StoreApp
|
||||
app.bundleIdentifier = StoreApp.altstoreAppID
|
||||
app.developerName = "Riley Testut"
|
||||
app.localizedDescription = "AltStore is an alternative App Store."
|
||||
app.iconName = ""
|
||||
app.screenshotNames = []
|
||||
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")!
|
||||
|
||||
@@ -79,7 +79,7 @@ extension Source
|
||||
let source = Source(context: context)
|
||||
source.name = "AltStore"
|
||||
source.identifier = Source.altStoreIdentifier
|
||||
source.sourceURL = URL(string: "https://www.dropbox.com/s/6qi1vt6hsi88lv6/Apps-Dev.json?dl=1")!
|
||||
source.sourceURL = URL(string: "https://www.dropbox.com/s/ernal98djzo4pe3/Apps-Staging.json?dl=1")!
|
||||
|
||||
return source
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ import Roxas
|
||||
|
||||
import AltSign
|
||||
|
||||
import Nuke
|
||||
|
||||
private let maximumCollapsedUpdatesCount = 2
|
||||
|
||||
extension MyAppsViewController
|
||||
@@ -159,7 +161,8 @@ private extension MyAppsViewController
|
||||
cell.tintColor = app.tintColor ?? .altGreen
|
||||
cell.nameLabel.text = app.name
|
||||
cell.versionDescriptionTextView.text = app.versionDescription
|
||||
cell.appIconImageView.image = UIImage(named: app.iconName)
|
||||
cell.appIconImageView.image = nil
|
||||
cell.appIconImageView.isIndicatingActivity = true
|
||||
|
||||
cell.updateButton.isIndicatingActivity = false
|
||||
cell.updateButton.addTarget(self, action: #selector(MyAppsViewController.updateApp(_:)), for: .primaryActionTriggered)
|
||||
@@ -182,6 +185,34 @@ private extension MyAppsViewController
|
||||
|
||||
cell.setNeedsLayout()
|
||||
}
|
||||
dataSource.prefetchHandler = { (installedApp, indexPath, completionHandler) in
|
||||
guard let iconURL = installedApp.storeApp?.iconURL else { return nil }
|
||||
|
||||
return RSTAsyncBlockOperation() { (operation) in
|
||||
ImagePipeline.shared.loadImage(with: iconURL, progress: nil, completion: { (response, error) in
|
||||
guard !operation.isCancelled else { return }
|
||||
|
||||
if let image = response?.image
|
||||
{
|
||||
completionHandler(image, nil)
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
||||
let cell = cell as! UpdateCollectionViewCell
|
||||
cell.appIconImageView.isIndicatingActivity = false
|
||||
cell.appIconImageView.image = image
|
||||
|
||||
if let error = error
|
||||
{
|
||||
print("Error loading image:", error)
|
||||
}
|
||||
}
|
||||
|
||||
return dataSource
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "AltStore",
|
||||
"identifier": "com.rileytestut.AltStore",
|
||||
"sourceURL": "https://www.dropbox.com/s/6qi1vt6hsi88lv6/Apps-Dev.json?dl=1",
|
||||
"sourceURL": "https://www.dropbox.com/s/ernal98djzo4pe3/Apps-Staging.json?dl=1",
|
||||
"apps": [
|
||||
{
|
||||
"name": "AltStore",
|
||||
@@ -12,7 +12,7 @@
|
||||
"versionDescription": "AltStore has been updated with bug fixes and improvements and other nice goodies for you to enjoy.",
|
||||
"downloadURL": "https://www.dropbox.com/s/w1gn9iztlqvltyp/AltStore.ipa?dl=1",
|
||||
"localizedDescription": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Sed enim ut sem viverra aliquet eget sit amet. Viverra mauris in aliquam sem fringilla ut. Egestas erat imperdiet sed euismod nisi porta. Sit amet dictum sit amet justo donec enim diam vulputate. Phasellus vestibulum lorem sed risus ultricies tristique nulla. Elit pellentesque habitant morbi tristique. Ut lectus arcu bibendum at. Ullamcorper a lacus vestibulum sed. Mi tempus imperdiet nulla malesuada pellentesque elit eget gravida. Nec feugiat nisl pretium fusce id velit ut. Amet nulla facilisi morbi tempus. Ut sem nulla pharetra diam sit amet nisl.\n\nTortor at auctor urna nunc id cursus metus. Commodo ullamcorper a lacus vestibulum sed arcu non odio. Faucibus turpis in eu mi bibendum neque egestas. Auctor augue mauris augue neque gravida in fermentum et sollicitudin. Aliquam vestibulum morbi blandit cursus risus at ultrices mi tempus. Placerat in egestas erat imperdiet sed euismod nisi. Aliquam id diam maecenas ultricies mi eget mauris. Nunc faucibus a pellentesque sit amet porttitor eget dolor. Sed faucibus turpis in eu mi. Tortor vitae purus faucibus ornare suspendisse sed nisi lacus sed. Id semper risus in hendrerit gravida rutrum quisque. At lectus urna duis convallis convallis. Egestas maecenas pharetra convallis posuere. Id velit ut tortor pretium viverra. Quam pellentesque nec nam aliquam sem et tortor consequat. Risus pretium quam vulputate dignissim. Urna nec tincidunt praesent semper feugiat nibh sed pulvinar.\n\nTempor orci eu lobortis elementum nibh tellus. Mattis rhoncus urna neque viverra justo nec. Maecenas pharetra convallis posuere morbi leo. Rhoncus mattis rhoncus urna neque viverra justo nec. Gravida dictum fusce ut placerat orci nulla pellentesque dignissim enim. At imperdiet dui accumsan sit amet. Elit sed vulputate mi sit amet mauris commodo. Pellentesque habitant morbi tristique senectus. Tortor id aliquet lectus proin nibh. Magna etiam tempor orci eu lobortis elementum. Est pellentesque elit ullamcorper dignissim. Dapibus ultrices in iaculis nunc. Commodo quis imperdiet massa tincidunt nunc pulvinar sapien et ligula. Diam vel quam elementum pulvinar. Vel turpis nunc eget lorem dolor sed viverra. Tortor pretium viverra suspendisse potenti nullam ac tortor vitae. Euismod nisi porta lorem mollis. Massa id neque aliquam vestibulum morbi blandit.\n\nMassa massa ultricies mi quis hendrerit dolor magna eget est. Augue interdum velit euismod in pellentesque massa. Sed risus ultricies tristique nulla aliquet enim. Risus viverra adipiscing at in tellus. Donec adipiscing tristique risus nec feugiat. Eget sit amet tellus cras adipiscing enim eu turpis. Auctor neque vitae tempus quam pellentesque nec. Sit amet tellus cras adipiscing enim eu turpis egestas. Dui faucibus in ornare quam viverra. Fermentum iaculis eu non diam phasellus vestibulum lorem. Odio ut enim blandit volutpat maecenas. Dolor sit amet consectetur adipiscing elit pellentesque habitant morbi tristique. Pellentesque diam volutpat commodo sed egestas egestas. Aliquam purus sit amet luctus venenatis lectus magna fringilla. Viverra mauris in aliquam sem fringilla ut morbi tincidunt. Elit duis tristique sollicitudin nibh sit. Fermentum dui faucibus in ornare quam viverra orci sagittis. Aliquet eget sit amet tellus cras adipiscing.",
|
||||
"iconName": "AppIcon",
|
||||
"iconURL": "https://user-images.githubusercontent.com/705880/63392210-540c5980-c37b-11e9-968c-8742fc68ab2e.png",
|
||||
"size": 10010524,
|
||||
"permissions": [
|
||||
{
|
||||
@@ -35,7 +35,7 @@
|
||||
"versionDescription": "Finally, after almost 5 years of waiting, Delta is out of beta and ready for everyone to enjoy!\n\nCurrently supports NES, SNES, N64, GB(C), and GBA games, with more to come in the future.",
|
||||
"downloadURL": "https://www.dropbox.com/s/31i4hcqnorucrxi/Delta.ipa?dl=1",
|
||||
"localizedDescription": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Sed enim ut sem viverra aliquet eget sit amet. Viverra mauris in aliquam sem fringilla ut. Egestas erat imperdiet sed euismod nisi porta. Sit amet dictum sit amet justo donec enim diam vulputate. Phasellus vestibulum lorem sed risus ultricies tristique nulla. Elit pellentesque habitant morbi tristique. Ut lectus arcu bibendum at. Ullamcorper a lacus vestibulum sed. Mi tempus imperdiet nulla malesuada pellentesque elit eget gravida. Nec feugiat nisl pretium fusce id velit ut. Amet nulla facilisi morbi tempus. Ut sem nulla pharetra diam sit amet nisl.\n\nTortor at auctor urna nunc id cursus metus. Commodo ullamcorper a lacus vestibulum sed arcu non odio. Faucibus turpis in eu mi bibendum neque egestas. Auctor augue mauris augue neque gravida in fermentum et sollicitudin. Aliquam vestibulum morbi blandit cursus risus at ultrices mi tempus. Placerat in egestas erat imperdiet sed euismod nisi. Aliquam id diam maecenas ultricies mi eget mauris. Nunc faucibus a pellentesque sit amet porttitor eget dolor. Sed faucibus turpis in eu mi. Tortor vitae purus faucibus ornare suspendisse sed nisi lacus sed. Id semper risus in hendrerit gravida rutrum quisque. At lectus urna duis convallis convallis. Egestas maecenas pharetra convallis posuere. Id velit ut tortor pretium viverra. Quam pellentesque nec nam aliquam sem et tortor consequat. Risus pretium quam vulputate dignissim. Urna nec tincidunt praesent semper feugiat nibh sed pulvinar.\n\nTempor orci eu lobortis elementum nibh tellus. Mattis rhoncus urna neque viverra justo nec. Maecenas pharetra convallis posuere morbi leo. Rhoncus mattis rhoncus urna neque viverra justo nec. Gravida dictum fusce ut placerat orci nulla pellentesque dignissim enim. At imperdiet dui accumsan sit amet. Elit sed vulputate mi sit amet mauris commodo. Pellentesque habitant morbi tristique senectus. Tortor id aliquet lectus proin nibh. Magna etiam tempor orci eu lobortis elementum. Est pellentesque elit ullamcorper dignissim. Dapibus ultrices in iaculis nunc. Commodo quis imperdiet massa tincidunt nunc pulvinar sapien et ligula. Diam vel quam elementum pulvinar. Vel turpis nunc eget lorem dolor sed viverra. Tortor pretium viverra suspendisse potenti nullam ac tortor vitae. Euismod nisi porta lorem mollis. Massa id neque aliquam vestibulum morbi blandit.\n\nMassa massa ultricies mi quis hendrerit dolor magna eget est. Augue interdum velit euismod in pellentesque massa. Sed risus ultricies tristique nulla aliquet enim. Risus viverra adipiscing at in tellus. Donec adipiscing tristique risus nec feugiat. Eget sit amet tellus cras adipiscing enim eu turpis. Auctor neque vitae tempus quam pellentesque nec. Sit amet tellus cras adipiscing enim eu turpis egestas. Dui faucibus in ornare quam viverra. Fermentum iaculis eu non diam phasellus vestibulum lorem. Odio ut enim blandit volutpat maecenas. Dolor sit amet consectetur adipiscing elit pellentesque habitant morbi tristique. Pellentesque diam volutpat commodo sed egestas egestas. Aliquam purus sit amet luctus venenatis lectus magna fringilla. Viverra mauris in aliquam sem fringilla ut morbi tincidunt. Elit duis tristique sollicitudin nibh sit. Fermentum dui faucibus in ornare quam viverra orci sagittis. Aliquet eget sit amet tellus cras adipiscing.",
|
||||
"iconName": "DeltaIcon",
|
||||
"iconURL": "https://user-images.githubusercontent.com/705880/63391976-4d311700-c37a-11e9-91a8-4fb0c454413d.png",
|
||||
"tintColor": "8A28F7",
|
||||
"size": 26908804,
|
||||
"permissions": [
|
||||
@@ -44,10 +44,10 @@
|
||||
"usageDescription": "Allows Delta to use images from your Photo Library as game artwork."
|
||||
}
|
||||
],
|
||||
"screenshotNames": [
|
||||
"Delta1",
|
||||
"Delta2",
|
||||
"Delta3"
|
||||
"screenshotURLs": [
|
||||
"https://user-images.githubusercontent.com/705880/63391859-c714d080-c379-11e9-8d2f-967b6820485d.png",
|
||||
"https://user-images.githubusercontent.com/705880/63391863-c8de9400-c379-11e9-9b3f-39da2dff4a8a.png",
|
||||
"https://user-images.githubusercontent.com/705880/63391865-c9772a80-c379-11e9-848b-d7d62cda06fd.png"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -60,7 +60,7 @@
|
||||
"versionDescription": "Bug fixes and improvements.",
|
||||
"downloadURL": "https://www.dropbox.com/s/x11b4m8jvmz6tpl/Clip.ipa?dl=1",
|
||||
"localizedDescription": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Sed enim ut sem viverra aliquet eget sit amet. Viverra mauris in aliquam sem fringilla ut. Egestas erat imperdiet sed euismod nisi porta. Sit amet dictum sit amet justo donec enim diam vulputate. Phasellus vestibulum lorem sed risus ultricies tristique nulla. Elit pellentesque habitant morbi tristique. Ut lectus arcu bibendum at. Ullamcorper a lacus vestibulum sed. Mi tempus imperdiet nulla malesuada pellentesque elit eget gravida. Nec feugiat nisl pretium fusce id velit ut. Amet nulla facilisi morbi tempus. Ut sem nulla pharetra diam sit amet nisl.\n\nTortor at auctor urna nunc id cursus metus. Commodo ullamcorper a lacus vestibulum sed arcu non odio. Faucibus turpis in eu mi bibendum neque egestas. Auctor augue mauris augue neque gravida in fermentum et sollicitudin. Aliquam vestibulum morbi blandit cursus risus at ultrices mi tempus. Placerat in egestas erat imperdiet sed euismod nisi. Aliquam id diam maecenas ultricies mi eget mauris. Nunc faucibus a pellentesque sit amet porttitor eget dolor. Sed faucibus turpis in eu mi. Tortor vitae purus faucibus ornare suspendisse sed nisi lacus sed. Id semper risus in hendrerit gravida rutrum quisque. At lectus urna duis convallis convallis. Egestas maecenas pharetra convallis posuere. Id velit ut tortor pretium viverra. Quam pellentesque nec nam aliquam sem et tortor consequat. Risus pretium quam vulputate dignissim. Urna nec tincidunt praesent semper feugiat nibh sed pulvinar.\n\nTempor orci eu lobortis elementum nibh tellus. Mattis rhoncus urna neque viverra justo nec. Maecenas pharetra convallis posuere morbi leo. Rhoncus mattis rhoncus urna neque viverra justo nec. Gravida dictum fusce ut placerat orci nulla pellentesque dignissim enim. At imperdiet dui accumsan sit amet. Elit sed vulputate mi sit amet mauris commodo. Pellentesque habitant morbi tristique senectus. Tortor id aliquet lectus proin nibh. Magna etiam tempor orci eu lobortis elementum. Est pellentesque elit ullamcorper dignissim. Dapibus ultrices in iaculis nunc. Commodo quis imperdiet massa tincidunt nunc pulvinar sapien et ligula. Diam vel quam elementum pulvinar. Vel turpis nunc eget lorem dolor sed viverra. Tortor pretium viverra suspendisse potenti nullam ac tortor vitae. Euismod nisi porta lorem mollis. Massa id neque aliquam vestibulum morbi blandit.\n\nMassa massa ultricies mi quis hendrerit dolor magna eget est. Augue interdum velit euismod in pellentesque massa. Sed risus ultricies tristique nulla aliquet enim. Risus viverra adipiscing at in tellus. Donec adipiscing tristique risus nec feugiat. Eget sit amet tellus cras adipiscing enim eu turpis. Auctor neque vitae tempus quam pellentesque nec. Sit amet tellus cras adipiscing enim eu turpis egestas. Dui faucibus in ornare quam viverra. Fermentum iaculis eu non diam phasellus vestibulum lorem. Odio ut enim blandit volutpat maecenas. Dolor sit amet consectetur adipiscing elit pellentesque habitant morbi tristique. Pellentesque diam volutpat commodo sed egestas egestas. Aliquam purus sit amet luctus venenatis lectus magna fringilla. Viverra mauris in aliquam sem fringilla ut morbi tincidunt. Elit duis tristique sollicitudin nibh sit. Fermentum dui faucibus in ornare quam viverra orci sagittis. Aliquet eget sit amet tellus cras adipiscing.",
|
||||
"iconName": "ClipboardIcon",
|
||||
"iconURL": "https://user-images.githubusercontent.com/705880/63391981-5326f800-c37a-11e9-99d8-760fd06bb601.png",
|
||||
"tintColor": "EC008C",
|
||||
"size": 438855,
|
||||
"permissions": [
|
||||
@@ -69,9 +69,9 @@
|
||||
"usageDescription": "Allows Clip to continuously monitor your clipboard in the background."
|
||||
}
|
||||
],
|
||||
"screenshotNames": [
|
||||
"Clip1",
|
||||
"Clip2"
|
||||
"screenshotURLs": [
|
||||
"https://user-images.githubusercontent.com/705880/63391948-32f73900-c37a-11e9-976c-275bd8d557f0.png",
|
||||
"https://user-images.githubusercontent.com/705880/63391950-34286600-c37a-11e9-965f-832efe3da507.png"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
Before Width: | Height: | Size: 110 KiB |
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "ClipIcon.png"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "DeltaIcon.png"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 65 KiB |
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "IMG_4222.png"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.9 MiB |
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "IMG_4221.png"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.2 MiB |
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "IMG_4226.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.4 MiB |
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "IMG_4225.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.3 MiB |
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "IMG_4227.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.1 MiB |
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "first.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "second.pdf"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
1
Podfile
@@ -7,5 +7,6 @@ target 'AltStore' do
|
||||
|
||||
# Pods for AltStore
|
||||
pod 'KeychainAccess', '~> 3.2.0'
|
||||
pod 'Nuke', '~> 7.0'
|
||||
|
||||
end
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
PODS:
|
||||
- KeychainAccess (3.2.0)
|
||||
- Nuke (7.6.2)
|
||||
|
||||
DEPENDENCIES:
|
||||
- KeychainAccess (~> 3.2.0)
|
||||
- Nuke (~> 7.0)
|
||||
|
||||
SPEC REPOS:
|
||||
https://github.com/cocoapods/specs.git:
|
||||
- KeychainAccess
|
||||
- Nuke
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
KeychainAccess: 3b1bf8a77eb4c6ea1ce9404c292e48f948954c6b
|
||||
Nuke: 392eff7cb037adbec6699febe9237849ac5dcc47
|
||||
|
||||
PODFILE CHECKSUM: 6a1695fc6ba95ee2a4044665937e8ac08a4c4572
|
||||
PODFILE CHECKSUM: 8c674682f3a0a24c842f7c680830b0bb1ebc324d
|
||||
|
||||
COCOAPODS: 1.6.1
|
||||
|
||||
6
Pods/Manifest.lock
generated
@@ -1,16 +1,20 @@
|
||||
PODS:
|
||||
- KeychainAccess (3.2.0)
|
||||
- Nuke (7.6.2)
|
||||
|
||||
DEPENDENCIES:
|
||||
- KeychainAccess (~> 3.2.0)
|
||||
- Nuke (~> 7.0)
|
||||
|
||||
SPEC REPOS:
|
||||
https://github.com/cocoapods/specs.git:
|
||||
- KeychainAccess
|
||||
- Nuke
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
KeychainAccess: 3b1bf8a77eb4c6ea1ce9404c292e48f948954c6b
|
||||
Nuke: 392eff7cb037adbec6699febe9237849ac5dcc47
|
||||
|
||||
PODFILE CHECKSUM: 6a1695fc6ba95ee2a4044665937e8ac08a4c4572
|
||||
PODFILE CHECKSUM: 8c674682f3a0a24c842f7c680830b0bb1ebc324d
|
||||
|
||||
COCOAPODS: 1.6.1
|
||||
|
||||
21
Pods/Nuke/LICENSE
generated
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2018 Alexander Grebenyuk
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
513
Pods/Nuke/README.md
generated
Normal file
@@ -0,0 +1,513 @@
|
||||
<br/>
|
||||
|
||||
<p align="center"><img src="https://cloud.githubusercontent.com/assets/1567433/13918338/f8670eea-ef7f-11e5-814d-f15bdfd6b2c0.png" height="180"/>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/cocoapods/v/Nuke.svg?label=version">
|
||||
<img src="https://img.shields.io/badge/supports-CocoaPods%2C%20Carthage%2C%20SwiftPM-green.svg">
|
||||
<img src="https://img.shields.io/badge/platforms-iOS%2C%20macOS%2C%20watchOS%2C%20tvOS-lightgrey.svg">
|
||||
<a href="https://travis-ci.org/kean/Nuke"><img src="https://img.shields.io/travis/kean/Nuke/master.svg"></a>
|
||||
</p>
|
||||
|
||||
A powerful **image loading** and **caching** system. It makes simple tasks like loading images into views extremely simple, while also supporting advanced features for more demanding apps.
|
||||
|
||||
- Fast LRU memory cache, native HTTP disk cache, and custom aggressive LRU disk cache
|
||||
- Progressive image loading (progressive JPEG and WebP)
|
||||
- Resumable downloads, request prioritization, deduplication, rate limiting and more
|
||||
- [Alamofire](https://github.com/kean/Nuke-Alamofire-Plugin), [WebP](https://github.com/ryokosuge/Nuke-WebP-Plugin), [Gifu](https://github.com/kean/Nuke-Gifu-Plugin), [FLAnimatedImage](https://github.com/kean/Nuke-FLAnimatedImage-Plugin) extensions
|
||||
- [RxNuke](https://github.com/kean/RxNuke) - [RxSwift](https://github.com/ReactiveX/RxSwift) extensions
|
||||
- Automates [prefetching](https://kean.github.io/post/image-preheating) with [Preheat](https://github.com/kean/Preheat) (*deprecated in iOS 10*)
|
||||
|
||||
# <a name="h_getting_started"></a>Getting Started
|
||||
|
||||
> Upgrading from the previous version? Use a [**Migration Guide**](https://github.com/kean/Nuke/blob/master/Documentation/Migrations).
|
||||
|
||||
- [**Quick Start Guide**](#h_usage)
|
||||
- [Load Image into Image View](#load-image-into-image-view)
|
||||
- [Placeholders, Transitions and More](#placeholders-transitions-and-more)
|
||||
- [Image Requests](#image-requests), [Process an Image](#process-an-image)
|
||||
- [**Advanced Usage Guide**](#advanced-usage)
|
||||
- [Image Pipeline](#image-pipeline), [Configuring Image Pipeline](#configuring-image-pipeline)
|
||||
- [Memory Cache](#memory-cache), [HTTP Disk Cache](#http-disk-cache), [Aggressive Disk Cache](#aggressive-disk-cache)
|
||||
- [Preheat Images](#preheat-images)
|
||||
- [Progressive Decoding](#progressive-decoding), [Animated Images](#animated-images), [WebP](#webp)
|
||||
- [RxNuke](#rxnuke)
|
||||
- Detailed [**Image Pipeline**](#h_design) description
|
||||
- An entire section dedicated to [**Performance**](#h_performance)
|
||||
- List of [**Extensions**](#h_plugins)
|
||||
- [**Contributing**](#h_contribute) and roadmap
|
||||
- [**Requirements**](#h_requirements)
|
||||
|
||||
More information is available in [**Documentation**](https://github.com/kean/Nuke/blob/master/Documentation/) directory and a full [**API Reference**](https://kean.github.io/Nuke/reference/7.3/index.html). When you are ready to install Nuke you can follow an [**Installation Guide**](https://github.com/kean/Nuke/blob/master/Documentation/Guides/Installation%20Guide.md) - all major package managers are supported.
|
||||
|
||||
# <a name="h_usage"></a>Quick Start
|
||||
|
||||
#### Load Image into Image View
|
||||
|
||||
You can load an image into an image view with a single line of code.
|
||||
|
||||
```swift
|
||||
Nuke.loadImage(with: url, into: imageView)
|
||||
```
|
||||
|
||||
Nuke will automatically load image data, decompress it in the background, store image in memory cache and display it.
|
||||
|
||||
> To learn more about the `ImagePipeline` [see the dedicated section](#h_design).
|
||||
|
||||
When you request a new image for the view, the previous outstanding request gets canceled and the image is set to `nil`. The request also gets canceled automatically when the view is deallocated.
|
||||
|
||||
```swift
|
||||
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
...
|
||||
Nuke.loadImage(with: url, into: cell.imageView)
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
#### Placeholders, Transitions and More
|
||||
|
||||
Use an `options` parameter (`ImageLoadingOptions`) to customize the way images are loaded and displayed. You can provide a placeholder, select one of the built-in transitions or provide a custom one. When using transitions, be aware that UIKit may keep a reference to the image, preventing it from being removed for long animations or loading many transitions at once.
|
||||
|
||||
```swift
|
||||
Nuke.loadImage(
|
||||
with: url,
|
||||
options: ImageLoadingOptions(
|
||||
placeholder: UIImage(named: "placeholder"),
|
||||
transition: .fadeIn(duration: 0.33)
|
||||
),
|
||||
into: imageView
|
||||
)
|
||||
```
|
||||
|
||||
There is a very common scenario when the placeholder (or the failure image) needs to be displayed with a _content mode_ different from the one used for the loaded image.
|
||||
|
||||
```swift
|
||||
let options = ImageLoadingOptions(
|
||||
placeholder: UIImage(named: "placeholder"),
|
||||
failureImage: UIImage(named: "failure_image"),
|
||||
contentModes: .init(
|
||||
success: .scaleAspectFill,
|
||||
failure: .center,
|
||||
placeholder: .center
|
||||
)
|
||||
)
|
||||
|
||||
Nuke.loadImage(with: url, options: options, into: imageView)
|
||||
```
|
||||
|
||||
To make all image views in the app share the same behavior modify `ImageLoadingOptions.shared`.
|
||||
|
||||
> If `ImageLoadingOptions` are missing a feature that you need, please use `ImagePipeline` directly. If you think that everyone could benefit from this feature, PRs are welcome.
|
||||
|
||||
#### Image Requests
|
||||
|
||||
Each request is represented by an `ImageRequest` struct. A request can be created either with `URL` or `URLRequest`.
|
||||
|
||||
```swift
|
||||
var request = ImageRequest(url: url)
|
||||
// var request = ImageRequest(urlRequest: URLRequest(url: url))
|
||||
|
||||
// Change memory cache policy:
|
||||
request.memoryCacheOptions.isWriteAllowed = false
|
||||
|
||||
// Update the request priority:
|
||||
request.priority = .high
|
||||
|
||||
Nuke.loadImage(with: request, into: imageView)
|
||||
```
|
||||
|
||||
#### Process an Image
|
||||
|
||||
Resize an image using special `ImageRequest` initializer.
|
||||
|
||||
```swift
|
||||
// Target size is in pixels.
|
||||
ImageRequest(url: url, targetSize: CGSize(width: 640, height: 320), contentMode: .aspectFill)
|
||||
```
|
||||
|
||||
Perform custom tranformation using `processed(key:closure:)` method. Her's how to create a circular avatar using [Toucan](https://github.com/gavinbunney/Toucan).
|
||||
|
||||
```swift
|
||||
ImageRequest(url: url).process(key: "circularAvatar") {
|
||||
Toucan(image: $0).maskWithEllipse().image
|
||||
}
|
||||
```
|
||||
|
||||
All those APIs are built on top of `ImageProcessing` protocol which you can also use to implement custom processors. Keep in mind that `ImageProcessing` also requires `Equatable` conformance which helps Nuke identify images in memory cache.
|
||||
|
||||
> See [Core Image Integration Guide](https://github.com/kean/Nuke/blob/master/Documentation/Guides/Core%20Image%20Integration%20Guide.md) for info about using Core Image with Nuke
|
||||
|
||||
# Advanced Usage
|
||||
|
||||
#### Image Pipeline
|
||||
|
||||
Use `ImagePipeline` directly to load images without a view.
|
||||
|
||||
```swift
|
||||
let task = ImagePipeline.shared.loadImage(
|
||||
with: url,
|
||||
progress: { _, completed, total in
|
||||
print("progress updated")
|
||||
},
|
||||
completion: { response, error in
|
||||
print("task completed")
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
Tasks can be used to monitor download progress, cancel the requests, and dynamically update download priority.
|
||||
|
||||
```swift
|
||||
task.cancel()
|
||||
task.setPriority(.high)
|
||||
```
|
||||
|
||||
> To learn more about the `ImagePipeline` [see the dedicated section](#h_design).
|
||||
|
||||
#### Configuring Image Pipeline
|
||||
|
||||
Apart from using a shared `ImagePipeline` instance, you can create your own.
|
||||
|
||||
```swift
|
||||
let pipeline = ImagePipeline {
|
||||
$0.dataLoader = ...
|
||||
$0.dataLoadingQueue = ...
|
||||
$0.imageCache = ...
|
||||
...
|
||||
}
|
||||
|
||||
// When you're done you can make the pipeline a shared one:
|
||||
ImagePipeline.shared = pipeline
|
||||
```
|
||||
|
||||
#### Memory Cache
|
||||
|
||||
Default Nuke's `ImagePipeline` has two cache layers.
|
||||
|
||||
First, there is a memory cache for storing processed images ready for display. You can get a direct access to this cache:
|
||||
|
||||
```swift
|
||||
// Configure cache
|
||||
ImageCache.shared.costLimit = 1024 * 1024 * 100 // 100 MB
|
||||
ImageCache.shared.countLimit = 100
|
||||
ImageCache.shared.ttl = 120 // Invalidate image after 120 sec
|
||||
|
||||
// Read and write images
|
||||
let request = ImageRequest(url: url)
|
||||
ImageCache.shared[request] = image
|
||||
let image = ImageCache.shared[request]
|
||||
|
||||
// Clear cache
|
||||
ImageCache.shared.removeAll()
|
||||
```
|
||||
|
||||
#### HTTP Disk Cache
|
||||
|
||||
To store unprocessed image data Nuke uses a `URLCache` instance:
|
||||
|
||||
```swift
|
||||
// Configure cache
|
||||
DataLoader.sharedUrlCache.diskCapacity = 100
|
||||
DataLoader.sharedUrlCache.memoryCapacity = 0
|
||||
|
||||
// Read and write responses
|
||||
let request = ImageRequest(url: url)
|
||||
let _ = DataLoader.sharedUrlCache.cachedResponse(for: request.urlRequest)
|
||||
DataLoader.sharedUrlCache.removeCachedResponse(for: request.urlRequest)
|
||||
|
||||
// Clear cache
|
||||
DataLoader.sharedUrlCache.removeAllCachedResponses()
|
||||
```
|
||||
|
||||
#### Aggressive Disk Cache
|
||||
|
||||
A custom LRU disk cache can be used for fast and reliable *aggressive* data caching (ignores [HTTP cache control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control)). You can enable it using pipeline's configuration.
|
||||
|
||||
```swift
|
||||
$0.dataCache = try! DataCache(name: "com.myapp.datacache")
|
||||
```
|
||||
|
||||
If you enable aggressive disk cache, make sure that you also disable native URL cache (see `DataLoader`), or you might end up storing the same image data twice.
|
||||
|
||||
> `DataCache` type implements public `DataCaching` protocol which can be used for implementing custom data caches.
|
||||
|
||||
#### Prefetching Images
|
||||
|
||||
[Prefethcing](https://kean.github.io/post/image-preheating) images in advance reduces the wait time for users. Nuke provides an `ImagePreheater` to do just that:
|
||||
|
||||
```swift
|
||||
let preheater = ImagePreheater()
|
||||
preheater.startPreheating(with: urls)
|
||||
|
||||
// Cancels all of the preheating tasks created for the given requests.
|
||||
preheater.stopPreheating(with: urls)
|
||||
```
|
||||
|
||||
There are trade-offs, prefetching takes up users's data and puts an extra pressure on CPU and memory. To reduce the CPU and memory usage you have an option to choose only the disk cache as a prefetching destination:
|
||||
|
||||
```swift
|
||||
// The preheater with `.diskCache` destination will skip image data decoding
|
||||
// entirely to reduce CPU and memory usage. It will still load the image data
|
||||
// and store it in disk caches to be used later.
|
||||
let preheater = ImagePreheater(destination: .diskCache)
|
||||
```
|
||||
|
||||
To make sure that the prefetching requests don't interfere with normal requests it's best to reduce their priority.
|
||||
|
||||
You can use Nuke in combination with [Preheat](https://github.com/kean/Preheat) library which automates preheating of content in `UICollectionView` and `UITableView`. On iOS 10.0 you might want to use new [prefetching APIs](https://developer.apple.com/reference/uikit/uitableviewdatasourceprefetching) provided by iOS instead.
|
||||
|
||||
> Check out [Performance Guide](https://github.com/kean/Nuke/blob/master/Documentation/Guides/Performance%20Guide.md) to see what else you can do to improve performance
|
||||
|
||||
#### Progressive Decoding
|
||||
|
||||
To use progressive image loading you need a pipeline with progressive decoding enabled.
|
||||
|
||||
```swift
|
||||
let pipeline = ImagePipeline {
|
||||
$0.isProgressiveDecodingEnabled = true
|
||||
}
|
||||
```
|
||||
|
||||
And that's it, you can start observing images as they are produced by the pipeline. The progress handler also works as a progressive image handler.
|
||||
|
||||
```swift
|
||||
let imageView = UIImageView()
|
||||
let task = ImagePipeline.shared.loadImage(
|
||||
with: url,
|
||||
progress: { response, _, _ in
|
||||
imageView.image = response?.image
|
||||
},
|
||||
completion: { response, _ in
|
||||
imageView.image = response?.image
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
> See "Progressive Decoding" demo to see progressive JPEG in practice.
|
||||
|
||||
#### Animated Images
|
||||
|
||||
Nuke extends `UIImage` with `animatedImageData` property. If you enable it by setting `ImagePipeline.Configuration.isAnimatedImageDataEnabled` to `true` the pipeline will start attaching original image data to the animated images (built-in decoder only supports GIFs for now).
|
||||
|
||||
> `ImageCache` takes `animatedImageData` into account when computing the cost of cached items. `ImagePipeline` doesn't apply processors to the images with animated data.
|
||||
|
||||
There is no built-in way to render those images, but there are two integrations available: [FLAnimatedImage](https://github.com/kean/Nuke-FLAnimatedImage-Plugin) and [Gifu](https://github.com/kean/Nuke-Gifu-Plugin) which are both fast and efficient.
|
||||
|
||||
> `GIF` is not the most efficient format for transferring and displaying animated images. The current best practice is to [use short videos instead of GIFs](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/replace-animated-gifs-with-video/) (e.g. `MP4`, `WebM`). There is a PoC available in the demo project which uses Nuke to load, cache and display an `MP4` video.
|
||||
|
||||
#### WebP
|
||||
|
||||
WebP support is provided by [Nuke WebP Plugin](https://github.com/ryokosuge/Nuke-WebP-Plugin) built by [Ryo Kosuge](https://github.com/ryokosuge). Please follow the intructions from the repo to install it.
|
||||
|
||||
#### RxNuke
|
||||
|
||||
[RxNuke](https://github.com/kean/RxNuke) adds [RxSwift](https://github.com/ReactiveX/RxSwift) extensions for Nuke and enables many common use cases:
|
||||
|
||||
- [Going from low to high resolution](https://github.com/kean/RxNuke#going-from-low-to-high-resolution)
|
||||
- [Loading the first available image](https://github.com/kean/RxNuke#loading-the-first-available-image)
|
||||
- [Showing stale image while validating it](https://github.com/kean/RxNuke#showing-stale-image-while-validating-it)
|
||||
- [Load multiple images, display all at once](https://github.com/kean/RxNuke#load-multiple-images-display-all-at-once)
|
||||
- [Auto retry on failures](https://github.com/kean/RxNuke#auto-retry)
|
||||
- And [more...](https://github.com/kean/RxNuke#use-cases)
|
||||
|
||||
Here's an example of how easy it is to load go flow log to high resolution:
|
||||
|
||||
```swift
|
||||
let pipeline = ImagePipeline.shared
|
||||
Observable.concat(pipeline.loadImage(with: lowResUrl).orEmpty,
|
||||
pipeline.loadImage(with: highResUtl).orEmpty)
|
||||
.subscribe(onNext: { imageView.image = $0 })
|
||||
.disposed(by: disposeBag)
|
||||
```
|
||||
|
||||
<a name="h_design"></a>
|
||||
# Image Pipeline
|
||||
|
||||
Nuke's image pipeline consists of roughly five stages which can be customized using the following protocols:
|
||||
|
||||
|Protocol|Description|
|
||||
|--------|-----------|
|
||||
|`DataLoading`|Download (or return cached) image data|
|
||||
|`DataCaching`|Custom data cache|
|
||||
|`ImageDecoding`|Convert data into image objects|
|
||||
|`ImageProcessing`|Apply image transformations|
|
||||
|`ImageCaching`|Store image into memory cache|
|
||||
|
||||
### Default Image Pipeline
|
||||
|
||||
The default image pipeline configuration looks like this:
|
||||
|
||||
```swift
|
||||
ImagePipeline {
|
||||
// Shared image cache with a `sizeLimit` equal to ~20% of available RAM.
|
||||
$0.imageCache = ImageCache.shared
|
||||
|
||||
// Data loader with a `URLSessionConfiguration.default` but with a
|
||||
// custom shared URLCache instance:
|
||||
//
|
||||
// public static let sharedUrlCache = URLCache(
|
||||
// memoryCapacity: 0,
|
||||
// diskCapacity: 150 * 1024 * 1024, // 150 MB
|
||||
// diskPath: "com.github.kean.Nuke.Cache"
|
||||
// )
|
||||
$0.dataLoader = DataLoader()
|
||||
|
||||
// Custom disk cache is disabled by default, the native URL cache used
|
||||
// by a `DataLoader` is used instead.
|
||||
$0.dataCache = nil
|
||||
|
||||
// Each stage is executed on a dedicated queue with has its own limits.
|
||||
$0.dataLoadingQueue.maxConcurrentOperationCount = 6
|
||||
$0.imageDecodingQueue.maxConcurrentOperationCount = 1
|
||||
$0.imageProcessingQueue.maxConcurrentOperationCount = 2
|
||||
|
||||
// Combine the requests for the same original image into one.
|
||||
$0.isDeduplicationEnabled = true
|
||||
|
||||
// Progressive decoding is a resource intensive feature so it is
|
||||
// disabled by default.
|
||||
$0.isProgressiveDecodingEnabled = false
|
||||
}
|
||||
```
|
||||
|
||||
### Image Pipeline Overview
|
||||
|
||||
Here's what happens when you call `Nuke.loadImage(with: url, into: imageView` method.
|
||||
|
||||
First, Nuke synchronously checks if the image is available in the memory cache (`pipeline.configuration.imageCache`). If it's not, Nuke calls `pipeline.loadImage(with: request)` method. The pipeline also checks if the image is available in its memory cache, and if not, starts loading it.
|
||||
|
||||
Before starting to load image data, the pipeline also checks whether there are any existing outstanding requests for the same image. If it finds one, no new requests are created.
|
||||
|
||||
By default, the data is loaded using [`URLSession`](https://developer.apple.com/reference/foundation/nsurlsession) with a custom [`URLCache`](https://developer.apple.com/reference/foundation/urlcache) instance (see configuration above). The `URLCache` supports on-disk caching but it requires HTTP cache to be enabled.
|
||||
|
||||
> See [Image Caching Guide](https://kean.github.io/post/image-caching) to learn more.
|
||||
|
||||
When the data is loaded the pipeline decodes the data (creates `UIImage` object from `Data`). Then it applies a default image processor - `ImageDecompressor` - to force data decompression in a background. The processed image is then stored in the memory cache and returned in the completion closure.
|
||||
|
||||
> When you create `UIImage` object form data, the data doesn't get decoded immediately. It's decoded the first time it's used - for example, when you display the image in an image view. Decoding is a resource-intensive operation, if you do it on the main thread you might see dropped frames, especially for image formats like JPEG.
|
||||
>
|
||||
> To prevent decoding happening on the main thread, Nuke perform it in a background for you. But for even better performance it's recommended to downsample the images. To do so create a request with a target view size:
|
||||
>
|
||||
> ImageRequest(url: url, targetSize: CGSize(width: 640, height: 320), contentMode: .aspectFill)
|
||||
>
|
||||
> **Warning:** target size is in pixels!
|
||||
>
|
||||
> See [Image and Graphics Best Practices](https://developer.apple.com/videos/play/wwdc2018/219) to learn more about image decoding and downsampling.
|
||||
|
||||
### Data Loading and Caching
|
||||
|
||||
A built-in `DataLoader` class implements `DataLoading` protocol and uses [`URLSession`](https://developer.apple.com/reference/foundation/nsurlsession) to load image data. The data is cached on disk using a [`URLCache`](https://developer.apple.com/reference/foundation/urlcache) instance, which by default is initialized with a memory capacity of 0 MB (Nuke stores images in memory, not image data) and a disk capacity of 150 MB.
|
||||
|
||||
The `URLSession` class natively supports the `data`, `file`, `ftp`, `http`, and `https` URL schemes. Image pipeline can be used with any of those schemes as well.
|
||||
|
||||
> See [Image Caching Guide](https://kean.github.io/post/image-caching) to learn more about image caching
|
||||
|
||||
> See [Third Party Libraries](https://github.com/kean/Nuke/blob/master/Documentation/Guides/Third%20Party%20Libraries.md#using-other-caching-libraries) guide to learn how to use a custom data loader or cache
|
||||
|
||||
Most developers either implement their own networking layer or use a third-party framework. Nuke supports both of those workflows. You can integrate your custom networking layer by implementing `DataLoading` protocol.
|
||||
|
||||
> See [Alamofire Plugin](https://github.com/kean/Nuke-Alamofire-Plugin) that implements `DataLoading` protocol using [Alamofire](https://github.com/Alamofire/Alamofire) framework
|
||||
|
||||
### Memory Cache
|
||||
|
||||
Processed images which are ready to be displayed are stored in a fast in-memory cache (`ImageCache`). It uses [LRU (least recently used)](https://en.wikipedia.org/wiki/Cache_algorithms#Examples) replacement algorithm and has a limit which prevents it from using more than ~20% of available RAM. As a good citizen, `ImageCache` automatically evicts images on memory warnings and removes most of the images when the application enters background.
|
||||
|
||||
### Resumable Downloads
|
||||
|
||||
If the data task is terminated (either because of a failure or a cancelation) and the image was partially loaded, the next load will resume where it was left off.
|
||||
|
||||
Resumable downloads require server to support [HTTP Range Requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests). Nuke supports both validators (`ETag` and `Last-Modified`). The resumable downloads are enabled by default.
|
||||
|
||||
> By default resumable data is stored in an efficient memory cache. Future versions might include more customization.
|
||||
|
||||
### Request Dedupication
|
||||
|
||||
By default `ImagePipeline` combines the requests for the same image (but can be different processors) into the same task. The task's priority is set to the highest priority of registered requests and gets updated when requests are added or removed to the task. The task only gets canceled when all the registered requests are.
|
||||
|
||||
> Deduplication can be disabled using `ImagePipeline.Configuration`.
|
||||
|
||||
<a name="h_performance"></a>
|
||||
# Performance
|
||||
|
||||
Performance is one of the key differentiating factors for Nuke.
|
||||
|
||||
The framework is tuned to do as little work on the main thread as possible. It uses multiple optimizations techniques to achieve that: reducing number of allocations, reducing dynamic dispatch, backing some structs by reference typed storage to reduce ARC overhead, etc.
|
||||
|
||||
Nuke is fully asynchronous and works great under stress. `ImagePipeline` schedules each of its stages on a dedicated queue. Each queue limits the number of concurrent tasks, respect request priorities even when moving between queue, and cancels the work as soon as possible. Under certain loads, `ImagePipeline` will also rate limit the requests to prevent trashing of the underlying systems.
|
||||
|
||||
Another important performance characteristic is memory usage. Nuke uses a custom memory cache with [LRU (least recently used)](https://en.wikipedia.org/wiki/Cache_algorithms#Examples) replacement algorithm. It has a limit which prevents it from using more than ~20% of available RAM. As a good citizen, `ImageCache` automatically evicts images on memory warnings and removes most of the images when the application enters background.
|
||||
|
||||
### Performance Metrics
|
||||
|
||||
When optimizing performance, it's important to measure. Nuke collects detailed performance metrics during the execution of each image task:
|
||||
|
||||
```swift
|
||||
ImagePipeline.shared.didFinishCollectingMetrics = { task, metrics in
|
||||
print(metrics)
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
```
|
||||
(lldb) po metrics
|
||||
|
||||
Task Information {
|
||||
Task ID - 1
|
||||
Duration - 22:35:16.123 – 22:35:16.475 (0.352s)
|
||||
Was canceled - false
|
||||
Is Memory Cache Hit - false
|
||||
Was Subscribed To Existing Session - false
|
||||
}
|
||||
Session Information {
|
||||
Session ID - 1
|
||||
Total Duration - 0.351s
|
||||
Was Canceled - false
|
||||
}
|
||||
Timeline {
|
||||
22:35:16.124 – 22:35:16.475 (0.351s) - Total
|
||||
------------------------------------
|
||||
nil – nil (nil) - Check Disk Cache
|
||||
22:35:16.131 – 22:35:16.410 (0.278s) - Load Data
|
||||
22:35:16.410 – 22:35:16.468 (0.057s) - Decode
|
||||
22:35:16.469 – 22:35:16.474 (0.005s) - Process
|
||||
}
|
||||
Resumable Data {
|
||||
Was Resumed - nil
|
||||
Resumable Data Count - nil
|
||||
Server Confirmed Resume - nil
|
||||
}
|
||||
```
|
||||
|
||||
<a name="h_plugins"></a>
|
||||
# Extensions
|
||||
|
||||
There are a variety extensions available for Nuke some of which are built by the community.
|
||||
|
||||
|Name|Description|
|
||||
|--|--|
|
||||
|[**RxNuke**](https://github.com/kean/RxNuke)|[RxSwift](https://github.com/ReactiveX/RxSwift) extensions for Nuke with examples of common use cases solved by Rx|
|
||||
|[**Alamofire**](https://github.com/kean/Nuke-Alamofire-Plugin)|Replace networking layer with [Alamofire](https://github.com/Alamofire/Alamofire) and combine the power of both frameworks|
|
||||
|[**WebP**](https://github.com/ryokosuge/Nuke-WebP-Plugin)| **[Community]** [WebP](https://developers.google.com/speed/webp/) support, built by [Ryo Kosuge](https://github.com/ryokosuge)|
|
||||
|[**Gifu**](https://github.com/kean/Nuke-Gifu-Plugin)|Use [Gifu](https://github.com/kaishin/Gifu) to load and display animated GIFs|
|
||||
|[**FLAnimatedImage**](https://github.com/kean/Nuke-AnimatedImage-Plugin)|Use [FLAnimatedImage](https://github.com/Flipboard/FLAnimatedImage) to load and display [animated GIFs]((https://www.youtube.com/watch?v=fEJqQMJrET4))|
|
||||
|
||||
|
||||
<a name="h_contribute"></a>
|
||||
# Contribution
|
||||
|
||||
[Nuke's roadmap](https://trello.com/b/Us4rHryT/nuke) is managed in Trello and is publically available.
|
||||
|
||||
If you'd like to contribute, please feel free to create a PR.
|
||||
|
||||
<a name="h_requirements"></a>
|
||||
# Requirements
|
||||
|
||||
| Nuke | Swift | Xcode | Platforms |
|
||||
|------------------ |----------------------- |------------------ |------------------------------------------------- |
|
||||
| Nuke 7.6 | Swift 4.2 – 5.0 | Xcode 10.1 – 10.2 | iOS 10.0 / watchOS 3.0 / macOS 10.12 / tvOS 10.0 |
|
||||
| Nuke 7.2 – 7.5.2 | Swift 4.0 – 4.2 | Xcode 9.2 – 10.1 | iOS 9.0 / watchOS 2.0 / macOS 10.10 / tvOS 9.0 |
|
||||
|
||||
# License
|
||||
|
||||
Nuke is available under the MIT license. See the LICENSE file for more info.
|
||||
441
Pods/Nuke/Sources/DataCache.swift
generated
Normal file
@@ -0,0 +1,441 @@
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) 2015-2019 Alexander Grebenyuk (github.com/kean).
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - DataCaching
|
||||
|
||||
/// Data cache.
|
||||
///
|
||||
/// - warning: The implementation must be thread safe.
|
||||
public protocol DataCaching {
|
||||
/// Retrieves data from cache for the given key.
|
||||
func cachedData(for key: String) -> Data?
|
||||
|
||||
/// Stores data for the given key.
|
||||
/// - note: The implementation must return immediately and store data
|
||||
/// asynchronously.
|
||||
func storeData(_ data: Data, for key: String)
|
||||
}
|
||||
|
||||
// MARK: - DataCache
|
||||
|
||||
/// Data cache backed by a local storage.
|
||||
///
|
||||
/// The DataCache uses LRU cleanup policy (least recently used items are removed
|
||||
/// first). The elements stored in the cache are automatically discarded if
|
||||
/// either *cost* or *count* limit is reached. The sweeps are performed periodically.
|
||||
///
|
||||
/// DataCache always writes and removes data asynchronously. It also allows for
|
||||
/// reading and writing data in parallel. This is implemented using a "staging"
|
||||
/// area which stores changes until they are flushed to disk:
|
||||
///
|
||||
/// // Schedules data to be written asynchronously and returns immediately
|
||||
/// cache[key] = data
|
||||
///
|
||||
/// // The data is returned from the staging area
|
||||
/// let data = cache[key]
|
||||
///
|
||||
/// // Schedules data to be removed asynchronously and returns immediately
|
||||
/// cache[key] = nil
|
||||
///
|
||||
/// // Data is nil
|
||||
/// let data = cache[key]
|
||||
///
|
||||
/// Thread-safe.
|
||||
///
|
||||
/// - warning: It's possible to have more than one instance of `DataCache` with
|
||||
/// the same `path` but it is not recommended.
|
||||
public final class DataCache: DataCaching {
|
||||
/// A cache key.
|
||||
public typealias Key = String
|
||||
|
||||
/// The maximum number of items. `1000` by default.
|
||||
///
|
||||
/// Changes tos `countLimit` will take effect when the next LRU sweep is run.
|
||||
public var countLimit: Int = 1000
|
||||
|
||||
/// Size limit in bytes. `100 Mb` by default.
|
||||
///
|
||||
/// Changes to `sizeLimit` will take effect when the next LRU sweep is run.
|
||||
public var sizeLimit: Int = 1024 * 1024 * 100
|
||||
|
||||
/// When performing a sweep, the cache will remote entries until the size of
|
||||
/// the remaining items is lower than or equal to `sizeLimit * trimRatio` and
|
||||
/// the total count is lower than or equal to `countLimit * trimRatio`. `0.7`
|
||||
/// by default.
|
||||
internal var trimRatio = 0.7
|
||||
|
||||
/// The path for the directory managed by the cache.
|
||||
public let path: URL
|
||||
|
||||
/// The number of seconds between each LRU sweep. 30 by default.
|
||||
/// The first sweep is performed right after the cache is initialized.
|
||||
///
|
||||
/// Sweeps are performed in a background and can be performed in parallel
|
||||
/// with reading.
|
||||
public var sweepInterval: TimeInterval = 30
|
||||
|
||||
/// The delay after which the initial sweep is performed. 10 by default.
|
||||
/// The initial sweep is performed after a delay to avoid competing with
|
||||
/// other subsystems for the resources.
|
||||
private var initialSweepDelay: TimeInterval = 15
|
||||
|
||||
// Staging
|
||||
private let _lock = NSLock()
|
||||
private var _staging = Staging()
|
||||
|
||||
/* testable */ let _wqueue = DispatchQueue(label: "com.github.kean.Nuke.DataCache.WriteQueue")
|
||||
|
||||
/// A function which generates a filename for the given key. A good candidate
|
||||
/// for a filename generator is a _cryptographic_ hash function like SHA1.
|
||||
///
|
||||
/// The reason why filename needs to be generated in the first place is
|
||||
/// that filesystems have a size limit for filenames (e.g. 255 UTF-8 characters
|
||||
/// in AFPS) and do not allow certain characters to be used in filenames.
|
||||
public typealias FilenameGenerator = (_ key: String) -> String?
|
||||
|
||||
private let _filenameGenerator: FilenameGenerator
|
||||
|
||||
/// Creates a cache instance with a given `name`. The cache creates a directory
|
||||
/// with the given `name` in a `.cachesDirectory` in `.userDomainMask`.
|
||||
/// - parameter filenameGenerator: Generates a filename for the given URL.
|
||||
/// The default implementation generates a filename using SHA1 hash function.
|
||||
public convenience init(name: String, filenameGenerator: @escaping (String) -> String? = DataCache.filename(for:)) throws {
|
||||
guard let root = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else {
|
||||
throw NSError(domain: NSCocoaErrorDomain, code: NSFileNoSuchFileError, userInfo: nil)
|
||||
}
|
||||
try self.init(path: root.appendingPathComponent(name, isDirectory: true), filenameGenerator: filenameGenerator)
|
||||
}
|
||||
|
||||
/// Creates a cache instance with a given path.
|
||||
/// - parameter filenameGenerator: Generates a filename for the given URL.
|
||||
/// The default implementation generates a filename using SHA1 hash function.
|
||||
public init(path: URL, filenameGenerator: @escaping (String) -> String? = DataCache.filename(for:)) throws {
|
||||
self.path = path
|
||||
self._filenameGenerator = filenameGenerator
|
||||
try self._didInit()
|
||||
}
|
||||
|
||||
/// A `FilenameGenerator` implementation which uses SHA1 hash function to
|
||||
/// generate a filename from the given key.
|
||||
public static func filename(for key: String) -> String? {
|
||||
return key.sha1
|
||||
}
|
||||
|
||||
private func _didInit() throws {
|
||||
try FileManager.default.createDirectory(at: path, withIntermediateDirectories: true, attributes: nil)
|
||||
_wqueue.asyncAfter(deadline: .now() + initialSweepDelay) { [weak self] in
|
||||
self?._performAndScheduleSweep()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: DataCaching
|
||||
|
||||
/// Retrieves data for the given key. The completion will be called
|
||||
/// syncrhonously if there is no cached data for the given key.
|
||||
public func cachedData(for key: Key) -> Data? {
|
||||
_lock.lock()
|
||||
|
||||
if let change = _staging.change(for: key) {
|
||||
_lock.unlock()
|
||||
switch change {
|
||||
case let .add(data):
|
||||
return data
|
||||
case .remove:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
_lock.unlock()
|
||||
|
||||
guard let url = _url(for: key) else {
|
||||
return nil
|
||||
}
|
||||
return try? Data(contentsOf: url)
|
||||
}
|
||||
|
||||
/// Stores data for the given key. The method returns instantly and the data
|
||||
/// is written asynchronously.
|
||||
public func storeData(_ data: Data, for key: Key) {
|
||||
_lock.sync {
|
||||
let change = _staging.add(data: data, for: key)
|
||||
_wqueue.async {
|
||||
if let url = self._url(for: key) {
|
||||
try? data.write(to: url)
|
||||
}
|
||||
self._lock.sync {
|
||||
self._staging.flushed(change)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes data for the given key. The method returns instantly, the data
|
||||
/// is removed asynchronously.
|
||||
public func removeData(for key: Key) {
|
||||
_lock.sync {
|
||||
let change = _staging.removeData(for: key)
|
||||
_wqueue.async {
|
||||
if let url = self._url(for: key) {
|
||||
try? FileManager.default.removeItem(at: url)
|
||||
}
|
||||
self._lock.sync {
|
||||
self._staging.flushed(change)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes all items. The method returns instantly, the data is removed
|
||||
/// asynchronously.
|
||||
public func removeAll() {
|
||||
_lock.sync {
|
||||
let change = _staging.removeAll()
|
||||
_wqueue.async {
|
||||
try? FileManager.default.removeItem(at: self.path)
|
||||
try? FileManager.default.createDirectory(at: self.path, withIntermediateDirectories: true, attributes: nil)
|
||||
self._lock.sync {
|
||||
self._staging.flushed(change)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Accesses the data associated with the given key for reading and writing.
|
||||
///
|
||||
/// When you assign a new data for a key and the key already exists, the cache
|
||||
/// overwrites the existing data.
|
||||
///
|
||||
/// When assigning or removing data, the subscript adds a requested operation
|
||||
/// in a staging area and returns immediately. The staging area allows for
|
||||
/// reading and writing data in parallel.
|
||||
///
|
||||
/// // Schedules data to be written asynchronously and returns immediately
|
||||
/// cache[key] = data
|
||||
///
|
||||
/// // The data is returned from the staging area
|
||||
/// let data = cache[key]
|
||||
///
|
||||
/// // Schedules data to be removed asynchronously and returns immediately
|
||||
/// cache[key] = nil
|
||||
///
|
||||
/// // Data is nil
|
||||
/// let data = cache[key]
|
||||
///
|
||||
public subscript(key: Key) -> Data? {
|
||||
get {
|
||||
return cachedData(for: key)
|
||||
}
|
||||
set {
|
||||
if let data = newValue {
|
||||
storeData(data, for: key)
|
||||
} else {
|
||||
removeData(for: key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Managing URLs
|
||||
|
||||
/// Uses the `FilenameGenerator` that the cache was initialized with to
|
||||
/// generate and return a filename for the given key.
|
||||
public func filename(for key: Key) -> String? {
|
||||
return _filenameGenerator(key)
|
||||
}
|
||||
|
||||
/* testable */ func _url(for key: Key) -> URL? {
|
||||
guard let filename = self.filename(for: key) else {
|
||||
return nil
|
||||
}
|
||||
return self.path.appendingPathComponent(filename, isDirectory: false)
|
||||
}
|
||||
|
||||
// MARK: Flush Changes
|
||||
|
||||
/// Synchronously waits on the caller's thread until all outstanding disk IO
|
||||
/// operations are finished.
|
||||
func flush() {
|
||||
_wqueue.sync {}
|
||||
}
|
||||
|
||||
// MARK: Sweep
|
||||
|
||||
private func _performAndScheduleSweep() {
|
||||
_sweep()
|
||||
_wqueue.asyncAfter(deadline: .now() + sweepInterval) { [weak self] in
|
||||
self?._performAndScheduleSweep()
|
||||
}
|
||||
}
|
||||
|
||||
/// Schedules a cache sweep to be performed immediately.
|
||||
public func sweep() {
|
||||
_wqueue.async {
|
||||
self._sweep()
|
||||
}
|
||||
}
|
||||
|
||||
/// Discards the least recently used items first.
|
||||
private func _sweep() {
|
||||
var items = contents(keys: [.contentAccessDateKey, .totalFileAllocatedSizeKey])
|
||||
guard !items.isEmpty else {
|
||||
return
|
||||
}
|
||||
var size = items.reduce(0) { $0 + ($1.meta.totalFileAllocatedSize ?? 0) }
|
||||
var count = items.count
|
||||
let sizeLimit = self.sizeLimit / Int(1 / trimRatio)
|
||||
let countLimit = self.countLimit / Int(1 / trimRatio)
|
||||
|
||||
guard size > sizeLimit || count > countLimit else {
|
||||
return // All good, no need to perform any work.
|
||||
}
|
||||
|
||||
// Most recently accessed items first
|
||||
let past = Date.distantPast
|
||||
items.sort { // Sort in place
|
||||
($0.meta.contentAccessDate ?? past) > ($1.meta.contentAccessDate ?? past)
|
||||
}
|
||||
|
||||
// Remove the items until we satisfy both size and count limits.
|
||||
while (size > sizeLimit || count > countLimit), let item = items.popLast() {
|
||||
size -= (item.meta.totalFileAllocatedSize ?? 0)
|
||||
count -= 1
|
||||
try? FileManager.default.removeItem(at: item.url)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Contents
|
||||
|
||||
struct Entry {
|
||||
let url: URL
|
||||
let meta: URLResourceValues
|
||||
}
|
||||
|
||||
func contents(keys: [URLResourceKey] = []) -> [Entry] {
|
||||
guard let urls = try? FileManager.default.contentsOfDirectory(at: path, includingPropertiesForKeys: keys, options: .skipsHiddenFiles) else {
|
||||
return []
|
||||
}
|
||||
let _keys = Set(keys)
|
||||
return urls.compactMap {
|
||||
guard let meta = try? $0.resourceValues(forKeys: _keys) else {
|
||||
return nil
|
||||
}
|
||||
return Entry(url: $0, meta: meta)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Inspection
|
||||
|
||||
/// The total number of items in the cache.
|
||||
/// - warning: Requires disk IO, avoid using from the main thread.
|
||||
public var totalCount: Int {
|
||||
return contents().count
|
||||
}
|
||||
|
||||
/// The total file size of items written on disk.
|
||||
///
|
||||
/// Uses `URLResourceKey.fileSizeKey` to calculate the size of each entry.
|
||||
/// The total allocated size (see `totalAllocatedSize`. on disk might
|
||||
/// actually be bigger.
|
||||
///
|
||||
/// - warning: Requires disk IO, avoid using from the main thread.
|
||||
public var totalSize: Int {
|
||||
return contents(keys: [.fileSizeKey]).reduce(0) {
|
||||
$0 + ($1.meta.fileSize ?? 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// The total file allocated size of all the items written on disk.
|
||||
///
|
||||
/// Uses `URLResourceKey.totalFileAllocatedSizeKey`.
|
||||
///
|
||||
/// - warning: Requires disk IO, avoid using from the main thread.
|
||||
public var totalAllocatedSize: Int {
|
||||
return contents(keys: [.totalFileAllocatedSizeKey]).reduce(0) {
|
||||
$0 + ($1.meta.totalFileAllocatedSize ?? 0)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Staging
|
||||
|
||||
/// DataCache allows for parallel reads and writes. This is made possible by
|
||||
/// DataCacheStaging.
|
||||
///
|
||||
/// For example, when the data is added in cache, it is first added to staging
|
||||
/// and is removed from staging only after data is written to disk. Removal works
|
||||
/// the same way.
|
||||
private final class Staging {
|
||||
private var changes = [String: Change]()
|
||||
private var changeRemoveAll: ChangeRemoveAll?
|
||||
|
||||
struct ChangeRemoveAll {
|
||||
let id: Int
|
||||
}
|
||||
|
||||
struct Change {
|
||||
let key: String
|
||||
let id: Int
|
||||
let type: ChangeType
|
||||
}
|
||||
|
||||
enum ChangeType {
|
||||
case add(Data)
|
||||
case remove
|
||||
}
|
||||
|
||||
private var nextChangeId = 0
|
||||
|
||||
// MARK: Changes
|
||||
|
||||
func change(for key: String) -> ChangeType? {
|
||||
if let change = changes[key] {
|
||||
return change.type
|
||||
}
|
||||
if changeRemoveAll != nil {
|
||||
return .remove
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MARK: Register Changes
|
||||
|
||||
func add(data: Data, for key: String) -> Change {
|
||||
return _makeChange(.add(data), for: key)
|
||||
}
|
||||
|
||||
func removeData(for key: String) -> Change {
|
||||
return _makeChange(.remove, for: key)
|
||||
}
|
||||
|
||||
private func _makeChange(_ type: ChangeType, for key: String) -> Change {
|
||||
nextChangeId += 1
|
||||
let change = Change(key: key, id: nextChangeId, type: type)
|
||||
changes[key] = change
|
||||
return change
|
||||
}
|
||||
|
||||
func removeAll() -> ChangeRemoveAll {
|
||||
nextChangeId += 1
|
||||
let change = ChangeRemoveAll(id: nextChangeId)
|
||||
changeRemoveAll = change
|
||||
changes.removeAll()
|
||||
return change
|
||||
}
|
||||
|
||||
// MARK: Flush Changes
|
||||
|
||||
func flushed(_ change: Change) {
|
||||
if let index = changes.index(forKey: change.key),
|
||||
changes[index].value.id == change.id {
|
||||
changes.remove(at: index)
|
||||
}
|
||||
}
|
||||
|
||||
func flushed(_ change: ChangeRemoveAll) {
|
||||
if changeRemoveAll?.id == change.id {
|
||||
changeRemoveAll = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
160
Pods/Nuke/Sources/DataLoader.swift
generated
Normal file
@@ -0,0 +1,160 @@
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) 2015-2019 Alexander Grebenyuk (github.com/kean).
|
||||
|
||||
import Foundation
|
||||
|
||||
public protocol Cancellable: class {
|
||||
func cancel()
|
||||
}
|
||||
|
||||
public protocol DataLoading {
|
||||
/// - parameter didReceiveData: Can be called multiple times if streaming
|
||||
/// is supported.
|
||||
/// - parameter completion: Must be called once after all (or none in case
|
||||
/// of an error) `didReceiveData` closures have been called.
|
||||
func loadData(with request: URLRequest,
|
||||
didReceiveData: @escaping (Data, URLResponse) -> Void,
|
||||
completion: @escaping (Error?) -> Void) -> Cancellable
|
||||
}
|
||||
|
||||
extension URLSessionTask: Cancellable {}
|
||||
|
||||
/// Provides basic networking using `URLSession`.
|
||||
public final class DataLoader: DataLoading {
|
||||
public let session: URLSession
|
||||
private let _impl: _DataLoader
|
||||
|
||||
/// Initializes `DataLoader` with the given configuration.
|
||||
/// - parameter configuration: `URLSessionConfiguration.default` with
|
||||
/// `URLCache` with 0 MB memory capacity and 150 MB disk capacity.
|
||||
public init(configuration: URLSessionConfiguration = DataLoader.defaultConfiguration,
|
||||
validate: @escaping (URLResponse) -> Swift.Error? = DataLoader.validate) {
|
||||
self._impl = _DataLoader()
|
||||
self.session = URLSession(configuration: configuration, delegate: _impl, delegateQueue: _impl.queue)
|
||||
self._impl.session = self.session
|
||||
self._impl.validate = validate
|
||||
}
|
||||
|
||||
/// Returns a default configuration which has a `sharedUrlCache` set
|
||||
/// as a `urlCache`.
|
||||
public static var defaultConfiguration: URLSessionConfiguration {
|
||||
let conf = URLSessionConfiguration.default
|
||||
conf.urlCache = DataLoader.sharedUrlCache
|
||||
return conf
|
||||
}
|
||||
|
||||
/// Validates `HTTP` responses by checking that the status code is 2xx. If
|
||||
/// it's not returns `DataLoader.Error.statusCodeUnacceptable`.
|
||||
public static func validate(response: URLResponse) -> Swift.Error? {
|
||||
guard let response = response as? HTTPURLResponse else { return nil }
|
||||
return (200..<300).contains(response.statusCode) ? nil : Error.statusCodeUnacceptable(response.statusCode)
|
||||
}
|
||||
|
||||
#if !os(macOS)
|
||||
private static let cachePath = "com.github.kean.Nuke.Cache"
|
||||
#else
|
||||
private static let cachePath: String = {
|
||||
let cachePaths = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true)
|
||||
if let cachePath = cachePaths.first, let identifier = Bundle.main.bundleIdentifier {
|
||||
return cachePath.appending("/" + identifier)
|
||||
}
|
||||
|
||||
return ""
|
||||
}()
|
||||
#endif
|
||||
|
||||
/// Shared url cached used by a default `DataLoader`. The cache is
|
||||
/// initialized with 0 MB memory capacity and 150 MB disk capacity.
|
||||
public static let sharedUrlCache = URLCache(
|
||||
memoryCapacity: 0,
|
||||
diskCapacity: 150 * 1024 * 1024, // 150 MB
|
||||
diskPath: cachePath
|
||||
)
|
||||
|
||||
public func loadData(with request: URLRequest, didReceiveData: @escaping (Data, URLResponse) -> Void, completion: @escaping (Swift.Error?) -> Void) -> Cancellable {
|
||||
return _impl.loadData(with: request, didReceiveData: didReceiveData, completion: completion)
|
||||
}
|
||||
|
||||
/// Errors produced by `DataLoader`.
|
||||
public enum Error: Swift.Error, CustomDebugStringConvertible {
|
||||
/// Validation failed.
|
||||
case statusCodeUnacceptable(Int)
|
||||
/// Either the response or body was empty.
|
||||
@available(*, deprecated, message: "This error case is not used any more")
|
||||
case responseEmpty
|
||||
|
||||
public var debugDescription: String {
|
||||
switch self {
|
||||
case let .statusCodeUnacceptable(code): return "Response status code was unacceptable: " + code.description // compiles faster than interpolation
|
||||
case .responseEmpty: return "Either the response or body was empty."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Actual data loader implementation. We hide NSObject inheritance, hide
|
||||
// URLSessionDataDelegate conformance, and break retain cycle between URLSession
|
||||
// and URLSessionDataDelegate.
|
||||
private final class _DataLoader: NSObject, URLSessionDataDelegate {
|
||||
weak var session: URLSession! // This is safe.
|
||||
var validate: (URLResponse) -> Swift.Error? = DataLoader.validate
|
||||
let queue = OperationQueue()
|
||||
|
||||
private var handlers = [URLSessionTask: _Handler]()
|
||||
|
||||
override init() {
|
||||
self.queue.maxConcurrentOperationCount = 1
|
||||
}
|
||||
|
||||
/// Loads data with the given request.
|
||||
func loadData(with request: URLRequest, didReceiveData: @escaping (Data, URLResponse) -> Void, completion: @escaping (Error?) -> Void) -> Cancellable {
|
||||
let task = session.dataTask(with: request)
|
||||
let handler = _Handler(didReceiveData: didReceiveData, completion: completion)
|
||||
queue.addOperation { // `URLSession` is configured to use this same queue
|
||||
self.handlers[task] = handler
|
||||
}
|
||||
task.resume()
|
||||
return task
|
||||
}
|
||||
|
||||
// MARK: URLSessionDelegate
|
||||
|
||||
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
|
||||
guard let handler = handlers[dataTask] else {
|
||||
completionHandler(.cancel)
|
||||
return
|
||||
}
|
||||
// Validate response as soon as we receive it can cancel the request if necessary
|
||||
if let error = validate(response) {
|
||||
handler.completion(error)
|
||||
completionHandler(.cancel)
|
||||
return
|
||||
}
|
||||
completionHandler(.allow)
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
|
||||
guard let handler = handlers[task] else { return }
|
||||
handlers[task] = nil
|
||||
handler.completion(error)
|
||||
}
|
||||
|
||||
// MARK: URLSessionDataDelegate
|
||||
|
||||
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
|
||||
guard let handler = handlers[dataTask], let response = dataTask.response else { return }
|
||||
// We don't store data anywhere, just send it to the pipeline.
|
||||
handler.didReceiveData(data, response)
|
||||
}
|
||||
|
||||
private final class _Handler {
|
||||
let didReceiveData: (Data, URLResponse) -> Void
|
||||
let completion: (Error?) -> Void
|
||||
|
||||
init(didReceiveData: @escaping (Data, URLResponse) -> Void, completion: @escaping (Error?) -> Void) {
|
||||
self.didReceiveData = didReceiveData
|
||||
self.completion = completion
|
||||
}
|
||||
}
|
||||
}
|
||||
300
Pods/Nuke/Sources/ImageCache.swift
generated
Normal file
@@ -0,0 +1,300 @@
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) 2015-2019 Alexander Grebenyuk (github.com/kean).
|
||||
|
||||
import Foundation
|
||||
#if !os(macOS)
|
||||
import UIKit
|
||||
#else
|
||||
import Cocoa
|
||||
#endif
|
||||
|
||||
/// In-memory image cache.
|
||||
///
|
||||
/// The implementation must be thread safe.
|
||||
public protocol ImageCaching: class {
|
||||
/// Returns the `ImageResponse` stored in the cache with the given request.
|
||||
func cachedResponse(for request: ImageRequest) -> ImageResponse?
|
||||
|
||||
/// Stores the given `ImageResponse` in the cache using the given request.
|
||||
func storeResponse(_ response: ImageResponse, for request: ImageRequest)
|
||||
|
||||
/// Remove the response for the given request.
|
||||
func removeResponse(for request: ImageRequest)
|
||||
}
|
||||
|
||||
/// Convenience subscript.
|
||||
public extension ImageCaching {
|
||||
/// Accesses the image associated with the given request.
|
||||
subscript(request: ImageRequest) -> Image? {
|
||||
get {
|
||||
return cachedResponse(for: request)?.image
|
||||
}
|
||||
set {
|
||||
if let newValue = newValue {
|
||||
storeResponse(ImageResponse(image: newValue, urlResponse: nil), for: request)
|
||||
} else {
|
||||
removeResponse(for: request)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Memory cache with LRU cleanup policy (least recently used are removed first).
|
||||
///
|
||||
/// The elements stored in cache are automatically discarded if either *cost* or
|
||||
/// *count* limit is reached. The default cost limit represents a number of bytes
|
||||
/// and is calculated based on the amount of physical memory available on the
|
||||
/// device. The default cmount limit is set to `Int.max`.
|
||||
///
|
||||
/// `Cache` automatically removes all stored elements when it received a
|
||||
/// memory warning. It also automatically removes *most* of cached elements
|
||||
/// when the app enters background.
|
||||
public final class ImageCache: ImageCaching {
|
||||
private let _impl: _Cache<ImageRequest.CacheKey, ImageResponse>
|
||||
|
||||
/// The maximum total cost that the cache can hold.
|
||||
public var costLimit: Int {
|
||||
get { return _impl.costLimit }
|
||||
set { _impl.costLimit = newValue }
|
||||
}
|
||||
|
||||
/// The maximum number of items that the cache can hold.
|
||||
public var countLimit: Int {
|
||||
get { return _impl.countLimit }
|
||||
set { _impl.countLimit = newValue }
|
||||
}
|
||||
|
||||
/// Default TTL (time to live) for each entry. Can be used to make sure that
|
||||
/// the entries get validated at some point. `0` (never expire) by default.
|
||||
public var ttl: TimeInterval {
|
||||
get { return _impl.ttl }
|
||||
set { _impl.ttl = newValue }
|
||||
}
|
||||
|
||||
/// The total cost of items in the cache.
|
||||
public var totalCost: Int {
|
||||
return _impl.totalCost
|
||||
}
|
||||
|
||||
/// The total number of items in the cache.
|
||||
public var totalCount: Int {
|
||||
return _impl.totalCount
|
||||
}
|
||||
|
||||
/// Shared `Cache` instance.
|
||||
public static let shared = ImageCache()
|
||||
|
||||
/// Initializes `Cache`.
|
||||
/// - parameter costLimit: Default value representes a number of bytes and is
|
||||
/// calculated based on the amount of the phisical memory available on the device.
|
||||
/// - parameter countLimit: `Int.max` by default.
|
||||
public init(costLimit: Int = ImageCache.defaultCostLimit(), countLimit: Int = Int.max) {
|
||||
_impl = _Cache(costLimit: costLimit, countLimit: countLimit)
|
||||
}
|
||||
|
||||
/// Returns a recommended cost limit which is computed based on the amount
|
||||
/// of the phisical memory available on the device.
|
||||
public static func defaultCostLimit() -> Int {
|
||||
let physicalMemory = ProcessInfo.processInfo.physicalMemory
|
||||
let ratio = physicalMemory <= (536_870_912 /* 512 Mb */) ? 0.1 : 0.2
|
||||
let limit = physicalMemory / UInt64(1 / ratio)
|
||||
return limit > UInt64(Int.max) ? Int.max : Int(limit)
|
||||
}
|
||||
|
||||
/// Returns the `ImageResponse` stored in the cache with the given request.
|
||||
public func cachedResponse(for request: ImageRequest) -> ImageResponse? {
|
||||
return _impl.value(forKey: ImageRequest.CacheKey(request: request))
|
||||
}
|
||||
|
||||
/// Stores the given `ImageResponse` in the cache using the given request.
|
||||
public func storeResponse(_ response: ImageResponse, for request: ImageRequest) {
|
||||
_impl.set(response, forKey: ImageRequest.CacheKey(request: request), cost: self.cost(for: response.image))
|
||||
}
|
||||
|
||||
/// Removes response stored with the given request.
|
||||
public func removeResponse(for request: ImageRequest) {
|
||||
_impl.removeValue(forKey: ImageRequest.CacheKey(request: request))
|
||||
}
|
||||
|
||||
/// Removes all cached images.
|
||||
public func removeAll() {
|
||||
_impl.removeAll()
|
||||
}
|
||||
/// Removes least recently used items from the cache until the total cost
|
||||
/// of the remaining items is less than the given cost limit.
|
||||
public func trim(toCost limit: Int) {
|
||||
_impl.trim(toCost: limit)
|
||||
}
|
||||
|
||||
/// Removes least recently used items from the cache until the total count
|
||||
/// of the remaining items is less than the given count limit.
|
||||
public func trim(toCount limit: Int) {
|
||||
_impl.trim(toCount: limit)
|
||||
}
|
||||
|
||||
/// Returns cost for the given image by approximating its bitmap size in bytes in memory.
|
||||
func cost(for image: Image) -> Int {
|
||||
#if !os(macOS)
|
||||
let dataCost = ImagePipeline.Configuration.isAnimatedImageDataEnabled ? (image.animatedImageData?.count ?? 0) : 0
|
||||
|
||||
// bytesPerRow * height gives a rough estimation of how much memory
|
||||
// image uses in bytes. In practice this algorithm combined with a
|
||||
// concervative default cost limit works OK.
|
||||
guard let cgImage = image.cgImage else {
|
||||
return 1 + dataCost
|
||||
}
|
||||
return cgImage.bytesPerRow * cgImage.height + dataCost
|
||||
|
||||
#else
|
||||
return 1
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
internal final class _Cache<Key: Hashable, Value> {
|
||||
// We don't use `NSCache` because it's not LRU
|
||||
|
||||
private var map = [Key: LinkedList<Entry>.Node]()
|
||||
private let list = LinkedList<Entry>()
|
||||
private let lock = NSLock()
|
||||
|
||||
var costLimit: Int {
|
||||
didSet { lock.sync(_trim) }
|
||||
}
|
||||
|
||||
var countLimit: Int {
|
||||
didSet { lock.sync(_trim) }
|
||||
}
|
||||
|
||||
private(set) var totalCost = 0
|
||||
var ttl: TimeInterval = 0
|
||||
|
||||
var totalCount: Int {
|
||||
return map.count
|
||||
}
|
||||
|
||||
init(costLimit: Int, countLimit: Int) {
|
||||
self.costLimit = costLimit
|
||||
self.countLimit = countLimit
|
||||
#if os(iOS) || os(tvOS)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(removeAll), name: UIApplication.didReceiveMemoryWarningNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(didEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
|
||||
#endif
|
||||
}
|
||||
|
||||
deinit {
|
||||
#if os(iOS) || os(tvOS)
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
#endif
|
||||
}
|
||||
|
||||
func value(forKey key: Key) -> Value? {
|
||||
lock.lock(); defer { lock.unlock() }
|
||||
|
||||
guard let node = map[key] else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard !node.value.isExpired else {
|
||||
_remove(node: node)
|
||||
return nil
|
||||
}
|
||||
|
||||
// bubble node up to make it last added (most recently used)
|
||||
list.remove(node)
|
||||
list.append(node)
|
||||
|
||||
return node.value.value
|
||||
}
|
||||
|
||||
func set(_ value: Value, forKey key: Key, cost: Int = 0, ttl: TimeInterval? = nil) {
|
||||
lock.lock(); defer { lock.unlock() }
|
||||
|
||||
let ttl = ttl ?? self.ttl
|
||||
let expiration = ttl == 0 ? nil : (Date() + ttl)
|
||||
let entry = Entry(value: value, key: key, cost: cost, expiration: expiration)
|
||||
_add(entry)
|
||||
_trim() // _trim is extremely fast, it's OK to call it each time
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func removeValue(forKey key: Key) -> Value? {
|
||||
lock.lock(); defer { lock.unlock() }
|
||||
|
||||
guard let node = map[key] else { return nil }
|
||||
_remove(node: node)
|
||||
return node.value.value
|
||||
}
|
||||
|
||||
private func _add(_ element: Entry) {
|
||||
if let existingNode = map[element.key] {
|
||||
_remove(node: existingNode)
|
||||
}
|
||||
map[element.key] = list.append(element)
|
||||
totalCost += element.cost
|
||||
}
|
||||
|
||||
private func _remove(node: LinkedList<Entry>.Node) {
|
||||
list.remove(node)
|
||||
map[node.value.key] = nil
|
||||
totalCost -= node.value.cost
|
||||
}
|
||||
|
||||
@objc dynamic func removeAll() {
|
||||
lock.sync {
|
||||
map.removeAll()
|
||||
list.removeAll()
|
||||
totalCost = 0
|
||||
}
|
||||
}
|
||||
|
||||
private func _trim() {
|
||||
_trim(toCost: costLimit)
|
||||
_trim(toCount: countLimit)
|
||||
}
|
||||
|
||||
@objc private dynamic func didEnterBackground() {
|
||||
// Remove most of the stored items when entering background.
|
||||
// This behavior is similar to `NSCache` (which removes all
|
||||
// items). This feature is not documented and may be subject
|
||||
// to change in future Nuke versions.
|
||||
lock.sync {
|
||||
_trim(toCost: Int(Double(costLimit) * 0.1))
|
||||
_trim(toCount: Int(Double(countLimit) * 0.1))
|
||||
}
|
||||
}
|
||||
|
||||
func trim(toCost limit: Int) {
|
||||
lock.sync { _trim(toCost: limit) }
|
||||
}
|
||||
|
||||
private func _trim(toCost limit: Int) {
|
||||
_trim(while: { totalCost > limit })
|
||||
}
|
||||
|
||||
func trim(toCount limit: Int) {
|
||||
lock.sync { _trim(toCount: limit) }
|
||||
}
|
||||
|
||||
private func _trim(toCount limit: Int) {
|
||||
_trim(while: { totalCount > limit })
|
||||
}
|
||||
|
||||
private func _trim(while condition: () -> Bool) {
|
||||
while condition(), let node = list.first { // least recently used
|
||||
_remove(node: node)
|
||||
}
|
||||
}
|
||||
|
||||
private struct Entry {
|
||||
let value: Value
|
||||
let key: Key
|
||||
let cost: Int
|
||||
let expiration: Date?
|
||||
var isExpired: Bool {
|
||||
guard let expiration = expiration else { return false }
|
||||
return expiration.timeIntervalSinceNow < 0
|
||||
}
|
||||
}
|
||||
}
|
||||
219
Pods/Nuke/Sources/ImageDecoding.swift
generated
Normal file
@@ -0,0 +1,219 @@
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) 2015-2019 Alexander Grebenyuk (github.com/kean).
|
||||
|
||||
#if !os(macOS)
|
||||
import UIKit
|
||||
#else
|
||||
import Cocoa
|
||||
#endif
|
||||
|
||||
#if os(watchOS)
|
||||
import WatchKit
|
||||
#endif
|
||||
|
||||
// MARK: - ImageDecoding
|
||||
|
||||
/// Decodes image data.
|
||||
public protocol ImageDecoding {
|
||||
/// Produces an image from the image data. A decoder is a one-shot object
|
||||
/// created for a single image decoding session. If image pipeline has
|
||||
/// progressive decoding enabled, the `decode(data:isFinal:)` method gets
|
||||
/// called each time the data buffer has new data available. The decoder may
|
||||
/// decide whether or not to produce a new image based on the previous scans.
|
||||
func decode(data: Data, isFinal: Bool) -> Image?
|
||||
}
|
||||
|
||||
// An image decoder that uses native APIs. Supports progressive decoding.
|
||||
// The decoder is stateful.
|
||||
public final class ImageDecoder: ImageDecoding {
|
||||
// `nil` if decoder hasn't detected whether progressive decoding is enabled.
|
||||
private(set) internal var isProgressive: Bool?
|
||||
// Number of scans that the decoder has found so far. The last scan might be
|
||||
// incomplete at this point.
|
||||
private(set) internal var numberOfScans = 0
|
||||
private var lastStartOfScan: Int = 0 // Index of the last Start of Scan that we found
|
||||
private var scannedIndex: Int = -1 // Index at which previous scan was finished
|
||||
|
||||
public init() { }
|
||||
|
||||
public func decode(data: Data, isFinal: Bool) -> Image? {
|
||||
let format = ImageFormat.format(for: data)
|
||||
|
||||
guard !isFinal else { // Just decode the data.
|
||||
let image = _decode(data)
|
||||
if ImagePipeline.Configuration.isAnimatedImageDataEnabled, case .gif? = format { // Keep original data around in case of GIF
|
||||
image?.animatedImageData = data
|
||||
}
|
||||
return image
|
||||
}
|
||||
|
||||
// Determined (if we haven't yet) whether the image supports progressive
|
||||
// decoding or not (only proressive JPEG is allowed for now, but you can
|
||||
// add support for other formats by implementing your own decoder).
|
||||
isProgressive = isProgressive ?? format?.isProgressive
|
||||
guard isProgressive == true else { return nil }
|
||||
|
||||
// Check if there is more data to scan.
|
||||
guard (scannedIndex + 1) < data.count else { return nil }
|
||||
|
||||
// Start scaning from the where we left off previous time.
|
||||
var index = (scannedIndex + 1)
|
||||
var numberOfScans = self.numberOfScans
|
||||
while index < (data.count - 1) {
|
||||
scannedIndex = index
|
||||
// 0xFF, 0xDA - Start Of Scan
|
||||
if data[index] == 0xFF, data[index+1] == 0xDA {
|
||||
lastStartOfScan = index
|
||||
numberOfScans += 1
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
|
||||
// Found more scans this the previous time
|
||||
guard numberOfScans > self.numberOfScans else { return nil }
|
||||
self.numberOfScans = numberOfScans
|
||||
|
||||
// `> 1` checks that we've received a first scan (SOS) and then received
|
||||
// and also received a second scan (SOS). This way we know that we have
|
||||
// at least one full scan available.
|
||||
return (numberOfScans > 1 && lastStartOfScan > 0) ? _decode(data[0..<lastStartOfScan]) : nil
|
||||
}
|
||||
}
|
||||
|
||||
// Image initializers are documented as fully-thread safe:
|
||||
//
|
||||
// > The immutable nature of image objects also means that they are safe
|
||||
// to create and use from any thread.
|
||||
//
|
||||
// However, there are some versions of iOS which violated this. The
|
||||
// `UIImage` is supposably fully thread safe again starting with iOS 10.
|
||||
//
|
||||
// The `queue.sync` call below prevents the majority of the potential
|
||||
// crashes that could happen on the previous versions of iOS.
|
||||
//
|
||||
// See also https://github.com/AFNetworking/AFNetworking/issues/2572
|
||||
private let _queue = DispatchQueue(label: "com.github.kean.Nuke.DataDecoder")
|
||||
|
||||
internal func _decode(_ data: Data) -> Image? {
|
||||
return _queue.sync {
|
||||
#if os(macOS)
|
||||
return NSImage(data: data)
|
||||
#else
|
||||
#if os(iOS) || os(tvOS)
|
||||
let scale = UIScreen.main.scale
|
||||
#else
|
||||
let scale = WKInterfaceDevice.current().screenScale
|
||||
#endif
|
||||
return UIImage(data: data, scale: scale)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ImageDecoderRegistry
|
||||
|
||||
/// A register of image codecs (only decoding).
|
||||
public final class ImageDecoderRegistry {
|
||||
/// A shared registry.
|
||||
public static let shared = ImageDecoderRegistry()
|
||||
|
||||
private var matches = [(ImageDecodingContext) -> ImageDecoding?]()
|
||||
|
||||
/// Returns a decoder which matches the given context.
|
||||
public func decoder(for context: ImageDecodingContext) -> ImageDecoding {
|
||||
for match in matches {
|
||||
if let decoder = match(context) {
|
||||
return decoder
|
||||
}
|
||||
}
|
||||
return ImageDecoder() // Return default decoder if couldn't find a custom one.
|
||||
}
|
||||
|
||||
/// Registers a decoder to be used in a given decoding context. The closure
|
||||
/// is going to be executed before all other already registered closures.
|
||||
public func register(_ match: @escaping (ImageDecodingContext) -> ImageDecoding?) {
|
||||
matches.insert(match, at: 0)
|
||||
}
|
||||
|
||||
func clear() {
|
||||
matches = []
|
||||
}
|
||||
}
|
||||
|
||||
/// Image decoding context used when selecting which decoder to use.
|
||||
public struct ImageDecodingContext {
|
||||
public let request: ImageRequest
|
||||
internal let urlResponse: URLResponse?
|
||||
public let data: Data
|
||||
}
|
||||
|
||||
// MARK: - Image Formats
|
||||
|
||||
enum ImageFormat: Equatable {
|
||||
/// `isProgressive` is nil if we determined that it's a jpeg, but we don't
|
||||
/// know if it is progressive or baseline yet.
|
||||
case jpeg(isProgressive: Bool?)
|
||||
case png
|
||||
case gif
|
||||
|
||||
// Returns `nil` if not enough data.
|
||||
static func format(for data: Data) -> ImageFormat? {
|
||||
// JPEG magic numbers https://en.wikipedia.org/wiki/JPEG
|
||||
if _match(data, [0xFF, 0xD8, 0xFF]) {
|
||||
var index = 3 // start scanning right after magic numbers
|
||||
while index < (data.count - 1) {
|
||||
// A example of first few bytes of progressive jpeg image:
|
||||
// FF D8 FF E0 00 10 4A 46 49 46 00 01 01 00 00 48 00 ...
|
||||
//
|
||||
// 0xFF, 0xC0 - Start Of Frame (baseline DCT)
|
||||
// 0xFF, 0xC2 - Start Of Frame (progressive DCT)
|
||||
// https://en.wikipedia.org/wiki/JPEG
|
||||
if data[index] == 0xFF {
|
||||
if data[index+1] == 0xC2 { return .jpeg(isProgressive: true) } // progressive
|
||||
if data[index+1] == 0xC0 { return .jpeg(isProgressive: false) } // baseline
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
// It's a jpeg but we don't know if progressive or not yet.
|
||||
return .jpeg(isProgressive: nil)
|
||||
}
|
||||
|
||||
// GIF magic numbers https://en.wikipedia.org/wiki/GIF
|
||||
if _match(data, [0x47, 0x49, 0x46]) {
|
||||
return .gif
|
||||
}
|
||||
|
||||
// PNG Magic numbers https://en.wikipedia.org/wiki/Portable_Network_Graphics
|
||||
if _match(data, [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) {
|
||||
return .png
|
||||
}
|
||||
|
||||
// Either not enough data, or we just don't know this format yet.
|
||||
return nil
|
||||
}
|
||||
|
||||
var isProgressive: Bool? {
|
||||
if case let .jpeg(isProgressive) = self { return isProgressive }
|
||||
return false
|
||||
}
|
||||
|
||||
private static func _match(_ data: Data, _ numbers: [UInt8]) -> Bool {
|
||||
guard data.count >= numbers.count else { return false }
|
||||
return !zip(numbers.indices, numbers).contains { (index, number) in
|
||||
data[index] != number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Animated Images
|
||||
|
||||
private var _animatedImageDataAK = "Nuke.AnimatedImageData.AssociatedKey"
|
||||
|
||||
extension Image {
|
||||
// Animated image data. Only not `nil` when image data actually contains
|
||||
// an animated image.
|
||||
public var animatedImageData: Data? {
|
||||
get { return objc_getAssociatedObject(self, &_animatedImageDataAK) as? Data }
|
||||
set { objc_setAssociatedObject(self, &_animatedImageDataAK, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
|
||||
}
|
||||
}
|
||||
911
Pods/Nuke/Sources/ImagePipeline.swift
generated
Normal file
@@ -0,0 +1,911 @@
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) 2015-2019 Alexander Grebenyuk (github.com/kean).
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - ImageTask
|
||||
|
||||
/// A task performed by the `ImagePipeline`. The pipeline maintains a strong
|
||||
/// reference to the task until the request finishes or fails; you do not need
|
||||
/// to maintain a reference to the task unless it is useful to do so for your
|
||||
/// app’s internal bookkeeping purposes.
|
||||
public /* final */ class ImageTask: Hashable {
|
||||
/// An identifier uniquely identifies the task within a given pipeline. Only
|
||||
/// unique within this pipeline.
|
||||
public let taskId: Int
|
||||
|
||||
fileprivate weak var delegate: ImageTaskDelegate?
|
||||
|
||||
/// The request with which the task was created. The request might change
|
||||
/// during the exetucion of a task. When you update the priority of the task,
|
||||
/// the request's prir also gets updated.
|
||||
public private(set) var request: ImageRequest
|
||||
|
||||
/// The number of bytes that the task has received.
|
||||
public fileprivate(set) var completedUnitCount: Int64 = 0
|
||||
|
||||
/// A best-guess upper bound on the number of bytes the client expects to send.
|
||||
public fileprivate(set) var totalUnitCount: Int64 = 0
|
||||
|
||||
/// Returns a progress object for the task. The object is created lazily.
|
||||
public var progress: Progress {
|
||||
if _progress == nil { _progress = Progress() }
|
||||
return _progress!
|
||||
}
|
||||
fileprivate private(set) var _progress: Progress?
|
||||
|
||||
/// A completion handler to be called when task finishes or fails.
|
||||
public typealias Completion = (_ response: ImageResponse?, _ error: ImagePipeline.Error?) -> Void
|
||||
|
||||
/// A progress handler to be called periodically during the lifetime of a task.
|
||||
public typealias ProgressHandler = (_ response: ImageResponse?, _ completed: Int64, _ total: Int64) -> Void
|
||||
|
||||
// internal stuff associated with a task
|
||||
fileprivate var metrics: ImageTaskMetrics
|
||||
|
||||
fileprivate weak var session: ImageLoadingSession?
|
||||
|
||||
internal init(taskId: Int, request: ImageRequest) {
|
||||
self.taskId = taskId
|
||||
self.request = request
|
||||
self.metrics = ImageTaskMetrics(taskId: taskId, startDate: Date())
|
||||
}
|
||||
|
||||
// MARK: - Priority
|
||||
|
||||
/// Update s priority of the task even if the task is already running.
|
||||
public func setPriority(_ priority: ImageRequest.Priority) {
|
||||
request.priority = priority
|
||||
delegate?.imageTask(self, didUpdatePrioity: priority)
|
||||
}
|
||||
|
||||
// MARK: - Cancellation
|
||||
|
||||
fileprivate var isCancelled: Bool {
|
||||
return _isCancelled.value
|
||||
}
|
||||
|
||||
private var _isCancelled = Atomic(false)
|
||||
|
||||
/// Marks task as being cancelled.
|
||||
///
|
||||
/// The pipeline will immediately cancel any work associated with a task
|
||||
/// unless there is an equivalent outstanding task running (see
|
||||
/// `ImagePipeline.Configuration.isDeduplicationEnabled` for more info).
|
||||
public func cancel() {
|
||||
// Make sure that we ignore if `cancel` being called more than once.
|
||||
if _isCancelled.swap(to: true, ifEqual: false) {
|
||||
delegate?.imageTaskWasCancelled(self)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Hashable
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(ObjectIdentifier(self).hashValue)
|
||||
}
|
||||
|
||||
public static func == (lhs: ImageTask, rhs: ImageTask) -> Bool {
|
||||
return ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
|
||||
}
|
||||
}
|
||||
|
||||
protocol ImageTaskDelegate: class {
|
||||
func imageTaskWasCancelled(_ task: ImageTask)
|
||||
func imageTask(_ task: ImageTask, didUpdatePrioity: ImageRequest.Priority)
|
||||
}
|
||||
|
||||
// MARK: - ImageResponse
|
||||
|
||||
/// Represents an image response.
|
||||
public final class ImageResponse {
|
||||
public let image: Image
|
||||
public let urlResponse: URLResponse?
|
||||
// the response is only nil when new disk cache is enabled (it only stores
|
||||
// data for now, but this might change in the future).
|
||||
|
||||
public init(image: Image, urlResponse: URLResponse?) {
|
||||
self.image = image; self.urlResponse = urlResponse
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ImagePipeline
|
||||
|
||||
/// `ImagePipeline` will load and decode image data, process loaded images and
|
||||
/// store them in caches.
|
||||
///
|
||||
/// See [Nuke's README](https://github.com/kean/Nuke) for a detailed overview of
|
||||
/// the image pipeline and all of the related classes.
|
||||
///
|
||||
/// `ImagePipeline` is created with a configuration (`Configuration`).
|
||||
///
|
||||
/// `ImagePipeline` is thread-safe.
|
||||
public /* final */ class ImagePipeline: ImageTaskDelegate {
|
||||
public let configuration: Configuration
|
||||
|
||||
// This is a queue on which we access the sessions.
|
||||
private let queue = DispatchQueue(label: "com.github.kean.Nuke.ImagePipeline")
|
||||
|
||||
// Image loading sessions. One or more tasks can be handled by the same session.
|
||||
private var sessions = [AnyHashable: ImageLoadingSession]()
|
||||
|
||||
private var nextTaskId = Atomic<Int>(0)
|
||||
// Unlike `nextTaskId` doesn't need to be atomic because it's accessed only on a queue
|
||||
private var nextSessionId: Int = 0
|
||||
|
||||
private let rateLimiter: RateLimiter
|
||||
|
||||
/// Shared image pipeline.
|
||||
public static var shared = ImagePipeline()
|
||||
|
||||
/// The closure that gets called each time the task is completed (or cancelled).
|
||||
/// Guaranteed to be called on the main thread.
|
||||
public var didFinishCollectingMetrics: ((ImageTask, ImageTaskMetrics) -> Void)?
|
||||
|
||||
public struct Configuration {
|
||||
/// Image cache used by the pipeline.
|
||||
public var imageCache: ImageCaching?
|
||||
|
||||
/// Data loader used by the pipeline.
|
||||
public var dataLoader: DataLoading
|
||||
|
||||
/// Data loading queue. Default maximum concurrent task count is 6.
|
||||
public var dataLoadingQueue = OperationQueue()
|
||||
|
||||
/// Data cache used by the pipeline.
|
||||
public var dataCache: DataCaching?
|
||||
|
||||
/// Data caching queue. Default maximum concurrent task count is 2.
|
||||
public var dataCachingQueue = OperationQueue()
|
||||
|
||||
/// Default implementation uses shared `ImageDecoderRegistry` to create
|
||||
/// a decoder that matches the context.
|
||||
internal var imageDecoder: (ImageDecodingContext) -> ImageDecoding = {
|
||||
return ImageDecoderRegistry.shared.decoder(for: $0)
|
||||
}
|
||||
|
||||
/// Image decoding queue. Default maximum concurrent task count is 1.
|
||||
public var imageDecodingQueue = OperationQueue()
|
||||
|
||||
/// This is here just for backward compatibility with `Loader`.
|
||||
internal var imageProcessor: (Image, ImageRequest) -> AnyImageProcessor? = { $1.processor }
|
||||
|
||||
/// Image processing queue. Default maximum concurrent task count is 2.
|
||||
public var imageProcessingQueue = OperationQueue()
|
||||
|
||||
/// `true` by default. If `true` the pipeline will combine the requests
|
||||
/// with the same `loadKey` into a single request. The request only gets
|
||||
/// cancelled when all the registered requests are.
|
||||
public var isDeduplicationEnabled = true
|
||||
|
||||
/// `true` by default. It `true` the pipeline will rate limits the requests
|
||||
/// to prevent trashing of the underlying systems (e.g. `URLSession`).
|
||||
/// The rate limiter only comes into play when the requests are started
|
||||
/// and cancelled at a high rate (e.g. scrolling through a collection view).
|
||||
public var isRateLimiterEnabled = true
|
||||
|
||||
/// `false` by default. If `true` the pipeline will try to produce a new
|
||||
/// image each time it receives a new portion of data from data loader.
|
||||
/// The decoder used by the image loading session determines whether
|
||||
/// to produce a partial image or not.
|
||||
public var isProgressiveDecodingEnabled = false
|
||||
|
||||
/// If the data task is terminated (either because of a failure or a
|
||||
/// cancellation) and the image was partially loaded, the next load will
|
||||
/// resume where it was left off. Supports both validators (`ETag`,
|
||||
/// `Last-Modified`). The resumable downloads are enabled by default.
|
||||
public var isResumableDataEnabled = true
|
||||
|
||||
/// If `true` pipeline will detects GIFs and set `animatedImageData`
|
||||
/// (`UIImage` property). It will also disable processing of such images,
|
||||
/// and alter the way cache cost is calculated. However, this will not
|
||||
/// enable actual animated image rendering. To do that take a look at
|
||||
/// satellite projects (FLAnimatedImage and Gifu plugins for Nuke).
|
||||
/// `false` by default (to preserve resources).
|
||||
public static var isAnimatedImageDataEnabled = false
|
||||
|
||||
/// Creates default configuration.
|
||||
/// - parameter dataLoader: `DataLoader()` by default.
|
||||
/// - parameter imageCache: `Cache.shared` by default.
|
||||
public init(dataLoader: DataLoading = DataLoader(), imageCache: ImageCaching? = ImageCache.shared) {
|
||||
self.dataLoader = dataLoader
|
||||
self.imageCache = imageCache
|
||||
|
||||
self.dataLoadingQueue.maxConcurrentOperationCount = 6
|
||||
self.dataCachingQueue.maxConcurrentOperationCount = 2
|
||||
self.imageDecodingQueue.maxConcurrentOperationCount = 1
|
||||
self.imageProcessingQueue.maxConcurrentOperationCount = 2
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes `ImagePipeline` instance with the given configuration.
|
||||
/// - parameter configuration: `Configuration()` by default.
|
||||
public init(configuration: Configuration = Configuration()) {
|
||||
self.configuration = configuration
|
||||
self.rateLimiter = RateLimiter(queue: queue)
|
||||
}
|
||||
|
||||
public convenience init(_ configure: (inout ImagePipeline.Configuration) -> Void) {
|
||||
var configuration = ImagePipeline.Configuration()
|
||||
configure(&configuration)
|
||||
self.init(configuration: configuration)
|
||||
}
|
||||
|
||||
// MARK: Loading Images
|
||||
|
||||
/// Loads an image with the given url.
|
||||
@discardableResult
|
||||
public func loadImage(with url: URL, progress: ImageTask.ProgressHandler? = nil, completion: ImageTask.Completion? = nil) -> ImageTask {
|
||||
return loadImage(with: ImageRequest(url: url), progress: progress, completion: completion)
|
||||
}
|
||||
|
||||
/// Loads an image for the given request using image loading pipeline.
|
||||
@discardableResult
|
||||
public func loadImage(with request: ImageRequest, progress: ImageTask.ProgressHandler? = nil, completion: ImageTask.Completion? = nil) -> ImageTask {
|
||||
let task = ImageTask(taskId: getNextTaskId(), request: request)
|
||||
task.delegate = self
|
||||
queue.async {
|
||||
// Fast memory cache lookup. We do this asynchronously because we
|
||||
// expect users to check memory cache synchronously if needed.
|
||||
if task.request.memoryCacheOptions.isReadAllowed,
|
||||
let response = self.configuration.imageCache?.cachedResponse(for: task.request) {
|
||||
task.metrics.isMemoryCacheHit = true
|
||||
self._didCompleteTask(task, response: response, error: nil, completion: completion)
|
||||
return
|
||||
}
|
||||
// Memory cache lookup failed -> start loading.
|
||||
self._startLoadingImage(
|
||||
for: task,
|
||||
handlers: ImageLoadingSession.Handlers(progress: progress, completion: completion)
|
||||
)
|
||||
}
|
||||
return task
|
||||
}
|
||||
|
||||
private func getNextTaskId() -> Int {
|
||||
return nextTaskId.increment()
|
||||
}
|
||||
|
||||
private func getNextSessionId() -> Int {
|
||||
nextSessionId += 1
|
||||
return nextSessionId
|
||||
}
|
||||
|
||||
private func _startLoadingImage(for task: ImageTask, handlers: ImageLoadingSession.Handlers) {
|
||||
// Create a new image loading session or register with an existing one.
|
||||
let session = _createSession(with: task.request)
|
||||
task.session = session
|
||||
|
||||
task.metrics.session = session.metrics
|
||||
task.metrics.wasSubscibedToExistingSession = !session.tasks.isEmpty
|
||||
|
||||
// Register handler with a session.
|
||||
session.tasks[task] = handlers
|
||||
session.updatePriority()
|
||||
|
||||
// Already loaded and decoded the final image and started processing
|
||||
// for previously registered tasks (if any).
|
||||
if let image = session.decodedFinalImage {
|
||||
_session(session, processImage: image, for: task)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: ImageTaskDelegate
|
||||
|
||||
func imageTaskWasCancelled(_ task: ImageTask) {
|
||||
queue.async {
|
||||
self._didCancelTask(task)
|
||||
}
|
||||
}
|
||||
|
||||
func imageTask(_ task: ImageTask, didUpdatePrioity: ImageRequest.Priority) {
|
||||
queue.async {
|
||||
guard let session = task.session else { return }
|
||||
session.updatePriority()
|
||||
session.processingSessions[task]?.updatePriority()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: ImageLoadingSession (Managing)
|
||||
|
||||
private func _createSession(with request: ImageRequest) -> ImageLoadingSession {
|
||||
// Check if session for the given key already exists.
|
||||
//
|
||||
// This part is more clever than I would like. The reason why we need a
|
||||
// key even when deduplication is disabled is to have a way to retain
|
||||
// a session by storing it in `sessions` dictionary.
|
||||
let key: AnyHashable = configuration.isDeduplicationEnabled ? ImageRequest.LoadKey(request: request) : UUID()
|
||||
if let session = sessions[key] {
|
||||
return session
|
||||
}
|
||||
let session = ImageLoadingSession(sessionId: getNextSessionId(), request: request, key: key)
|
||||
sessions[key] = session
|
||||
_loadImage(for: session) // Start the pipeline
|
||||
return session
|
||||
}
|
||||
|
||||
private func _cancelSession(for task: ImageTask) {
|
||||
guard let session = task.session else { return }
|
||||
|
||||
session.tasks[task] = nil
|
||||
|
||||
// When all registered tasks are cancelled, the session is deallocated
|
||||
// and the underlying operation is cancelled automatically.
|
||||
let processingSession = session.processingSessions.removeValue(forKey: task)
|
||||
processingSession?.tasks.remove(task)
|
||||
|
||||
// Cancel the session when there are no remaining tasks.
|
||||
if session.tasks.isEmpty {
|
||||
_tryToSaveResumableData(for: session)
|
||||
session.cts.cancel()
|
||||
session.metrics.wasCancelled = true
|
||||
_didFinishSession(session)
|
||||
} else {
|
||||
// We're not cancelling the task session yet because there are
|
||||
// still tasks registered to it, but we need to update the priority.
|
||||
session.updatePriority()
|
||||
processingSession?.updatePriority()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Pipeline (Loading Data)
|
||||
|
||||
private func _loadImage(for session: ImageLoadingSession) {
|
||||
// Use rate limiter to prevent trashing of the underlying systems
|
||||
if configuration.isRateLimiterEnabled {
|
||||
// Rate limiter is synchronized on pipeline's queue. Delayed work is
|
||||
// executed asynchronously also on this same queue.
|
||||
rateLimiter.execute(token: session.cts.token) { [weak self, weak session] in
|
||||
guard let session = session else { return }
|
||||
self?._checkDiskCache(for: session)
|
||||
}
|
||||
} else { // Start loading immediately.
|
||||
_checkDiskCache(for: session)
|
||||
}
|
||||
}
|
||||
|
||||
private func _checkDiskCache(for session: ImageLoadingSession) {
|
||||
guard let cache = configuration.dataCache, let key = session.request.urlString else {
|
||||
_loadData(for: session) // Skip disk cache lookup, load data
|
||||
return
|
||||
}
|
||||
|
||||
session.metrics.checkDiskCacheStartDate = Date()
|
||||
|
||||
let operation = BlockOperation { [weak self, weak session] in
|
||||
guard let session = session else { return }
|
||||
let data = cache.cachedData(for: key)
|
||||
session.metrics.checkDiskCacheEndDate = Date()
|
||||
self?.queue.async {
|
||||
if let data = data {
|
||||
self?._decodeFinalImage(for: session, data: data)
|
||||
} else {
|
||||
self?._loadData(for: session)
|
||||
}
|
||||
}
|
||||
}
|
||||
configuration.dataCachingQueue.enqueue(operation, for: session)
|
||||
}
|
||||
|
||||
private func _loadData(for session: ImageLoadingSession) {
|
||||
guard !session.token.isCancelling else { return } // Preflight check
|
||||
|
||||
// Wrap data request in an operation to limit maximum number of
|
||||
// concurrent data tasks.
|
||||
let operation = Operation(starter: { [weak self, weak session] finish in
|
||||
guard let session = session else { finish(); return }
|
||||
self?.queue.async {
|
||||
self?._actuallyLoadData(for: session, finish: finish)
|
||||
}
|
||||
})
|
||||
configuration.dataLoadingQueue.enqueue(operation, for: session)
|
||||
}
|
||||
|
||||
// This methods gets called inside data loading operation (Operation).
|
||||
private func _actuallyLoadData(for session: ImageLoadingSession, finish: @escaping () -> Void) {
|
||||
session.metrics.loadDataStartDate = Date()
|
||||
|
||||
var urlRequest = session.request.urlRequest
|
||||
|
||||
// Read and remove resumable data from cache (we're going to insert it
|
||||
// back in the cache if the request fails to complete again).
|
||||
if configuration.isResumableDataEnabled,
|
||||
let resumableData = ResumableData.removeResumableData(for: urlRequest) {
|
||||
// Update headers to add "Range" and "If-Range" headers
|
||||
resumableData.resume(request: &urlRequest)
|
||||
// Save resumable data so that we could use it later (we need to
|
||||
// verify that server returns "206 Partial Content" before using it.
|
||||
session.resumableData = resumableData
|
||||
|
||||
// Collect metrics
|
||||
session.metrics.wasResumed = true
|
||||
session.metrics.resumedDataCount = resumableData.data.count
|
||||
}
|
||||
|
||||
let task = configuration.dataLoader.loadData(
|
||||
with: urlRequest,
|
||||
didReceiveData: { [weak self, weak session] (data, response) in
|
||||
self?.queue.async {
|
||||
guard let session = session else { return }
|
||||
self?._session(session, didReceiveData: data, response: response)
|
||||
}
|
||||
},
|
||||
completion: { [weak self, weak session] (error) in
|
||||
finish() // Important! Mark Operation as finished.
|
||||
self?.queue.async {
|
||||
guard let session = session else { return }
|
||||
self?._session(session, didFinishLoadingDataWithError: error)
|
||||
}
|
||||
})
|
||||
session.token.register {
|
||||
task.cancel()
|
||||
finish() // Make sure we always finish the operation.
|
||||
}
|
||||
}
|
||||
|
||||
private func _session(_ session: ImageLoadingSession, didReceiveData chunk: Data, response: URLResponse) {
|
||||
// Check if this is the first response.
|
||||
if session.urlResponse == nil {
|
||||
// See if the server confirmed that we can use the resumable data.
|
||||
if let resumableData = session.resumableData {
|
||||
if ResumableData.isResumedResponse(response) {
|
||||
session.data = resumableData.data
|
||||
session.resumedDataCount = Int64(resumableData.data.count)
|
||||
session.metrics.serverConfirmedResume = true
|
||||
}
|
||||
session.resumableData = nil // Get rid of resumable data
|
||||
}
|
||||
}
|
||||
|
||||
// Append data and save response
|
||||
session.data.append(chunk)
|
||||
session.urlResponse = response
|
||||
|
||||
// Collect metrics
|
||||
session.metrics.downloadedDataCount = ((session.metrics.downloadedDataCount ?? 0) + chunk.count)
|
||||
|
||||
// Update tasks' progress and call progress closures if any
|
||||
let (completed, total) = (Int64(session.data.count), response.expectedContentLength + session.resumedDataCount)
|
||||
let tasks = session.tasks
|
||||
DispatchQueue.main.async {
|
||||
for (task, handlers) in tasks where !task.isCancelled {
|
||||
(task.completedUnitCount, task.totalUnitCount) = (completed, total)
|
||||
handlers.progress?(nil, completed, total)
|
||||
task._progress?.completedUnitCount = completed
|
||||
task._progress?.totalUnitCount = total
|
||||
}
|
||||
}
|
||||
|
||||
// Check if progressive decoding is enabled (disabled by default)
|
||||
if configuration.isProgressiveDecodingEnabled {
|
||||
// Check if we haven't loaded an entire image yet. We give decoder
|
||||
// an opportunity to decide whether to decode this chunk or not.
|
||||
// In case `expectedContentLength` is undetermined (e.g. 0) we
|
||||
// don't allow progressive decoding.
|
||||
guard session.data.count < response.expectedContentLength else { return }
|
||||
|
||||
_setNeedsDecodePartialImage(for: session)
|
||||
}
|
||||
}
|
||||
|
||||
private func _session(_ session: ImageLoadingSession, didFinishLoadingDataWithError error: Swift.Error?) {
|
||||
session.metrics.loadDataEndDate = Date()
|
||||
|
||||
if let error = error {
|
||||
_tryToSaveResumableData(for: session)
|
||||
_session(session, didFailWithError: .dataLoadingFailed(error))
|
||||
return
|
||||
}
|
||||
|
||||
let data = session.data
|
||||
session.data.removeAll() // We no longer need the data stored in session.
|
||||
|
||||
_decodeFinalImage(for: session, data: data)
|
||||
}
|
||||
|
||||
// MARK: Pipeline (Decoding)
|
||||
|
||||
private func _setNeedsDecodePartialImage(for session: ImageLoadingSession) {
|
||||
guard session.decodingOperation == nil else {
|
||||
return // Already enqueued an operation.
|
||||
}
|
||||
let operation = BlockOperation { [weak self, weak session] in
|
||||
guard let session = session else { return }
|
||||
self?._actuallyDecodePartialImage(for: session)
|
||||
}
|
||||
_enqueueDecodingOperation(operation, for: session)
|
||||
}
|
||||
|
||||
private func _actuallyDecodePartialImage(for session: ImageLoadingSession) {
|
||||
// As soon as we get a chance to execute, grab the latest available
|
||||
// data, create a decoder (if necessary) and decode the data.
|
||||
let (data, decoder): (Data, ImageDecoding?) = queue.sync {
|
||||
let data = session.data
|
||||
let decoder = _decoder(for: session, data: data)
|
||||
return (data, decoder)
|
||||
}
|
||||
|
||||
// Produce partial image
|
||||
if let image = decoder?.decode(data: data, isFinal: false) {
|
||||
let scanNumber: Int? = (decoder as? ImageDecoder)?.numberOfScans
|
||||
queue.async {
|
||||
let container = ImageContainer(image: image, isFinal: false, scanNumber: scanNumber)
|
||||
for task in session.tasks.keys {
|
||||
self._session(session, processImage: container, for: task)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func _decodeFinalImage(for session: ImageLoadingSession, data: Data) {
|
||||
// Basic sanity checks, should never happen in practice.
|
||||
guard !data.isEmpty, let decoder = _decoder(for: session, data: data) else {
|
||||
_session(session, didFailWithError: .decodingFailed)
|
||||
return
|
||||
}
|
||||
|
||||
let metrics = session.metrics
|
||||
let operation = BlockOperation { [weak self, weak session] in
|
||||
guard let session = session else { return }
|
||||
metrics.decodeStartDate = Date()
|
||||
let image = autoreleasepool {
|
||||
decoder.decode(data: data, isFinal: true) // Produce final image
|
||||
}
|
||||
metrics.decodeEndDate = Date()
|
||||
self?.queue.async {
|
||||
let container = image.map {
|
||||
ImageContainer(image: $0, isFinal: true, scanNumber: nil)
|
||||
}
|
||||
self?._session(session, didDecodeFinalImage: container, data: data)
|
||||
}
|
||||
}
|
||||
_enqueueDecodingOperation(operation, for: session)
|
||||
}
|
||||
|
||||
private func _enqueueDecodingOperation(_ operation: Foundation.Operation, for session: ImageLoadingSession) {
|
||||
configuration.imageDecodingQueue.enqueue(operation, for: session)
|
||||
session.decodingOperation?.cancel()
|
||||
session.decodingOperation = operation
|
||||
}
|
||||
|
||||
// Lazily creates a decoder if necessary.
|
||||
private func _decoder(for session: ImageLoadingSession, data: Data) -> ImageDecoding? {
|
||||
guard !session.isDecodingDisabled else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Return the existing processor in case it has already been created.
|
||||
if let decoder = session.decoder {
|
||||
return decoder
|
||||
}
|
||||
|
||||
// Basic sanity checks.
|
||||
guard !data.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let context = ImageDecodingContext(request: session.request, urlResponse: session.urlResponse, data: data)
|
||||
let decoder = configuration.imageDecoder(context)
|
||||
session.decoder = decoder
|
||||
return decoder
|
||||
}
|
||||
|
||||
private func _tryToSaveResumableData(for session: ImageLoadingSession) {
|
||||
// Try to save resumable data in case the task was cancelled
|
||||
// (`URLError.cancelled`) or failed to complete with other error.
|
||||
if configuration.isResumableDataEnabled,
|
||||
let response = session.urlResponse, !session.data.isEmpty,
|
||||
let resumableData = ResumableData(response: response, data: session.data) {
|
||||
ResumableData.storeResumableData(resumableData, for: session.request.urlRequest)
|
||||
}
|
||||
}
|
||||
|
||||
private func _session(_ session: ImageLoadingSession, didDecodeFinalImage image: ImageContainer?, data: Data) {
|
||||
session.decoder = nil // Decoding session completed, no longer need decoder.
|
||||
session.decodedFinalImage = image
|
||||
|
||||
guard let image = image else {
|
||||
_session(session, didFailWithError: .decodingFailed)
|
||||
return
|
||||
}
|
||||
|
||||
// Store data in data cache (in case it's enabled))
|
||||
if !data.isEmpty, let dataCache = configuration.dataCache, let key = session.request.urlString {
|
||||
dataCache.storeData(data, for: key)
|
||||
}
|
||||
|
||||
for task in session.tasks.keys {
|
||||
_session(session, processImage: image, for: task)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Pipeline (Processing)
|
||||
|
||||
/// Processes the input image for each of the given tasks. The image is processed
|
||||
/// only once for the equivalent processors.
|
||||
/// - parameter completion: Will get called synchronously if processing is not
|
||||
/// required. If it is will get called on `self.queue` when processing is finished.
|
||||
private func _session(_ session: ImageLoadingSession, processImage image: ImageContainer, for task: ImageTask) {
|
||||
let isFinal = image.isFinal
|
||||
guard let processor = _processor(for: image.image, request: task.request) else {
|
||||
_session(session, didProcessImage: image.image, isFinal: isFinal, metrics: TaskMetrics(), for: task)
|
||||
return // No processing needed.
|
||||
}
|
||||
|
||||
if !image.isFinal && session.processingSessions[task] != nil {
|
||||
return // Back pressure - we'are already busy processing another partial image
|
||||
}
|
||||
|
||||
// Find existing session or create a new one.
|
||||
let processingSession = _processingSession(for: image, processor: processor, session: session, task: task)
|
||||
|
||||
// Register task with a processing session.
|
||||
processingSession.tasks.insert(task)
|
||||
session.processingSessions[task] = processingSession
|
||||
processingSession.updatePriority()
|
||||
}
|
||||
|
||||
private func _processingSession(for image: ImageContainer, processor: AnyImageProcessor, session: ImageLoadingSession, task: ImageTask) -> ImageProcessingSession {
|
||||
func findExistingSession() -> ImageProcessingSession? {
|
||||
return session.processingSessions.values.first {
|
||||
$0.processor == processor && $0.image.image === image.image
|
||||
}
|
||||
}
|
||||
|
||||
if let processingSession = findExistingSession() {
|
||||
return processingSession
|
||||
}
|
||||
|
||||
let processingSession = ImageProcessingSession(processor: processor, image: image)
|
||||
|
||||
let isFinal = image.isFinal
|
||||
let operation = BlockOperation { [weak self, weak session, weak processingSession] in
|
||||
var metrics = TaskMetrics.started()
|
||||
let output: Image? = autoreleasepool {
|
||||
processor.process(image: image, request: task.request)
|
||||
}
|
||||
metrics.end()
|
||||
|
||||
self?.queue.async {
|
||||
guard let session = session else { return }
|
||||
for task in (processingSession?.tasks ?? []) {
|
||||
if session.processingSessions[task] === processingSession {
|
||||
session.processingSessions[task] = nil
|
||||
}
|
||||
self?._session(session, didProcessImage: output, isFinal: isFinal, metrics: metrics, for: task)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
operation.queuePriority = task.request.priority.queuePriority
|
||||
session.priority.observe { [weak operation] in
|
||||
operation?.queuePriority = $0.queuePriority
|
||||
}
|
||||
configuration.imageProcessingQueue.addOperation(operation)
|
||||
processingSession.operation = operation
|
||||
|
||||
return processingSession
|
||||
}
|
||||
|
||||
private func _processor(for image: Image, request: ImageRequest) -> AnyImageProcessor? {
|
||||
if Configuration.isAnimatedImageDataEnabled && image.animatedImageData != nil {
|
||||
return nil // Don't process animated images.
|
||||
}
|
||||
return configuration.imageProcessor(image, request)
|
||||
}
|
||||
|
||||
private func _session(_ session: ImageLoadingSession, didProcessImage image: Image?, isFinal: Bool, metrics: TaskMetrics, for task: ImageTask) {
|
||||
if isFinal {
|
||||
task.metrics.processStartDate = metrics.startDate
|
||||
task.metrics.processEndDate = metrics.endDate
|
||||
let error: Error? = (image == nil ? .processingFailed : nil)
|
||||
_session(session, didCompleteTask: task, image: image, error: error)
|
||||
} else {
|
||||
guard let image = image else { return }
|
||||
_session(session, didProducePartialImage: image, for: task)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: ImageLoadingSession (Callbacks)
|
||||
|
||||
private func _session(_ session: ImageLoadingSession, didProducePartialImage image: Image, for task: ImageTask) {
|
||||
// Check if we haven't completed the session yet by producing a final image
|
||||
// or cancelling the task.
|
||||
guard sessions[session.key] === session else { return }
|
||||
|
||||
let response = ImageResponse(image: image, urlResponse: session.urlResponse)
|
||||
if let handler = session.tasks[task], let progress = handler.progress {
|
||||
DispatchQueue.main.async {
|
||||
guard !task.isCancelled else { return }
|
||||
progress(response, task.completedUnitCount, task.totalUnitCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func _session(_ session: ImageLoadingSession, didCompleteTask task: ImageTask, image: Image?, error: Error?) {
|
||||
let response = image.map {
|
||||
ImageResponse(image: $0, urlResponse: session.urlResponse)
|
||||
}
|
||||
// Store response in memory cache if allowed.
|
||||
if let response = response, task.request.memoryCacheOptions.isWriteAllowed {
|
||||
configuration.imageCache?.storeResponse(response, for: task.request)
|
||||
}
|
||||
if let handlers = session.tasks.removeValue(forKey: task) {
|
||||
_didCompleteTask(task, response: response, error: error, completion: handlers.completion)
|
||||
}
|
||||
if session.tasks.isEmpty {
|
||||
_didFinishSession(session)
|
||||
}
|
||||
}
|
||||
|
||||
private func _session(_ session: ImageLoadingSession, didFailWithError error: Error) {
|
||||
for task in session.tasks.keys {
|
||||
_session(session, didCompleteTask: task, image: nil, error: error)
|
||||
}
|
||||
}
|
||||
|
||||
private func _didFinishSession(_ session: ImageLoadingSession) {
|
||||
// Check if session is still registered.
|
||||
guard sessions[session.key] === session else { return }
|
||||
session.metrics.endDate = Date()
|
||||
sessions[session.key] = nil
|
||||
}
|
||||
|
||||
// Cancel the session in case all handlers were removed.
|
||||
private func _didCancelTask(_ task: ImageTask) {
|
||||
task.metrics.wasCancelled = true
|
||||
task.metrics.endDate = Date()
|
||||
|
||||
_cancelSession(for: task)
|
||||
|
||||
guard let didCollectMetrics = didFinishCollectingMetrics else { return }
|
||||
DispatchQueue.main.async {
|
||||
didCollectMetrics(task, task.metrics)
|
||||
}
|
||||
}
|
||||
|
||||
private func _didCompleteTask(_ task: ImageTask, response: ImageResponse?, error: Error?, completion: ImageTask.Completion?) {
|
||||
task.metrics.endDate = Date()
|
||||
DispatchQueue.main.async {
|
||||
guard !task.isCancelled else { return }
|
||||
completion?(response, error)
|
||||
self.didFinishCollectingMetrics?(task, task.metrics)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Errors
|
||||
|
||||
/// Represents all possible image pipeline errors.
|
||||
public enum Error: Swift.Error, CustomDebugStringConvertible {
|
||||
/// Data loader failed to load image data with a wrapped error.
|
||||
case dataLoadingFailed(Swift.Error)
|
||||
/// Decoder failed to produce a final image.
|
||||
case decodingFailed
|
||||
/// Processor failed to produce a final image.
|
||||
case processingFailed
|
||||
|
||||
public var debugDescription: String {
|
||||
switch self {
|
||||
case let .dataLoadingFailed(error): return "Failed to load image data: \(error)"
|
||||
case .decodingFailed: return "Failed to create an image from the image data"
|
||||
case .processingFailed: return "Failed to process the image"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ImageLoadingSession
|
||||
|
||||
/// A image loading session. During a lifetime of a session handlers can
|
||||
/// subscribe to and unsubscribe from it.
|
||||
private final class ImageLoadingSession {
|
||||
let sessionId: Int
|
||||
|
||||
/// The original request with which the session was created.
|
||||
let request: ImageRequest
|
||||
let key: AnyHashable // loading key
|
||||
let cts = CancellationTokenSource()
|
||||
var token: CancellationToken { return cts.token }
|
||||
|
||||
// Registered image tasks.
|
||||
var tasks = [ImageTask: Handlers]()
|
||||
|
||||
struct Handlers {
|
||||
let progress: ImageTask.ProgressHandler?
|
||||
let completion: ImageTask.Completion?
|
||||
}
|
||||
|
||||
// Data loading session.
|
||||
var urlResponse: URLResponse?
|
||||
var resumableData: ResumableData?
|
||||
var resumedDataCount: Int64 = 0
|
||||
lazy var data = Data()
|
||||
|
||||
// Decoding session.
|
||||
var decoder: ImageDecoding?
|
||||
var decodedFinalImage: ImageContainer? // Decoding result
|
||||
weak var decodingOperation: Foundation.Operation?
|
||||
|
||||
// Processing sessions.
|
||||
var processingSessions = [ImageTask: ImageProcessingSession]()
|
||||
|
||||
// Metrics that we collect during the lifetime of a session.
|
||||
let metrics: ImageTaskMetrics.SessionMetrics
|
||||
|
||||
let priority: Property<ImageRequest.Priority>
|
||||
|
||||
deinit {
|
||||
decodingOperation?.cancel()
|
||||
}
|
||||
|
||||
init(sessionId: Int, request: ImageRequest, key: AnyHashable) {
|
||||
self.sessionId = sessionId
|
||||
self.request = request
|
||||
self.key = key
|
||||
self.metrics = ImageTaskMetrics.SessionMetrics(sessionId: sessionId)
|
||||
self.priority = Property(value: request.priority)
|
||||
}
|
||||
|
||||
func updatePriority() {
|
||||
priority.update(with: tasks.keys)
|
||||
}
|
||||
|
||||
var isDecodingDisabled: Bool {
|
||||
return !tasks.keys.contains {
|
||||
!$0.request.isDecodingDisabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class ImageProcessingSession {
|
||||
let processor: AnyImageProcessor
|
||||
let image: ImageContainer
|
||||
var tasks = Set<ImageTask>()
|
||||
weak var operation: Foundation.Operation?
|
||||
|
||||
let priority = Property<ImageRequest.Priority>(value: .normal)
|
||||
|
||||
deinit {
|
||||
operation?.cancel()
|
||||
}
|
||||
|
||||
init(processor: AnyImageProcessor, image: ImageContainer) {
|
||||
self.processor = processor; self.image = image
|
||||
}
|
||||
|
||||
// Update priority for processing operations (those are per image task,
|
||||
// not per image session).
|
||||
func updatePriority() {
|
||||
priority.update(with: tasks)
|
||||
}
|
||||
}
|
||||
|
||||
struct ImageContainer {
|
||||
let image: Image
|
||||
let isFinal: Bool
|
||||
let scanNumber: Int?
|
||||
}
|
||||
|
||||
// MARK: - Extensions
|
||||
|
||||
private extension Property where T == ImageRequest.Priority {
|
||||
func update<Tasks: Sequence>(with tasks: Tasks) where Tasks.Element == ImageTask {
|
||||
if let newPriority = tasks.map({ $0.request.priority }).max(), self.value != newPriority {
|
||||
self.value = newPriority
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension Foundation.OperationQueue {
|
||||
func enqueue(_ operation: Foundation.Operation, for session: ImageLoadingSession) {
|
||||
operation.queuePriority = session.priority.value.queuePriority
|
||||
session.priority.observe { [weak operation] in
|
||||
operation?.queuePriority = $0.queuePriority
|
||||
}
|
||||
session.token.register { [weak operation] in
|
||||
operation?.cancel()
|
||||
}
|
||||
addOperation(operation)
|
||||
}
|
||||
}
|
||||
172
Pods/Nuke/Sources/ImagePreheater.swift
generated
Normal file
@@ -0,0 +1,172 @@
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) 2015-2019 Alexander Grebenyuk (github.com/kean).
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Prefetches and caches image in order to eliminate delays when you request
|
||||
/// individual images later.
|
||||
///
|
||||
/// To start preheating call `startPreheating(with:)` method. When you
|
||||
/// need an individual image just start loading an image using `Loading` object.
|
||||
/// When preheating is no longer necessary call `stopPreheating(with:)` method.
|
||||
///
|
||||
/// All `Preheater` methods are thread-safe.
|
||||
public final class ImagePreheater {
|
||||
private let pipeline: ImagePipeline
|
||||
private let queue = DispatchQueue(label: "com.github.kean.Nuke.Preheater")
|
||||
private let preheatQueue = OperationQueue()
|
||||
private var tasks = [PreheatKey: Task]()
|
||||
private let destination: Destination
|
||||
|
||||
/// Prefetching destination.
|
||||
public enum Destination {
|
||||
/// Prefetches the image and stores it both in memory and disk caches
|
||||
/// (in case they are enabled, naturally, there is no reason to prefetch
|
||||
/// unless they are).
|
||||
case memoryCache
|
||||
|
||||
/// Prefetches image data and stores in disk cache. Will no decode
|
||||
/// the image data and will therefore useless less CPU.
|
||||
case diskCache
|
||||
}
|
||||
|
||||
/// Initializes the `Preheater` instance.
|
||||
/// - parameter manager: `Loader.shared` by default.
|
||||
/// - parameter `maxConcurrentRequestCount`: 2 by default.
|
||||
/// - parameter destination: `.memoryCache` by default.
|
||||
public init(pipeline: ImagePipeline = ImagePipeline.shared, destination: Destination = .memoryCache, maxConcurrentRequestCount: Int = 2) {
|
||||
self.pipeline = pipeline
|
||||
self.destination = destination
|
||||
self.preheatQueue.maxConcurrentOperationCount = maxConcurrentRequestCount
|
||||
}
|
||||
|
||||
/// Starte preheating images for the given urls.
|
||||
/// - note: See `func startPreheating(with requests: [ImageRequest])` for more info
|
||||
public func startPreheating(with urls: [URL]) {
|
||||
startPreheating(with: _requests(for: urls))
|
||||
}
|
||||
|
||||
/// Starts preheating images for the given requests.
|
||||
///
|
||||
/// When you call this method, `Preheater` starts to load and cache images
|
||||
/// for the given requests. At any time afterward, you can create tasks
|
||||
/// for individual images with equivalent requests.
|
||||
public func startPreheating(with requests: [ImageRequest]) {
|
||||
queue.async {
|
||||
for request in requests {
|
||||
self._startPreheating(with: self._updatedRequest(request))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func _startPreheating(with request: ImageRequest) {
|
||||
let key = PreheatKey(request: request)
|
||||
|
||||
// Check if we we've already started preheating.
|
||||
guard tasks[key] == nil else { return }
|
||||
|
||||
// Check if the image is already in memory cache.
|
||||
guard pipeline.configuration.imageCache?.cachedResponse(for: request) == nil else {
|
||||
return // already in memory cache
|
||||
}
|
||||
|
||||
let task = Task(request: request, key: key)
|
||||
let token = task.cts.token
|
||||
|
||||
let operation = Operation(starter: { [weak self] finish in
|
||||
let task = self?.pipeline.loadImage(with: request) { [weak self] _, _ in
|
||||
self?._remove(task)
|
||||
finish()
|
||||
}
|
||||
token.register {
|
||||
task?.cancel()
|
||||
finish()
|
||||
}
|
||||
})
|
||||
preheatQueue.addOperation(operation)
|
||||
token.register { [weak operation] in operation?.cancel() }
|
||||
|
||||
tasks[key] = task
|
||||
}
|
||||
|
||||
private func _remove(_ task: Task) {
|
||||
queue.async {
|
||||
guard self.tasks[task.key] === task else { return }
|
||||
self.tasks[task.key] = nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Stops preheating images for the given urls.
|
||||
public func stopPreheating(with urls: [URL]) {
|
||||
stopPreheating(with: _requests(for: urls))
|
||||
}
|
||||
|
||||
/// Stops preheating images for the given requests and cancels outstanding
|
||||
/// requests.
|
||||
///
|
||||
/// - parameter destination: `.memoryCache` by default.
|
||||
public func stopPreheating(with requests: [ImageRequest]) {
|
||||
queue.async {
|
||||
for request in requests {
|
||||
self._stopPreheating(with: self._updatedRequest(request))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func _stopPreheating(with request: ImageRequest) {
|
||||
if let task = tasks[PreheatKey(request: request)] {
|
||||
tasks[task.key] = nil
|
||||
task.cts.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
/// Stops all preheating tasks.
|
||||
public func stopPreheating() {
|
||||
queue.async {
|
||||
self.tasks.forEach { $0.1.cts.cancel() }
|
||||
self.tasks.removeAll()
|
||||
}
|
||||
}
|
||||
|
||||
private func _requests(for urls: [URL]) -> [ImageRequest] {
|
||||
return urls.map {
|
||||
var request = ImageRequest(url: $0)
|
||||
request.priority = .low
|
||||
return request
|
||||
}
|
||||
}
|
||||
|
||||
private func _updatedRequest(_ request: ImageRequest) -> ImageRequest {
|
||||
guard destination == .diskCache else {
|
||||
return request // Avoid creating a new copy
|
||||
}
|
||||
|
||||
var request = request
|
||||
// What we do under the hood is we disable decoding for the requests
|
||||
// that are meant to not be stored in memory cache.
|
||||
request.isDecodingDisabled = (destination == .diskCache)
|
||||
return request
|
||||
}
|
||||
|
||||
private final class Task {
|
||||
let key: PreheatKey
|
||||
let request: ImageRequest
|
||||
let cts = CancellationTokenSource()
|
||||
|
||||
init(request: ImageRequest, key: PreheatKey) {
|
||||
self.request = request
|
||||
self.key = key
|
||||
}
|
||||
}
|
||||
|
||||
private struct PreheatKey: Hashable {
|
||||
let cacheKey: ImageRequest.CacheKey
|
||||
let loadKey: ImageRequest.LoadKey
|
||||
|
||||
init(request: ImageRequest) {
|
||||
self.cacheKey = ImageRequest.CacheKey(request: request)
|
||||
self.loadKey = ImageRequest.LoadKey(request: request)
|
||||
}
|
||||
}
|
||||
}
|
||||
193
Pods/Nuke/Sources/ImageProcessing.swift
generated
Normal file
@@ -0,0 +1,193 @@
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) 2015-2019 Alexander Grebenyuk (github.com/kean).
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Performs image processing.
|
||||
public protocol ImageProcessing: Equatable {
|
||||
/// Returns processed image.
|
||||
func process(image: Image, context: ImageProcessingContext) -> Image?
|
||||
}
|
||||
|
||||
/// Image processing context used when selecting which processor to use.
|
||||
public struct ImageProcessingContext {
|
||||
public let request: ImageRequest
|
||||
public let isFinal: Bool
|
||||
public let scanNumber: Int? // need a more general purpose way to implement this
|
||||
}
|
||||
|
||||
/// Composes multiple processors.
|
||||
internal struct ImageProcessorComposition: ImageProcessing {
|
||||
private let processors: [AnyImageProcessor]
|
||||
|
||||
/// Composes multiple processors.
|
||||
public init(_ processors: [AnyImageProcessor]) {
|
||||
self.processors = processors
|
||||
}
|
||||
|
||||
/// Processes the given image by applying each processor in an order in
|
||||
/// which they were added. If one of the processors fails to produce
|
||||
/// an image the processing stops and `nil` is returned.
|
||||
func process(image: Image, context: ImageProcessingContext) -> Image? {
|
||||
return processors.reduce(image) { image, processor in
|
||||
return autoreleasepool {
|
||||
image.flatMap { processor.process(image: $0, context: context) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the underlying processors are pairwise-equivalent.
|
||||
public static func == (lhs: ImageProcessorComposition, rhs: ImageProcessorComposition) -> Bool {
|
||||
return lhs.processors == rhs.processors
|
||||
}
|
||||
}
|
||||
|
||||
/// Type-erased image processor.
|
||||
public struct AnyImageProcessor: ImageProcessing {
|
||||
private let _process: (Image, ImageProcessingContext) -> Image?
|
||||
private let _processor: Any
|
||||
private let _equals: (AnyImageProcessor) -> Bool
|
||||
|
||||
public init<P: ImageProcessing>(_ processor: P) {
|
||||
self._process = { processor.process(image: $0, context: $1) }
|
||||
self._processor = processor
|
||||
self._equals = { ($0._processor as? P) == processor }
|
||||
}
|
||||
|
||||
public func process(image: Image, context: ImageProcessingContext) -> Image? {
|
||||
return self._process(image, context)
|
||||
}
|
||||
|
||||
public static func == (lhs: AnyImageProcessor, rhs: AnyImageProcessor) -> Bool {
|
||||
return lhs._equals(rhs)
|
||||
}
|
||||
}
|
||||
|
||||
internal struct AnonymousImageProcessor<Key: Hashable>: ImageProcessing {
|
||||
private let _key: Key
|
||||
private let _closure: (Image) -> Image?
|
||||
|
||||
init(_ key: Key, _ closure: @escaping (Image) -> Image?) {
|
||||
self._key = key; self._closure = closure
|
||||
}
|
||||
|
||||
func process(image: Image, context: ImageProcessingContext) -> Image? {
|
||||
return self._closure(image)
|
||||
}
|
||||
|
||||
static func == (lhs: AnonymousImageProcessor, rhs: AnonymousImageProcessor) -> Bool {
|
||||
return lhs._key == rhs._key
|
||||
}
|
||||
}
|
||||
|
||||
extension ImageProcessing {
|
||||
func process(image: ImageContainer, request: ImageRequest) -> Image? {
|
||||
let context = ImageProcessingContext(request: request, isFinal: image.isFinal, scanNumber: image.scanNumber)
|
||||
return process(image: image.image, context: context)
|
||||
}
|
||||
}
|
||||
|
||||
#if !os(macOS)
|
||||
import UIKit
|
||||
|
||||
/// Decompresses and (optionally) scales down input images. Maintains
|
||||
/// original aspect ratio.
|
||||
///
|
||||
/// Decompressing compressed image formats (such as JPEG) can significantly
|
||||
/// improve drawing performance as it allows a bitmap representation to be
|
||||
/// created in a background rather than on the main thread.
|
||||
public struct ImageDecompressor: ImageProcessing {
|
||||
|
||||
/// An option for how to resize the image.
|
||||
public enum ContentMode {
|
||||
/// Scales the image so that it completely fills the target size.
|
||||
/// Doesn't clip images.
|
||||
case aspectFill
|
||||
|
||||
/// Scales the image so that it fits the target size.
|
||||
case aspectFit
|
||||
}
|
||||
|
||||
/// Size to pass to disable resizing.
|
||||
public static let MaximumSize = CGSize(
|
||||
width: CGFloat.greatestFiniteMagnitude,
|
||||
height: CGFloat.greatestFiniteMagnitude
|
||||
)
|
||||
|
||||
private let targetSize: CGSize
|
||||
private let contentMode: ContentMode
|
||||
private let upscale: Bool
|
||||
|
||||
/// Initializes `Decompressor` with the given parameters.
|
||||
/// - parameter targetSize: Size in pixels. `MaximumSize` by default.
|
||||
/// - parameter contentMode: An option for how to resize the image
|
||||
/// to the target size. `.aspectFill` by default.
|
||||
public init(targetSize: CGSize = MaximumSize, contentMode: ContentMode = .aspectFill, upscale: Bool = false) {
|
||||
self.targetSize = targetSize
|
||||
self.contentMode = contentMode
|
||||
self.upscale = upscale
|
||||
}
|
||||
|
||||
/// Decompresses and scales the image.
|
||||
public func process(image: Image, context: ImageProcessingContext) -> Image? {
|
||||
return decompress(image, targetSize: targetSize, contentMode: contentMode, upscale: upscale)
|
||||
}
|
||||
|
||||
/// Returns true if both have the same `targetSize` and `contentMode`.
|
||||
public static func == (lhs: ImageDecompressor, rhs: ImageDecompressor) -> Bool {
|
||||
return lhs.targetSize == rhs.targetSize && lhs.contentMode == rhs.contentMode
|
||||
}
|
||||
|
||||
#if !os(watchOS)
|
||||
/// Returns target size in pixels for the given view. Takes main screen
|
||||
/// scale into the account.
|
||||
public static func targetSize(for view: UIView) -> CGSize { // in pixels
|
||||
let scale = UIScreen.main.scale
|
||||
let size = view.bounds.size
|
||||
return CGSize(width: size.width * scale, height: size.height * scale)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
internal func decompress(_ image: UIImage, targetSize: CGSize, contentMode: ImageDecompressor.ContentMode, upscale: Bool) -> UIImage {
|
||||
guard let cgImage = image.cgImage else { return image }
|
||||
let bitmapSize = CGSize(width: cgImage.width, height: cgImage.height)
|
||||
let scaleHor = targetSize.width / bitmapSize.width
|
||||
let scaleVert = targetSize.height / bitmapSize.height
|
||||
let scale = contentMode == .aspectFill ? max(scaleHor, scaleVert) : min(scaleHor, scaleVert)
|
||||
return decompress(image, scale: CGFloat(upscale ? scale : min(scale, 1)))
|
||||
}
|
||||
|
||||
internal func decompress(_ image: UIImage, scale: CGFloat) -> UIImage {
|
||||
guard let cgImage = image.cgImage else { return image }
|
||||
|
||||
let size = CGSize(
|
||||
width: round(scale * CGFloat(cgImage.width)),
|
||||
height: round(scale * CGFloat(cgImage.height))
|
||||
)
|
||||
|
||||
// For more info see:
|
||||
// - Quartz 2D Programming Guide
|
||||
// - https://github.com/kean/Nuke/issues/35
|
||||
// - https://github.com/kean/Nuke/issues/57
|
||||
let alphaInfo: CGImageAlphaInfo = isOpaque(cgImage) ? .noneSkipLast : .premultipliedLast
|
||||
|
||||
guard let ctx = CGContext(
|
||||
data: nil,
|
||||
width: Int(size.width), height: Int(size.height),
|
||||
bitsPerComponent: 8, bytesPerRow: 0,
|
||||
space: CGColorSpaceCreateDeviceRGB(),
|
||||
bitmapInfo: alphaInfo.rawValue) else {
|
||||
return image
|
||||
}
|
||||
ctx.draw(cgImage, in: CGRect(origin: CGPoint.zero, size: size))
|
||||
guard let decompressed = ctx.makeImage() else { return image }
|
||||
return UIImage(cgImage: decompressed, scale: image.scale, orientation: image.imageOrientation)
|
||||
}
|
||||
|
||||
private func isOpaque(_ image: CGImage) -> Bool {
|
||||
let alpha = image.alphaInfo
|
||||
return alpha == .none || alpha == .noneSkipFirst || alpha == .noneSkipLast
|
||||
}
|
||||
#endif
|
||||
335
Pods/Nuke/Sources/ImageRequest.swift
generated
Normal file
@@ -0,0 +1,335 @@
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) 2015-2019 Alexander Grebenyuk (github.com/kean).
|
||||
|
||||
import Foundation
|
||||
|
||||
#if !os(macOS)
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
/// Represents an image request.
|
||||
public struct ImageRequest {
|
||||
|
||||
// MARK: Parameters of the Request
|
||||
|
||||
internal var urlString: String? {
|
||||
return _ref._urlString
|
||||
}
|
||||
|
||||
/// The `URLRequest` used for loading an image.
|
||||
public var urlRequest: URLRequest {
|
||||
get { return _ref.resource.urlRequest }
|
||||
set {
|
||||
_mutate {
|
||||
$0.resource = Resource.urlRequest(newValue)
|
||||
$0._urlString = newValue.url?.absoluteString
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Processor to be applied to the image. `Decompressor` by default.
|
||||
///
|
||||
/// Decompressing compressed image formats (such as JPEG) can significantly
|
||||
/// improve drawing performance as it allows a bitmap representation to be
|
||||
/// created in a background rather than on the main thread.
|
||||
public var processor: AnyImageProcessor? {
|
||||
get {
|
||||
// Default processor on macOS is nil, on other platforms is Decompressor
|
||||
#if !os(macOS)
|
||||
return _ref._isDefaultProcessorUsed ? ImageRequest.decompressor : _ref._processor
|
||||
#else
|
||||
return _ref._isDefaultProcessorUsed ? nil : _ref._processor
|
||||
#endif
|
||||
}
|
||||
set {
|
||||
_mutate {
|
||||
$0._isDefaultProcessorUsed = false
|
||||
$0._processor = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The policy to use when reading or writing images to the memory cache.
|
||||
public struct MemoryCacheOptions {
|
||||
/// `true` by default.
|
||||
public var isReadAllowed = true
|
||||
|
||||
/// `true` by default.
|
||||
public var isWriteAllowed = true
|
||||
|
||||
public init() {}
|
||||
}
|
||||
|
||||
/// `MemoryCacheOptions()` (read allowed, write allowed) by default.
|
||||
public var memoryCacheOptions: MemoryCacheOptions {
|
||||
get { return _ref.memoryCacheOptions }
|
||||
set { _mutate { $0.memoryCacheOptions = newValue } }
|
||||
}
|
||||
|
||||
/// The execution priority of the request.
|
||||
public enum Priority: Int, Comparable {
|
||||
case veryLow = 0, low, normal, high, veryHigh
|
||||
|
||||
internal var queuePriority: Operation.QueuePriority {
|
||||
switch self {
|
||||
case .veryLow: return .veryLow
|
||||
case .low: return .low
|
||||
case .normal: return .normal
|
||||
case .high: return .high
|
||||
case .veryHigh: return .veryHigh
|
||||
}
|
||||
}
|
||||
|
||||
public static func < (lhs: Priority, rhs: Priority) -> Bool {
|
||||
return lhs.rawValue < rhs.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
/// The relative priority of the operation. This value is used to influence
|
||||
/// the order in which requests are executed. `.normal` by default.
|
||||
public var priority: Priority {
|
||||
get { return _ref.priority }
|
||||
set { _mutate { $0.priority = newValue }}
|
||||
}
|
||||
|
||||
/// Returns a key that compares requests with regards to caching images.
|
||||
///
|
||||
/// The default key considers two requests equivalent it they have the same
|
||||
/// `URLRequests` and the same processors. `URLRequests` are compared
|
||||
/// just by their `URLs`.
|
||||
public var cacheKey: AnyHashable? {
|
||||
get { return _ref.cacheKey }
|
||||
set { _mutate { $0.cacheKey = newValue } }
|
||||
}
|
||||
|
||||
/// Returns a key that compares requests with regards to loading images.
|
||||
///
|
||||
/// The default key considers two requests equivalent it they have the same
|
||||
/// `URLRequests` and the same processors. `URLRequests` are compared by
|
||||
/// their `URL`, `cachePolicy`, and `allowsCellularAccess` properties.
|
||||
public var loadKey: AnyHashable? {
|
||||
get { return _ref.loadKey }
|
||||
set { _mutate { $0.loadKey = newValue } }
|
||||
}
|
||||
|
||||
/// If decoding is disabled, when the image data is loaded, the pipeline is
|
||||
/// not going to create an image from it and will produce the `.decodingFailed`
|
||||
/// error instead. `false` by default.
|
||||
var isDecodingDisabled: Bool {
|
||||
// This only used by `ImagePreheater` right now
|
||||
get { return _ref.isDecodingDisabled }
|
||||
set { _mutate { $0.isDecodingDisabled = newValue } }
|
||||
}
|
||||
|
||||
/// Custom info passed alongside the request.
|
||||
public var userInfo: Any? {
|
||||
get { return _ref.userInfo }
|
||||
set { _mutate { $0.userInfo = newValue }}
|
||||
}
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
/// Initializes a request with the given URL.
|
||||
public init(url: URL) {
|
||||
_ref = Container(resource: Resource.url(url))
|
||||
_ref._urlString = url.absoluteString
|
||||
// creating `.absoluteString` takes 50% of time of Request creation,
|
||||
// it's still faster than using URLs as cache keys
|
||||
}
|
||||
|
||||
/// Initializes a request with the given request.
|
||||
public init(urlRequest: URLRequest) {
|
||||
_ref = Container(resource: Resource.urlRequest(urlRequest))
|
||||
_ref._urlString = urlRequest.url?.absoluteString
|
||||
}
|
||||
|
||||
#if !os(macOS)
|
||||
|
||||
/// Initializes a request with the given URL.
|
||||
/// - parameter processor: Custom image processer.
|
||||
public init<Processor: ImageProcessing>(url: URL, processor: Processor) {
|
||||
self.init(url: url)
|
||||
self.processor = AnyImageProcessor(processor)
|
||||
}
|
||||
|
||||
/// Initializes a request with the given request.
|
||||
/// - parameter processor: Custom image processer.
|
||||
public init<Processor: ImageProcessing>(urlRequest: URLRequest, processor: Processor) {
|
||||
self.init(urlRequest: urlRequest)
|
||||
self.processor = AnyImageProcessor(processor)
|
||||
}
|
||||
|
||||
/// Initializes a request with the given URL.
|
||||
/// - parameter targetSize: Size in pixels.
|
||||
/// - parameter contentMode: An option for how to resize the image
|
||||
/// to the target size.
|
||||
public init(url: URL, targetSize: CGSize, contentMode: ImageDecompressor.ContentMode, upscale: Bool = false) {
|
||||
self.init(url: url, processor: ImageDecompressor(
|
||||
targetSize: targetSize,
|
||||
contentMode: contentMode,
|
||||
upscale: upscale
|
||||
))
|
||||
}
|
||||
|
||||
/// Initializes a request with the given request.
|
||||
/// - parameter targetSize: Size in pixels.
|
||||
/// - parameter contentMode: An option for how to resize the image
|
||||
/// to the target size.
|
||||
public init(urlRequest: URLRequest, targetSize: CGSize, contentMode: ImageDecompressor.ContentMode, upscale: Bool = false) {
|
||||
self.init(urlRequest: urlRequest, processor: ImageDecompressor(
|
||||
targetSize: targetSize,
|
||||
contentMode: contentMode,
|
||||
upscale: upscale
|
||||
))
|
||||
}
|
||||
|
||||
fileprivate static let decompressor = AnyImageProcessor(ImageDecompressor())
|
||||
|
||||
#endif
|
||||
|
||||
// CoW:
|
||||
|
||||
private var _ref: Container
|
||||
|
||||
private mutating func _mutate(_ closure: (Container) -> Void) {
|
||||
if !isKnownUniquelyReferenced(&_ref) {
|
||||
_ref = Container(container: _ref)
|
||||
}
|
||||
closure(_ref)
|
||||
}
|
||||
|
||||
/// Just like many Swift built-in types, `ImageRequest` uses CoW approach to
|
||||
/// avoid memberwise retain/releases when `ImageRequest` is passed around.
|
||||
private class Container {
|
||||
var resource: Resource
|
||||
var _urlString: String? // memoized absoluteString
|
||||
// true unless user set a custom one, this allows us not to store the
|
||||
// default processor anywhere in the `Container` & skip equality tests
|
||||
// when the default processor is used
|
||||
var _isDefaultProcessorUsed: Bool = true
|
||||
var _processor: AnyImageProcessor?
|
||||
var memoryCacheOptions = MemoryCacheOptions()
|
||||
var priority: ImageRequest.Priority = .normal
|
||||
var cacheKey: AnyHashable?
|
||||
var loadKey: AnyHashable?
|
||||
var isDecodingDisabled: Bool = false
|
||||
var userInfo: Any?
|
||||
|
||||
/// Creates a resource with a default processor.
|
||||
init(resource: Resource) {
|
||||
self.resource = resource
|
||||
}
|
||||
|
||||
/// Creates a copy.
|
||||
init(container ref: Container) {
|
||||
self.resource = ref.resource
|
||||
self._urlString = ref._urlString
|
||||
self._isDefaultProcessorUsed = ref._isDefaultProcessorUsed
|
||||
self._processor = ref._processor
|
||||
self.memoryCacheOptions = ref.memoryCacheOptions
|
||||
self.priority = ref.priority
|
||||
self.cacheKey = ref.cacheKey
|
||||
self.loadKey = ref.loadKey
|
||||
self.isDecodingDisabled = ref.isDecodingDisabled
|
||||
self.userInfo = ref.userInfo
|
||||
}
|
||||
}
|
||||
|
||||
/// Resource representation (either URL or URLRequest).
|
||||
private enum Resource {
|
||||
case url(URL)
|
||||
case urlRequest(URLRequest)
|
||||
|
||||
var urlRequest: URLRequest {
|
||||
switch self {
|
||||
case let .url(url): return URLRequest(url: url) // create lazily
|
||||
case let .urlRequest(urlRequest): return urlRequest
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension ImageRequest {
|
||||
/// Appends a processor to the request. You can append arbitrary number of
|
||||
/// processors to the request.
|
||||
mutating func process<P: ImageProcessing>(with processor: P) {
|
||||
guard let existing = self.processor else {
|
||||
self.processor = AnyImageProcessor(processor)
|
||||
return
|
||||
}
|
||||
// Chain new processor and the existing one.
|
||||
self.processor = AnyImageProcessor(ImageProcessorComposition([existing, AnyImageProcessor(processor)]))
|
||||
}
|
||||
|
||||
/// Appends a processor to the request. You can append arbitrary number of
|
||||
/// processors to the request.
|
||||
func processed<P: ImageProcessing>(with processor: P) -> ImageRequest {
|
||||
var request = self
|
||||
request.process(with: processor)
|
||||
return request
|
||||
}
|
||||
|
||||
/// Appends a processor to the request. You can append arbitrary number of
|
||||
/// processors to the request.
|
||||
mutating func process<Key: Hashable>(key: Key, _ closure: @escaping (Image) -> Image?) {
|
||||
process(with: AnonymousImageProcessor<Key>(key, closure))
|
||||
}
|
||||
|
||||
/// Appends a processor to the request. You can append arbitrary number of
|
||||
/// processors to the request.
|
||||
func processed<Key: Hashable>(key: Key, _ closure: @escaping (Image) -> Image?) -> ImageRequest {
|
||||
return processed(with: AnonymousImageProcessor<Key>(key, closure))
|
||||
}
|
||||
}
|
||||
|
||||
internal extension ImageRequest {
|
||||
struct CacheKey: Hashable {
|
||||
let request: ImageRequest
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
if let customKey = request._ref.cacheKey {
|
||||
hasher.combine(customKey)
|
||||
} else {
|
||||
hasher.combine(request._ref._urlString?.hashValue ?? 0)
|
||||
}
|
||||
}
|
||||
|
||||
static func == (lhs: CacheKey, rhs: CacheKey) -> Bool {
|
||||
let lhs = lhs.request, rhs = rhs.request
|
||||
if let lhsCustomKey = lhs._ref.cacheKey, let rhsCustomKey = rhs._ref.cacheKey {
|
||||
return lhsCustomKey == rhsCustomKey
|
||||
}
|
||||
guard lhs._ref._urlString == rhs._ref._urlString else {
|
||||
return false
|
||||
}
|
||||
return (lhs._ref._isDefaultProcessorUsed && rhs._ref._isDefaultProcessorUsed)
|
||||
|| (lhs.processor == rhs.processor)
|
||||
}
|
||||
}
|
||||
|
||||
struct LoadKey: Hashable {
|
||||
let request: ImageRequest
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
if let customKey = request._ref.loadKey {
|
||||
hasher.combine(customKey)
|
||||
} else {
|
||||
hasher.combine(request._ref._urlString?.hashValue ?? 0)
|
||||
}
|
||||
}
|
||||
|
||||
static func == (lhs: LoadKey, rhs: LoadKey) -> Bool {
|
||||
func isEqual(_ lhs: URLRequest, _ rhs: URLRequest) -> Bool {
|
||||
return lhs.cachePolicy == rhs.cachePolicy
|
||||
&& lhs.allowsCellularAccess == rhs.allowsCellularAccess
|
||||
}
|
||||
let lhs = lhs.request, rhs = rhs.request
|
||||
if let lhsCustomKey = lhs._ref.loadKey, let rhsCustomKey = rhs._ref.loadKey {
|
||||
return lhsCustomKey == rhsCustomKey
|
||||
}
|
||||
return lhs._ref._urlString == rhs._ref._urlString
|
||||
&& isEqual(lhs.urlRequest, rhs.urlRequest)
|
||||
}
|
||||
}
|
||||
}
|
||||
115
Pods/Nuke/Sources/ImageTaskMetrics.swift
generated
Normal file
@@ -0,0 +1,115 @@
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) 2015-2019 Alexander Grebenyuk (github.com/kean).
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct ImageTaskMetrics: CustomDebugStringConvertible {
|
||||
public let taskId: Int
|
||||
public internal(set) var wasCancelled: Bool = false
|
||||
public internal(set) var session: SessionMetrics?
|
||||
|
||||
public let startDate: Date
|
||||
public internal(set) var processStartDate: Date?
|
||||
public internal(set) var processEndDate: Date?
|
||||
public internal(set) var endDate: Date? // failed or completed
|
||||
public var totalDuration: TimeInterval? {
|
||||
guard let endDate = endDate else { return nil }
|
||||
return endDate.timeIntervalSince(startDate)
|
||||
}
|
||||
|
||||
/// Returns `true` is the task wasn't the one that initiated image loading.
|
||||
public internal(set) var wasSubscibedToExistingSession: Bool = false
|
||||
public internal(set) var isMemoryCacheHit: Bool = false
|
||||
|
||||
init(taskId: Int, startDate: Date) {
|
||||
self.taskId = taskId; self.startDate = startDate
|
||||
}
|
||||
|
||||
public var debugDescription: String {
|
||||
var printer = Printer()
|
||||
printer.section(title: "Task Information") {
|
||||
$0.value("Task ID", taskId)
|
||||
$0.timeline("Duration", startDate, endDate, isReversed: false)
|
||||
$0.timeline("Process", processStartDate, processEndDate)
|
||||
$0.value("Was Cancelled", wasCancelled)
|
||||
$0.value("Is Memory Cache Hit", isMemoryCacheHit)
|
||||
$0.value("Was Subscribed To Existing Image Loading Session", wasSubscibedToExistingSession)
|
||||
}
|
||||
printer.section(title: "Image Loading Session") {
|
||||
$0.string(session.map({ $0.debugDescription }) ?? "nil")
|
||||
}
|
||||
return printer.output()
|
||||
}
|
||||
|
||||
// Download session metrics. One more more tasks can share the same
|
||||
// session metrics.
|
||||
public final class SessionMetrics: CustomDebugStringConvertible {
|
||||
/// - important: Data loading might start prior to `timeResumed` if the task gets
|
||||
/// coalesced with another task.
|
||||
public let sessionId: Int
|
||||
public internal(set) var wasCancelled: Bool = false
|
||||
|
||||
// MARK: - Timeline
|
||||
|
||||
public let startDate = Date()
|
||||
|
||||
public internal(set) var checkDiskCacheStartDate: Date?
|
||||
public internal(set) var checkDiskCacheEndDate: Date?
|
||||
|
||||
public internal(set) var loadDataStartDate: Date?
|
||||
public internal(set) var loadDataEndDate: Date?
|
||||
|
||||
public internal(set) var decodeStartDate: Date?
|
||||
public internal(set) var decodeEndDate: Date?
|
||||
|
||||
@available(*, deprecated, message: "Please use the same property on `ImageTaskMetrics` instead.")
|
||||
public internal(set) var processStartDate: Date?
|
||||
|
||||
@available(*, deprecated, message: "Please use the same property on `ImageTaskMetrics` instead.")
|
||||
public internal(set) var processEndDate: Date?
|
||||
|
||||
public internal(set) var endDate: Date? // failed or completed
|
||||
|
||||
public var totalDuration: TimeInterval? {
|
||||
guard let endDate = endDate else { return nil }
|
||||
return endDate.timeIntervalSince(startDate)
|
||||
}
|
||||
|
||||
// MARK: - Resumable Data
|
||||
|
||||
public internal(set) var wasResumed: Bool?
|
||||
public internal(set) var resumedDataCount: Int?
|
||||
public internal(set) var serverConfirmedResume: Bool?
|
||||
|
||||
public internal(set) var downloadedDataCount: Int?
|
||||
public var totalDownloadedDataCount: Int? {
|
||||
guard let downloaded = self.downloadedDataCount else { return nil }
|
||||
return downloaded + (resumedDataCount ?? 0)
|
||||
}
|
||||
|
||||
init(sessionId: Int) { self.sessionId = sessionId }
|
||||
|
||||
public var debugDescription: String {
|
||||
var printer = Printer()
|
||||
printer.section(title: "Session Information") {
|
||||
$0.value("Session ID", sessionId)
|
||||
$0.value("Total Duration", Printer.duration(totalDuration))
|
||||
$0.value("Was Cancelled", wasCancelled)
|
||||
}
|
||||
printer.section(title: "Timeline") {
|
||||
$0.timeline("Total", startDate, endDate)
|
||||
$0.line(String(repeating: "-", count: 36))
|
||||
$0.timeline("Check Disk Cache", checkDiskCacheStartDate, checkDiskCacheEndDate)
|
||||
$0.timeline("Load Data", loadDataStartDate, loadDataEndDate)
|
||||
$0.timeline("Decode", decodeStartDate, decodeEndDate)
|
||||
}
|
||||
printer.section(title: "Resumable Data") {
|
||||
$0.value("Was Resumed", wasResumed)
|
||||
$0.value("Resumable Data Count", resumedDataCount)
|
||||
$0.value("Server Confirmed Resume", serverConfirmedResume)
|
||||
}
|
||||
return printer.output()
|
||||
}
|
||||
}
|
||||
}
|
||||
496
Pods/Nuke/Sources/ImageView.swift
generated
Normal file
@@ -0,0 +1,496 @@
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) 2015-2019 Alexander Grebenyuk (github.com/kean).
|
||||
|
||||
import Foundation
|
||||
|
||||
#if !os(macOS)
|
||||
import UIKit.UIImage
|
||||
/// Alias for `UIImage`.
|
||||
public typealias Image = UIImage
|
||||
#else
|
||||
import AppKit.NSImage
|
||||
/// Alias for `NSImage`.
|
||||
public typealias Image = NSImage
|
||||
#endif
|
||||
|
||||
#if !os(watchOS)
|
||||
|
||||
/// Displays images. Adopt this protocol in views to make them compatible with
|
||||
/// Nuke APIs.
|
||||
///
|
||||
/// The protocol is defined as `@objc` to enable users to override its methods
|
||||
/// in extensions (e.g. you can override `display(image:)` in `UIImageView` subclass).
|
||||
@objc public protocol ImageDisplaying {
|
||||
@objc func display(image: Image?)
|
||||
}
|
||||
|
||||
#if !os(macOS)
|
||||
import UIKit
|
||||
/// A `UIView` that implements `ImageDisplaying` protocol.
|
||||
public typealias ImageDisplayingView = UIView & ImageDisplaying
|
||||
|
||||
extension UIImageView: ImageDisplaying {
|
||||
/// Displays an image.
|
||||
open func display(image: Image?) {
|
||||
self.image = image
|
||||
}
|
||||
}
|
||||
#else
|
||||
import Cocoa
|
||||
/// An `NSView` that implements `ImageDisplaying` protocol.
|
||||
public typealias ImageDisplayingView = NSView & ImageDisplaying
|
||||
|
||||
extension NSImageView: ImageDisplaying {
|
||||
/// Displays an image.
|
||||
open func display(image: Image?) {
|
||||
self.image = image
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/// Loads an image into the view.
|
||||
///
|
||||
/// Before loading the new image prepares the view for reuse by cancelling any
|
||||
/// outstanding requests and removing previously displayed images (if any).
|
||||
///
|
||||
/// If the image is stored in memory cache, the image is displayed immediately.
|
||||
/// If not, the image is loaded using an image pipeline. Displays a `placeholder`
|
||||
/// if it was provided. When the request completes the loaded image is displayed
|
||||
/// (or `failureImage` in case of an error).
|
||||
///
|
||||
/// Nuke keeps a weak reference to the view. If the view is deallocated
|
||||
/// the associated request automatically gets cancelled.
|
||||
///
|
||||
/// - parameter options: `ImageLoadingOptions.shared` by default.
|
||||
/// - parameter progress: A closure to be called periodically on the main thread
|
||||
/// when the progress is updated. `nil` by default.
|
||||
/// - parameter completion: A closure to be called on the main thread when the
|
||||
/// request is finished. Gets called synchronously if the response was found in
|
||||
/// memory cache. `nil` by default.
|
||||
/// - returns: An image task of `nil` if the image was found in memory cache.
|
||||
@discardableResult
|
||||
public func loadImage(with url: URL,
|
||||
options: ImageLoadingOptions = ImageLoadingOptions.shared,
|
||||
into view: ImageDisplayingView,
|
||||
progress: ImageTask.ProgressHandler? = nil,
|
||||
completion: ImageTask.Completion? = nil) -> ImageTask? {
|
||||
return loadImage(with: ImageRequest(url: url), options: options, into: view, progress: progress, completion: completion)
|
||||
}
|
||||
|
||||
/// Loads an image into the view.
|
||||
///
|
||||
/// Before loading the new image prepares the view for reuse by cancelling any
|
||||
/// outstanding requests and removing previously displayed images (if any).
|
||||
///
|
||||
/// If the image is stored in memory cache, the image is displayed immediately.
|
||||
/// If not, the image is loaded using an image pipeline. Displays a `placeholder`
|
||||
/// if it was provided. When the request completes the loaded image is displayed
|
||||
/// (or `failureImage` in case of an error).
|
||||
///
|
||||
/// Nuke keeps a weak reference to the view. If the view is deallocated
|
||||
/// the associated request automatically gets cancelled.
|
||||
///
|
||||
/// - parameter options: `ImageLoadingOptions.shared` by default.
|
||||
/// - parameter progress: A closure to be called periodically on the main thread
|
||||
/// when the progress is updated. `nil` by default.
|
||||
/// - parameter completion: A closure to be called on the main thread when the
|
||||
/// request is finished. Gets called synchronously if the response was found in
|
||||
/// memory cache. `nil` by default.
|
||||
/// - returns: An image task of `nil` if the image was found in memory cache.
|
||||
@discardableResult
|
||||
public func loadImage(with request: ImageRequest,
|
||||
options: ImageLoadingOptions = ImageLoadingOptions.shared,
|
||||
into view: ImageDisplayingView,
|
||||
progress: ImageTask.ProgressHandler? = nil,
|
||||
completion: ImageTask.Completion? = nil) -> ImageTask? {
|
||||
assert(Thread.isMainThread)
|
||||
let controller = ImageViewController.controller(for: view)
|
||||
return controller.loadImage(with: request, options: options, progress: progress, completion: completion)
|
||||
}
|
||||
|
||||
/// Cancels an outstanding request associated with the view.
|
||||
public func cancelRequest(for view: ImageDisplayingView) {
|
||||
assert(Thread.isMainThread)
|
||||
ImageViewController.controller(for: view).cancelOutstandingTask()
|
||||
}
|
||||
|
||||
// MARK: - ImageLoadingOptions
|
||||
|
||||
/// A range of options that control how the image is loaded and displayed.
|
||||
public struct ImageLoadingOptions {
|
||||
/// Shared options.
|
||||
public static var shared = ImageLoadingOptions()
|
||||
|
||||
/// Placeholder to be displayed when the image is loading. `nil` by default.
|
||||
public var placeholder: Image?
|
||||
|
||||
/// The image transition animation performed when displaying a loaded image.
|
||||
/// Only runs when the image was not found in memory cache. `.nil` by default.
|
||||
public var transition: Transition?
|
||||
|
||||
/// Image to be displayed when the request fails. `nil` by default.
|
||||
public var failureImage: Image?
|
||||
|
||||
/// The image transition animation performed when displaying a failure image.
|
||||
/// `.nil` by default.
|
||||
public var failureImageTransition: Transition?
|
||||
|
||||
/// If true, the requested image will always appear with transition, even
|
||||
/// when loaded from cache
|
||||
public var alwaysTransition = false
|
||||
|
||||
/// If true, every time you request a new image for a view, the view will be
|
||||
/// automatically prepared for reuse: image will be set to `nil`, and animations
|
||||
/// will be removed. `true` by default.
|
||||
public var isPrepareForReuseEnabled = true
|
||||
|
||||
/// Custom pipeline to be used. `nil` by default.
|
||||
public var pipeline: ImagePipeline?
|
||||
|
||||
#if !os(macOS)
|
||||
/// Content modes to be used for each image type (placeholder, success,
|
||||
/// failure). `nil` by default (don't change content mode).
|
||||
public var contentModes: ContentModes?
|
||||
|
||||
/// Custom content modes to be used for each image type (placeholder, success,
|
||||
/// failure).
|
||||
public struct ContentModes {
|
||||
/// Content mode to be used for the loaded image.
|
||||
public var success: UIView.ContentMode
|
||||
/// Content mode to be used when displaying a `failureImage`.
|
||||
public var failure: UIView.ContentMode
|
||||
/// Content mode to be used when displaying a `placeholder`.
|
||||
public var placeholder: UIView.ContentMode
|
||||
|
||||
/// - parameter success: A content mode to be used with a loaded image.
|
||||
/// - parameter failure: A content mode to be used with a `failureImage`.
|
||||
/// - parameter placeholder: A content mode to be used with a `placeholder`.
|
||||
public init(success: UIView.ContentMode, failure: UIView.ContentMode, placeholder: UIView.ContentMode) {
|
||||
self.success = success; self.failure = failure; self.placeholder = placeholder
|
||||
}
|
||||
}
|
||||
|
||||
/// - parameter placeholder: Placeholder to be displayed when the image is
|
||||
/// loading . `nil` by default.
|
||||
/// - parameter transision: The image transition animation performed when
|
||||
/// displaying a loaded image. Only runs when the image was not found in
|
||||
/// memory cache `.nil` by default (no animations).
|
||||
/// - parameter failureImage: Image to be displayd when request fails.
|
||||
/// `nil` by default.
|
||||
/// - parameter failureImageTransition: The image transition animation
|
||||
/// performed when displaying a failure image. `.nil` by default.
|
||||
/// - parameter contentModes: Content modes to be used for each image type
|
||||
/// (placeholder, success, failure). `nil` by default (don't change content mode).
|
||||
public init(placeholder: Image? = nil, transition: Transition? = nil, failureImage: Image? = nil, failureImageTransition: Transition? = nil, contentModes: ContentModes? = nil) {
|
||||
self.placeholder = placeholder
|
||||
self.transition = transition
|
||||
self.failureImage = failureImage
|
||||
self.failureImageTransition = failureImageTransition
|
||||
self.contentModes = contentModes
|
||||
}
|
||||
#else
|
||||
public init(placeholder: Image? = nil, transition: Transition? = nil, failureImage: Image? = nil, failureImageTransition: Transition? = nil) {
|
||||
self.placeholder = placeholder
|
||||
self.transition = transition
|
||||
self.failureImage = failureImage
|
||||
self.failureImageTransition = failureImageTransition
|
||||
}
|
||||
#endif
|
||||
|
||||
/// An animated image transition.
|
||||
public struct Transition {
|
||||
var style: Style
|
||||
|
||||
struct Parameters { // internal representation
|
||||
let duration: TimeInterval
|
||||
#if !os(macOS)
|
||||
let options: UIView.AnimationOptions
|
||||
#endif
|
||||
}
|
||||
|
||||
enum Style { // internal representation
|
||||
case fadeIn(parameters: Parameters)
|
||||
case custom((ImageDisplayingView, Image) -> Void)
|
||||
}
|
||||
|
||||
#if !os(macOS)
|
||||
/// Fade-in transition (cross-fade in case the image view is already
|
||||
/// displaying an image).
|
||||
public static func fadeIn(duration: TimeInterval, options: UIView.AnimationOptions = .allowUserInteraction) -> Transition {
|
||||
return Transition(style: .fadeIn(parameters: Parameters(duration: duration, options: options)))
|
||||
}
|
||||
#else
|
||||
/// Fade-in transition.
|
||||
public static func fadeIn(duration: TimeInterval) -> Transition {
|
||||
return Transition(style: .fadeIn(parameters: Parameters(duration: duration)))
|
||||
}
|
||||
#endif
|
||||
|
||||
/// Custom transition. Only runs when the image was not found in memory cache.
|
||||
public static func custom(_ closure: @escaping (ImageDisplayingView, Image) -> Void) -> Transition {
|
||||
return Transition(style: .custom(closure))
|
||||
}
|
||||
}
|
||||
|
||||
public init() {}
|
||||
}
|
||||
|
||||
// MARK: - ImageViewController
|
||||
|
||||
/// Manages image requests on behalf of an image view.
|
||||
///
|
||||
/// - note: With a few modifications this might become public at some point,
|
||||
/// however as it stands today `ImageViewController` is just a helper class,
|
||||
/// making it public wouldn't expose any additional functionality to the users.
|
||||
private final class ImageViewController {
|
||||
// Ideally should be `unowned` but can't because of the Swift bug
|
||||
// https://bugs.swift.org/browse/SR-7369
|
||||
private weak var imageView: ImageDisplayingView?
|
||||
private weak var task: ImageTask?
|
||||
private var taskId: Int = 0
|
||||
|
||||
// Automatically cancel the request when the view is deallocated.
|
||||
deinit {
|
||||
cancelOutstandingTask()
|
||||
}
|
||||
|
||||
init(view: /* weak */ ImageDisplayingView) {
|
||||
self.imageView = view
|
||||
}
|
||||
|
||||
// MARK: - Associating Controller
|
||||
|
||||
static var controllerAK = "ImageViewController.AssociatedKey"
|
||||
|
||||
// Lazily create a controller for a given view and associate it with a view.
|
||||
static func controller(for view: ImageDisplayingView) -> ImageViewController {
|
||||
if let controller = objc_getAssociatedObject(view, &ImageViewController.controllerAK) as? ImageViewController {
|
||||
return controller
|
||||
}
|
||||
let controller = ImageViewController(view: view)
|
||||
objc_setAssociatedObject(view, &ImageViewController.controllerAK, controller, .OBJC_ASSOCIATION_RETAIN)
|
||||
return controller
|
||||
}
|
||||
|
||||
// MARK: - Loading Images
|
||||
|
||||
func loadImage(with request: ImageRequest,
|
||||
options: ImageLoadingOptions,
|
||||
progress: ImageTask.ProgressHandler? = nil,
|
||||
completion: ImageTask.Completion? = nil) -> ImageTask? {
|
||||
cancelOutstandingTask()
|
||||
|
||||
guard let imageView = imageView else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if options.isPrepareForReuseEnabled { // enabled by default
|
||||
#if !os(macOS)
|
||||
imageView.layer.removeAllAnimations()
|
||||
#else
|
||||
imageView.layer?.removeAllAnimations()
|
||||
#endif
|
||||
}
|
||||
|
||||
let pipeline = options.pipeline ?? ImagePipeline.shared
|
||||
|
||||
// Quick synchronous memory cache lookup
|
||||
if request.memoryCacheOptions.isReadAllowed,
|
||||
let imageCache = pipeline.configuration.imageCache,
|
||||
let response = imageCache.cachedResponse(for: request) {
|
||||
handle(response: response, error: nil, fromMemCache: true, options: options)
|
||||
completion?(response, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Display a placeholder.
|
||||
if let placeholder = options.placeholder {
|
||||
imageView.display(image: placeholder)
|
||||
#if !os(macOS)
|
||||
if let contentMode = options.contentModes?.placeholder {
|
||||
imageView.contentMode = contentMode
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
if options.isPrepareForReuseEnabled {
|
||||
imageView.display(image: nil) // Remove previously displayed images (if any)
|
||||
}
|
||||
}
|
||||
|
||||
// Makes sure that view reuse is handled correctly.
|
||||
let taskId = self.taskId
|
||||
|
||||
// Start the request.
|
||||
self.task = pipeline.loadImage(
|
||||
with: request,
|
||||
progress: { [weak self] response, completed, total in
|
||||
guard self?.taskId == taskId else { return }
|
||||
self?.handle(partialImage: response, options: options)
|
||||
progress?(response, completed, total)
|
||||
},
|
||||
completion: { [weak self] response, error in
|
||||
guard self?.taskId == taskId else { return }
|
||||
self?.handle(response: response, error: error, fromMemCache: false, options: options)
|
||||
completion?(response, error)
|
||||
}
|
||||
)
|
||||
return self.task
|
||||
}
|
||||
|
||||
func cancelOutstandingTask() {
|
||||
taskId += 1
|
||||
task?.cancel()
|
||||
task = nil
|
||||
}
|
||||
|
||||
// MARK: - Handling Responses
|
||||
|
||||
#if !os(macOS)
|
||||
|
||||
private func handle(response: ImageResponse?, error: Error?, fromMemCache: Bool, options: ImageLoadingOptions) {
|
||||
if let image = response?.image {
|
||||
_display(image, options.transition, options.alwaysTransition, fromMemCache, options.contentModes?.success)
|
||||
} else if let failureImage = options.failureImage {
|
||||
_display(failureImage, options.failureImageTransition, options.alwaysTransition, fromMemCache, options.contentModes?.failure)
|
||||
}
|
||||
self.task = nil
|
||||
}
|
||||
|
||||
private func handle(partialImage response: ImageResponse?, options: ImageLoadingOptions) {
|
||||
guard let image = response?.image else { return }
|
||||
_display(image, options.transition, options.alwaysTransition, false, options.contentModes?.success)
|
||||
}
|
||||
|
||||
private func _display(_ image: Image, _ transition: ImageLoadingOptions.Transition?, _ alwaysTransition: Bool, _ fromMemCache: Bool, _ newContentMode: UIView.ContentMode?) {
|
||||
guard let imageView = imageView else { return }
|
||||
|
||||
if !fromMemCache || alwaysTransition, let transition = transition {
|
||||
switch transition.style {
|
||||
case let .fadeIn(params):
|
||||
_runFadeInTransition(image: image, params: params, contentMode: newContentMode)
|
||||
case let .custom(closure):
|
||||
// The user is reponsible for both displaying an image and performing
|
||||
// animations.
|
||||
closure(imageView, image)
|
||||
}
|
||||
} else {
|
||||
imageView.display(image: image)
|
||||
}
|
||||
if let newContentMode = newContentMode {
|
||||
imageView.contentMode = newContentMode
|
||||
}
|
||||
}
|
||||
|
||||
// Image view used for cross-fade transition between images with different
|
||||
// content modes.
|
||||
private lazy var transitionImageView = UIImageView()
|
||||
|
||||
private func _runFadeInTransition(image: Image, params: ImageLoadingOptions.Transition.Parameters, contentMode: UIView.ContentMode?) {
|
||||
guard let imageView = imageView else { return }
|
||||
|
||||
// Special case where we animate between content modes, only works
|
||||
// on imageView subclasses.
|
||||
if let contentMode = contentMode, imageView.contentMode != contentMode, let imageView = imageView as? UIImageView, imageView.image != nil {
|
||||
_runCrossDissolveWithContentMode(imageView: imageView, image: image, params: params)
|
||||
} else {
|
||||
_runSimpleFadeIn(image: image, params: params)
|
||||
}
|
||||
}
|
||||
|
||||
private func _runSimpleFadeIn(image: Image, params: ImageLoadingOptions.Transition.Parameters) {
|
||||
guard let imageView = imageView else { return }
|
||||
|
||||
UIView.transition(
|
||||
with: imageView,
|
||||
duration: params.duration,
|
||||
options: params.options.union(.transitionCrossDissolve),
|
||||
animations: {
|
||||
imageView.display(image: image)
|
||||
},
|
||||
completion: nil
|
||||
)
|
||||
}
|
||||
|
||||
/// Performs cross-dissolve animation alonside transition to a new content
|
||||
/// mode. This isn't natively supported feature and it requires a second
|
||||
/// image view. There might be better ways to implement it.
|
||||
private func _runCrossDissolveWithContentMode(imageView: UIImageView, image: Image, params: ImageLoadingOptions.Transition.Parameters) {
|
||||
// Lazily create a transition view.
|
||||
let transitionView = self.transitionImageView
|
||||
|
||||
// Create a transition view which mimics current view's contents.
|
||||
transitionView.image = imageView.image
|
||||
transitionView.contentMode = imageView.contentMode
|
||||
imageView.addSubview(transitionView)
|
||||
transitionView.frame = imageView.bounds
|
||||
|
||||
// "Manual" cross-fade.
|
||||
transitionView.alpha = 1
|
||||
imageView.alpha = 0
|
||||
imageView.image = image // Display new image in current view
|
||||
|
||||
UIView.animate(
|
||||
withDuration: params.duration,
|
||||
delay: 0,
|
||||
options: params.options,
|
||||
animations: {
|
||||
transitionView.alpha = 0
|
||||
imageView.alpha = 1
|
||||
},
|
||||
completion: { isCompleted in
|
||||
if isCompleted {
|
||||
transitionView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
private func handle(response: ImageResponse?, error: Error?, fromMemCache: Bool, options: ImageLoadingOptions) {
|
||||
// NSImageView doesn't support content mode, unfortunately.
|
||||
if let image = response?.image {
|
||||
_display(image, options.transition, options.alwaysTransition, fromMemCache)
|
||||
} else if let failureImage = options.failureImage {
|
||||
_display(failureImage, options.failureImageTransition, options.alwaysTransition, fromMemCache)
|
||||
}
|
||||
self.task = nil
|
||||
}
|
||||
|
||||
private func handle(partialImage response: ImageResponse?, options: ImageLoadingOptions) {
|
||||
guard let image = response?.image else { return }
|
||||
_display(image, options.transition, options.alwaysTransition, false)
|
||||
}
|
||||
|
||||
private func _display(_ image: Image, _ transition: ImageLoadingOptions.Transition?, _ alwaysTransition: Bool, _ fromMemCache: Bool) {
|
||||
guard let imageView = imageView else { return }
|
||||
|
||||
if !fromMemCache || alwaysTransition, let transition = transition {
|
||||
switch transition.style {
|
||||
case let .fadeIn(params):
|
||||
_runFadeInTransition(image: image, params: params)
|
||||
case let .custom(closure):
|
||||
// The user is reponsible for both displaying an image and performing
|
||||
// animations.
|
||||
closure(imageView, image)
|
||||
}
|
||||
} else {
|
||||
imageView.display(image: image)
|
||||
}
|
||||
}
|
||||
|
||||
private func _runFadeInTransition(image: Image, params: ImageLoadingOptions.Transition.Parameters) {
|
||||
let animation = CABasicAnimation(keyPath: "opacity")
|
||||
animation.duration = params.duration
|
||||
animation.fromValue = 0
|
||||
animation.toValue = 1
|
||||
imageView?.layer?.add(animation, forKey: "imageTransition")
|
||||
|
||||
imageView?.display(image: image)
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif
|
||||
620
Pods/Nuke/Sources/Internal.swift
generated
Normal file
@@ -0,0 +1,620 @@
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) 2015-2019 Alexander Grebenyuk (github.com/kean).
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - Lock
|
||||
|
||||
extension NSLock {
|
||||
func sync<T>(_ closure: () -> T) -> T {
|
||||
lock(); defer { unlock() }
|
||||
return closure()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - RateLimiter
|
||||
|
||||
/// Controls the rate at which the work is executed. Uses the classic [token
|
||||
/// bucket](https://en.wikipedia.org/wiki/Token_bucket) algorithm.
|
||||
///
|
||||
/// The main use case for rate limiter is to support large (infinite) collections
|
||||
/// of images by preventing trashing of underlying systems, primary URLSession.
|
||||
///
|
||||
/// The implementation supports quick bursts of requests which can be executed
|
||||
/// without any delays when "the bucket is full". This is important to prevent
|
||||
/// rate limiter from affecting "normal" requests flow.
|
||||
internal final class RateLimiter {
|
||||
private let bucket: TokenBucket
|
||||
private let queue: DispatchQueue
|
||||
private var pending = LinkedList<Task>() // fast append, fast remove first
|
||||
private var isExecutingPendingTasks = false
|
||||
|
||||
private typealias Task = (CancellationToken, () -> Void)
|
||||
|
||||
/// Initializes the `RateLimiter` with the given configuration.
|
||||
/// - parameter queue: Queue on which to execute pending tasks.
|
||||
/// - parameter rate: Maximum number of requests per second. 80 by default.
|
||||
/// - parameter burst: Maximum number of requests which can be executed without
|
||||
/// any delays when "bucket is full". 25 by default.
|
||||
init(queue: DispatchQueue, rate: Int = 80, burst: Int = 25) {
|
||||
self.queue = queue
|
||||
self.bucket = TokenBucket(rate: Double(rate), burst: Double(burst))
|
||||
}
|
||||
|
||||
func execute(token: CancellationToken, _ closure: @escaping () -> Void) {
|
||||
let task = Task(token, closure)
|
||||
if !pending.isEmpty || !_execute(task) {
|
||||
pending.append(task)
|
||||
_setNeedsExecutePendingTasks()
|
||||
}
|
||||
}
|
||||
|
||||
private func _execute(_ task: Task) -> Bool {
|
||||
guard !task.0.isCancelling else {
|
||||
return true // No need to execute
|
||||
}
|
||||
return bucket.execute(task.1)
|
||||
}
|
||||
|
||||
private func _setNeedsExecutePendingTasks() {
|
||||
guard !isExecutingPendingTasks else { return }
|
||||
isExecutingPendingTasks = true
|
||||
// Compute a delay such that by the time the closure is executed the
|
||||
// bucket is refilled to a point that is able to execute at least one
|
||||
// pending task. With a rate of 100 tasks we expect a refill every 10 ms.
|
||||
let delay = Int(1.15 * (1000 / bucket.rate)) // 14 ms for rate 80 (default)
|
||||
let bounds = max(100, min(5, delay)) // Make the delay is reasonable
|
||||
queue.asyncAfter(deadline: .now() + .milliseconds(bounds), execute: _executePendingTasks)
|
||||
}
|
||||
|
||||
private func _executePendingTasks() {
|
||||
while let node = pending.first, _execute(node.value) {
|
||||
pending.remove(node)
|
||||
}
|
||||
isExecutingPendingTasks = false
|
||||
if !pending.isEmpty { // Not all pending items were executed
|
||||
_setNeedsExecutePendingTasks()
|
||||
}
|
||||
}
|
||||
|
||||
private final class TokenBucket {
|
||||
let rate: Double
|
||||
private let burst: Double // maximum bucket size
|
||||
private var bucket: Double
|
||||
private var timestamp: TimeInterval // last refill timestamp
|
||||
|
||||
/// - parameter rate: Rate (tokens/second) at which bucket is refilled.
|
||||
/// - parameter burst: Bucket size (maximum number of tokens).
|
||||
init(rate: Double, burst: Double) {
|
||||
self.rate = rate
|
||||
self.burst = burst
|
||||
self.bucket = burst
|
||||
self.timestamp = CFAbsoluteTimeGetCurrent()
|
||||
}
|
||||
|
||||
/// Returns `true` if the closure was executed, `false` if dropped.
|
||||
func execute(_ closure: () -> Void) -> Bool {
|
||||
refill()
|
||||
guard bucket >= 1.0 else {
|
||||
return false // bucket is empty
|
||||
}
|
||||
bucket -= 1.0
|
||||
closure()
|
||||
return true
|
||||
}
|
||||
|
||||
private func refill() {
|
||||
let now = CFAbsoluteTimeGetCurrent()
|
||||
bucket += rate * max(0, now - timestamp) // rate * (time delta)
|
||||
timestamp = now
|
||||
if bucket > burst { // prevent bucket overflow
|
||||
bucket = burst
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Operation
|
||||
|
||||
internal final class Operation: Foundation.Operation {
|
||||
private var _isExecuting = false
|
||||
private var _isFinished = false
|
||||
private var isFinishCalled = Atomic(false)
|
||||
|
||||
override var isExecuting: Bool {
|
||||
set {
|
||||
guard _isExecuting != newValue else {
|
||||
fatalError("Invalid state, operation is already (not) executing")
|
||||
}
|
||||
willChangeValue(forKey: "isExecuting")
|
||||
_isExecuting = newValue
|
||||
didChangeValue(forKey: "isExecuting")
|
||||
}
|
||||
get {
|
||||
return _isExecuting
|
||||
}
|
||||
}
|
||||
override var isFinished: Bool {
|
||||
set {
|
||||
guard !_isFinished else {
|
||||
fatalError("Invalid state, operation is already finished")
|
||||
}
|
||||
willChangeValue(forKey: "isFinished")
|
||||
_isFinished = newValue
|
||||
didChangeValue(forKey: "isFinished")
|
||||
}
|
||||
get {
|
||||
return _isFinished
|
||||
}
|
||||
}
|
||||
|
||||
typealias Starter = (_ finish: @escaping () -> Void) -> Void
|
||||
private let starter: Starter
|
||||
|
||||
init(starter: @escaping Starter) {
|
||||
self.starter = starter
|
||||
}
|
||||
|
||||
override func start() {
|
||||
guard !isCancelled else {
|
||||
isFinished = true
|
||||
return
|
||||
}
|
||||
isExecuting = true
|
||||
starter { [weak self] in
|
||||
self?._finish()
|
||||
}
|
||||
}
|
||||
|
||||
private func _finish() {
|
||||
// Make sure that we ignore if `finish` is called more than once.
|
||||
if isFinishCalled.swap(to: true, ifEqual: false) {
|
||||
isExecuting = false
|
||||
isFinished = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - LinkedList
|
||||
|
||||
/// A doubly linked list.
|
||||
internal final class LinkedList<Element> {
|
||||
// first <-> node <-> ... <-> last
|
||||
private(set) var first: Node?
|
||||
private(set) var last: Node?
|
||||
|
||||
deinit {
|
||||
removeAll()
|
||||
}
|
||||
|
||||
var isEmpty: Bool {
|
||||
return last == nil
|
||||
}
|
||||
|
||||
/// Adds an element to the end of the list.
|
||||
@discardableResult
|
||||
func append(_ element: Element) -> Node {
|
||||
let node = Node(value: element)
|
||||
append(node)
|
||||
return node
|
||||
}
|
||||
|
||||
/// Adds a node to the end of the list.
|
||||
func append(_ node: Node) {
|
||||
if let last = last {
|
||||
last.next = node
|
||||
node.previous = last
|
||||
self.last = node
|
||||
} else {
|
||||
last = node
|
||||
first = node
|
||||
}
|
||||
}
|
||||
|
||||
func remove(_ node: Node) {
|
||||
node.next?.previous = node.previous // node.previous is nil if node=first
|
||||
node.previous?.next = node.next // node.next is nil if node=last
|
||||
if node === last {
|
||||
last = node.previous
|
||||
}
|
||||
if node === first {
|
||||
first = node.next
|
||||
}
|
||||
node.next = nil
|
||||
node.previous = nil
|
||||
}
|
||||
|
||||
func removeAll() {
|
||||
// avoid recursive Nodes deallocation
|
||||
var node = first
|
||||
while let next = node?.next {
|
||||
node?.next = nil
|
||||
next.previous = nil
|
||||
node = next
|
||||
}
|
||||
last = nil
|
||||
first = nil
|
||||
}
|
||||
|
||||
final class Node {
|
||||
let value: Element
|
||||
fileprivate var next: Node?
|
||||
fileprivate var previous: Node?
|
||||
|
||||
init(value: Element) {
|
||||
self.value = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CancellationTokenSource
|
||||
|
||||
/// Manages cancellation tokens and signals them when cancellation is requested.
|
||||
///
|
||||
/// All `CancellationTokenSource` methods are thread safe.
|
||||
internal final class CancellationTokenSource {
|
||||
/// Returns `true` if cancellation has been requested.
|
||||
var isCancelling: Bool {
|
||||
return lock.sync { observers == nil }
|
||||
}
|
||||
|
||||
/// Creates a new token associated with the source.
|
||||
var token: CancellationToken {
|
||||
return CancellationToken(source: self)
|
||||
}
|
||||
|
||||
private var lock = NSLock()
|
||||
private var observers: [() -> Void]? = []
|
||||
|
||||
/// Initializes the `CancellationTokenSource` instance.
|
||||
init() {}
|
||||
|
||||
fileprivate func register(_ closure: @escaping () -> Void) {
|
||||
if !_register(closure) {
|
||||
closure()
|
||||
}
|
||||
}
|
||||
|
||||
private func _register(_ closure: @escaping () -> Void) -> Bool {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
|
||||
observers?.append(closure)
|
||||
return observers != nil
|
||||
}
|
||||
|
||||
/// Communicates a request for cancellation to the managed tokens.
|
||||
func cancel() {
|
||||
if let observers = _cancel() {
|
||||
observers.forEach { $0() }
|
||||
}
|
||||
}
|
||||
|
||||
private func _cancel() -> [() -> Void]? {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
|
||||
let observers = self.observers
|
||||
self.observers = nil // transition to `isCancelling` state
|
||||
return observers
|
||||
}
|
||||
}
|
||||
|
||||
/// Enables cooperative cancellation of operations.
|
||||
///
|
||||
/// You create a cancellation token by instantiating a `CancellationTokenSource`
|
||||
/// object and calling its `token` property. You then pass the token to any
|
||||
/// number of threads, tasks, or operations that should receive notice of
|
||||
/// cancellation. When the owning object calls `cancel()`, the `isCancelling`
|
||||
/// property on every copy of the cancellation token is set to `true`.
|
||||
/// The registered objects can respond in whatever manner is appropriate.
|
||||
///
|
||||
/// All `CancellationToken` methods are thread safe.
|
||||
internal struct CancellationToken {
|
||||
fileprivate let source: CancellationTokenSource? // no-op when `nil`
|
||||
|
||||
/// Returns `true` if cancellation has been requested for this token.
|
||||
/// Returns `false` if the source was deallocated.
|
||||
var isCancelling: Bool {
|
||||
return source?.isCancelling ?? false
|
||||
}
|
||||
|
||||
/// Registers the closure that will be called when the token is canceled.
|
||||
/// If this token is already cancelled, the closure will be run immediately
|
||||
/// and synchronously.
|
||||
func register(_ closure: @escaping () -> Void) {
|
||||
source?.register(closure)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ResumableData
|
||||
|
||||
/// Resumable data support. For more info see:
|
||||
/// - https://developer.apple.com/library/content/qa/qa1761/_index.html
|
||||
internal struct ResumableData {
|
||||
let data: Data
|
||||
let validator: String // Either Last-Modified or ETag
|
||||
|
||||
init?(response: URLResponse, data: Data) {
|
||||
// Check if "Accept-Ranges" is present and the response is valid.
|
||||
guard !data.isEmpty,
|
||||
let response = response as? HTTPURLResponse,
|
||||
response.statusCode == 200 /* OK */ || response.statusCode == 206, /* Partial Content */
|
||||
let acceptRanges = response.allHeaderFields["Accept-Ranges"] as? String,
|
||||
acceptRanges.lowercased() == "bytes",
|
||||
let validator = ResumableData._validator(from: response) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// NOTE: https://developer.apple.com/documentation/foundation/httpurlresponse/1417930-allheaderfields
|
||||
// HTTP headers are case insensitive. To simplify your code, certain
|
||||
// header field names are canonicalized into their standard form.
|
||||
// For example, if the server sends a content-length header,
|
||||
// it is automatically adjusted to be Content-Length.
|
||||
|
||||
self.data = data; self.validator = validator
|
||||
}
|
||||
|
||||
private static func _validator(from response: HTTPURLResponse) -> String? {
|
||||
if let entityTag = response.allHeaderFields["ETag"] as? String {
|
||||
return entityTag // Prefer ETag
|
||||
}
|
||||
// There seems to be a bug with ETag where HTTPURLResponse would canonicalize
|
||||
// it to Etag instead of ETag
|
||||
// https://bugs.swift.org/browse/SR-2429
|
||||
if let entityTag = response.allHeaderFields["Etag"] as? String {
|
||||
return entityTag // Prefer ETag
|
||||
}
|
||||
if let lastModified = response.allHeaderFields["Last-Modified"] as? String {
|
||||
return lastModified
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func resume(request: inout URLRequest) {
|
||||
var headers = request.allHTTPHeaderFields ?? [:]
|
||||
// "bytes=1000-" means bytes from 1000 up to the end (inclusive)
|
||||
headers["Range"] = "bytes=\(data.count)-"
|
||||
headers["If-Range"] = validator
|
||||
request.allHTTPHeaderFields = headers
|
||||
}
|
||||
|
||||
// Check if the server decided to resume the response.
|
||||
static func isResumedResponse(_ response: URLResponse) -> Bool {
|
||||
// "206 Partial Content" (server accepted "If-Range")
|
||||
return (response as? HTTPURLResponse)?.statusCode == 206
|
||||
}
|
||||
|
||||
// MARK: Storing Resumable Data
|
||||
|
||||
/// Shared between multiple pipelines. Thread safe. In the future version we
|
||||
/// might feature more customization options.
|
||||
static var _cache = _Cache<String, ResumableData>(costLimit: 32 * 1024 * 1024, countLimit: 100) // internal only for testing purposes
|
||||
|
||||
static func removeResumableData(for request: URLRequest) -> ResumableData? {
|
||||
guard let url = request.url?.absoluteString else { return nil }
|
||||
return _cache.removeValue(forKey: url)
|
||||
}
|
||||
|
||||
static func storeResumableData(_ data: ResumableData, for request: URLRequest) {
|
||||
guard let url = request.url?.absoluteString else { return }
|
||||
_cache.set(data, forKey: url, cost: data.data.count)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Printer
|
||||
|
||||
/// Helper type for printing nice debug descriptions.
|
||||
internal struct Printer {
|
||||
private(set) internal var _out = String()
|
||||
|
||||
private let timelineFormatter: DateFormatter
|
||||
|
||||
init(_ string: String = "") {
|
||||
self._out = string
|
||||
|
||||
timelineFormatter = DateFormatter()
|
||||
timelineFormatter.dateFormat = "HH:mm:ss.SSS"
|
||||
}
|
||||
|
||||
func output(indent: Int = 0) -> String {
|
||||
return _out.components(separatedBy: .newlines)
|
||||
.map { $0.isEmpty ? "" : String(repeating: " ", count: indent) + $0 }
|
||||
.joined(separator: "\n")
|
||||
}
|
||||
|
||||
mutating func string(_ str: String) {
|
||||
_out.append(str)
|
||||
}
|
||||
|
||||
mutating func line(_ str: String) {
|
||||
_out.append(str)
|
||||
_out.append("\n")
|
||||
}
|
||||
|
||||
mutating func value(_ key: String, _ value: CustomStringConvertible?) {
|
||||
let val = value.map { String(describing: $0) }
|
||||
line(key + " - " + (val ?? "nil"))
|
||||
}
|
||||
|
||||
/// For producting nicely formatted timelines like this:
|
||||
///
|
||||
/// 11:45:52.737 - Data Loading Start Date
|
||||
/// 11:45:52.739 - Data Loading End Date
|
||||
/// nil - Decoding Start Date
|
||||
mutating func timeline(_ key: String, _ date: Date?) {
|
||||
let value = date.map { timelineFormatter.string(from: $0) }
|
||||
self.value((value ?? "nil "), key) // Swtich key with value
|
||||
}
|
||||
|
||||
mutating func timeline(_ key: String, _ start: Date?, _ end: Date?, isReversed: Bool = true) {
|
||||
let duration = _duration(from: start, to: end)
|
||||
let value = "\(_string(from: start)) – \(_string(from: end)) (\(duration))"
|
||||
if isReversed {
|
||||
self.value(value.padding(toLength: 36, withPad: " ", startingAt: 0), key)
|
||||
} else {
|
||||
self.value(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
mutating func section(title: String, _ closure: (inout Printer) -> Void) {
|
||||
_out.append(contentsOf: title)
|
||||
_out.append(" {\n")
|
||||
var printer = Printer()
|
||||
closure(&printer)
|
||||
_out.append(printer.output(indent: 4))
|
||||
_out.append("}\n")
|
||||
}
|
||||
|
||||
// MARK: Formatters
|
||||
|
||||
private func _string(from date: Date?) -> String {
|
||||
return date.map { timelineFormatter.string(from: $0) } ?? "nil"
|
||||
}
|
||||
|
||||
private func _duration(from: Date?, to: Date?) -> String {
|
||||
guard let from = from else { return "nil" }
|
||||
guard let to = to else { return "unknown" }
|
||||
return Printer.duration(to.timeIntervalSince(from)) ?? "nil"
|
||||
}
|
||||
|
||||
static func duration(_ duration: TimeInterval?) -> String? {
|
||||
guard let duration = duration else { return nil }
|
||||
|
||||
let m: Int = Int(duration) / 60
|
||||
let s: Int = Int(duration) % 60
|
||||
let ms: Int = Int(duration * 1000) % 1000
|
||||
|
||||
var output = String()
|
||||
if m > 0 { output.append("\(m):") }
|
||||
output.append(output.isEmpty ? "\(s)." : String(format: "%02d.", s))
|
||||
output.append(String(format: "%03ds", ms))
|
||||
return output
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Misc
|
||||
|
||||
struct TaskMetrics {
|
||||
var startDate: Date? = nil
|
||||
var endDate: Date? = nil
|
||||
|
||||
static func started() -> TaskMetrics {
|
||||
var metrics = TaskMetrics()
|
||||
metrics.start()
|
||||
return metrics
|
||||
}
|
||||
|
||||
mutating func start() {
|
||||
startDate = Date()
|
||||
}
|
||||
|
||||
mutating func end() {
|
||||
endDate = Date()
|
||||
}
|
||||
}
|
||||
|
||||
/// A simple observable property. Not thread safe.
|
||||
final class Property<T> {
|
||||
var value: T {
|
||||
didSet {
|
||||
for observer in observers {
|
||||
observer(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(value: T) {
|
||||
self.value = value
|
||||
}
|
||||
|
||||
private var observers = [(T) -> Void]()
|
||||
|
||||
// For our use-cases we can just ignore unsubscribing for now.
|
||||
func observe(_ closure: @escaping (T) -> Void) {
|
||||
observers.append(closure)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Atomic
|
||||
|
||||
/// A thread-safe value wrapper.
|
||||
final class Atomic<T> {
|
||||
private var _value: T
|
||||
private let lock = NSLock()
|
||||
|
||||
init(_ value: T) {
|
||||
self._value = value
|
||||
}
|
||||
|
||||
var value: T {
|
||||
get {
|
||||
lock.lock()
|
||||
let value = _value
|
||||
lock.unlock()
|
||||
return value
|
||||
}
|
||||
set {
|
||||
lock.lock()
|
||||
_value = newValue
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Atomic where T: Equatable {
|
||||
/// "Compare and Swap"
|
||||
func swap(to newValue: T, ifEqual oldValue: T) -> Bool {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
|
||||
guard _value == oldValue else {
|
||||
return false
|
||||
}
|
||||
_value = newValue
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
extension Atomic where T == Int {
|
||||
/// Atomically increments the value and retruns a new incremented value.
|
||||
func increment() -> Int {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
|
||||
_value += 1
|
||||
return _value
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Misc
|
||||
|
||||
import CommonCrypto
|
||||
|
||||
extension String {
|
||||
/// Calculates SHA1 from the given string and returns its hex representation.
|
||||
///
|
||||
/// ```swift
|
||||
/// print("http://test.com".sha1)
|
||||
/// // prints "50334ee0b51600df6397ce93ceed4728c37fee4e"
|
||||
/// ```
|
||||
var sha1: String? {
|
||||
guard let input = self.data(using: .utf8) else { return nil }
|
||||
|
||||
#if swift(>=5.0)
|
||||
let hash = input.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) -> [UInt8] in
|
||||
var hash = [UInt8](repeating: 0, count: Int(CC_SHA1_DIGEST_LENGTH))
|
||||
CC_SHA1(bytes.baseAddress, CC_LONG(input.count), &hash)
|
||||
return hash
|
||||
}
|
||||
#else
|
||||
var hash = [UInt8](repeating: 0, count: Int(CC_SHA1_DIGEST_LENGTH))
|
||||
input.withUnsafeBytes {
|
||||
_ = CC_SHA1($0, CC_LONG(input.count), &hash)
|
||||
}
|
||||
#endif
|
||||
|
||||
return hash.map({ String(format: "%02x", $0) }).joined()
|
||||
}
|
||||
}
|
||||
473
Pods/Pods.xcodeproj/project.pbxproj
generated
@@ -7,54 +7,101 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
5A8E10AA29B392DAE441976836D393A5 /* KeychainAccess-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = DC95F84ADCC7EA0D937C9F7E1A13FD3F /* KeychainAccess-dummy.m */; };
|
||||
84799F206FB756858DA1341F7AEC702F /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1A8D4AA4DF9DABA6B3E0870FA4FF211 /* Keychain.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; };
|
||||
853F25B704B9C1E0728C8355490C3E8B /* Pods-AltStore-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = F5B40162929DC081B191D728E6A63F10 /* Pods-AltStore-dummy.m */; };
|
||||
8C461642DA35C9D0EE56FAC4A9B7EB98 /* Pods-AltStore-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = A6C251E1905FCD10CB7FAFE104093724 /* Pods-AltStore-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
92B9A2B19BE39566C6CACFD26BF134C4 /* KeychainAccess-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = C9FE5B73BA33889545E29DB12D4DF6CF /* KeychainAccess-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
E508379E4792F37F65760D68B758E547 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CB4607EFCA7C5F75397649E792E2AFCB /* Foundation.framework */; };
|
||||
0A18CFC96BA1E3D7DF11145FABF1E64E /* DataLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73E94415756FCFA45AAABA95247C470D /* DataLoader.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; };
|
||||
37EF20140753D50E70CF77C62F3728CF /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CB4607EFCA7C5F75397649E792E2AFCB /* Foundation.framework */; };
|
||||
4BC2A440F56B2681B7CE91EE8EBFC894 /* Internal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66E0693BEDE8CE4AD731BBDA0CAF76B1 /* Internal.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; };
|
||||
57E7588C7E69A7D306360A22C9D6FF5E /* Pods-AltStore-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = F5B40162929DC081B191D728E6A63F10 /* Pods-AltStore-dummy.m */; };
|
||||
5A8E10AA29B392DAE441976836D393A5 /* KeychainAccess-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 30E66D199ED15E5478F5AC21DC53D423 /* KeychainAccess-dummy.m */; };
|
||||
5C563C8FC51EFC2FF0A59C6CD5BE439E /* Pods-AltStore-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = A6C251E1905FCD10CB7FAFE104093724 /* Pods-AltStore-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
5D68B76EC70C7CD88977C7BBA5C47B1A /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0129EC06D5726BC1481C14706F422AAB /* ImageCache.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; };
|
||||
84799F206FB756858DA1341F7AEC702F /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = A43A8A22652762605F745962AD523D98 /* Keychain.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; };
|
||||
879AADFC0E22BDB7716910D0A3369C77 /* ImageTaskMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFB8C86DF1618F0847C64F795ED481DC /* ImageTaskMetrics.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; };
|
||||
87A66BE797FE126185C9FEB113AD1AAE /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB78EC364D47B2F846FE295F4FF03748 /* ImageView.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; };
|
||||
8D6722BD6426F3E66CBD894A443832CD /* Nuke-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = C8760B598757239C1268DD0FFA5F25A8 /* Nuke-dummy.m */; };
|
||||
92B9A2B19BE39566C6CACFD26BF134C4 /* KeychainAccess-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 21C9EC20F1FC5E9BEC2FE69D1708FA1F /* KeychainAccess-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
96F79411300DB3B1280948E2B165AA03 /* ImageProcessing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41B96790E373A18127130405D5122A95 /* ImageProcessing.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; };
|
||||
9B3853F97F020FD13063BBA1688D52FE /* ImageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80200203D381988E2B915E5AD52585C2 /* ImageRequest.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; };
|
||||
B84369E539EDE5956657E2ED55F9FA47 /* Nuke-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 3279D6493D2AA6C8B6E3E2AED8FBD5BD /* Nuke-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
BBC205B18AC4F860384AA48EB62E3151 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CB4607EFCA7C5F75397649E792E2AFCB /* Foundation.framework */; };
|
||||
C19654E5869F26968FE04867EBE0AA93 /* ImagePipeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 930F6A819392907A5BB85CC4B725EA47 /* ImagePipeline.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; };
|
||||
CF920935C6D30430A43066B25CC006B1 /* ImageDecoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 120DDD33C8844BEE1EC96C4C9CD2E821 /* ImageDecoding.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; };
|
||||
D1AF4602637FA0CCD336D08A60CA142D /* DataCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A00C297DE9E8B599AD9F1EEF46467B /* DataCache.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; };
|
||||
E8395200A45B1D891CABD0675CDFEB7B /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CB4607EFCA7C5F75397649E792E2AFCB /* Foundation.framework */; };
|
||||
FA2127BC3DA61718740A7405E7BBE85E /* ImagePreheater.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE53493439E4F70CF5112624095A1D9B /* ImagePreheater.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
9FFDB3D153C5FA18B6FD59B50C54CAAD /* PBXContainerItemProxy */ = {
|
||||
3895BFEFA1F8BAB69DF11C154E9DA338 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 3F895CBA524B654D3F837F25BFBE2262;
|
||||
remoteInfo = KeychainAccess;
|
||||
};
|
||||
C08AA1ABDB4BA9BB2BE4BBD9106AA301 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = A6293B46682B3506C97B73C333967DDB;
|
||||
remoteInfo = Nuke;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
0129EC06D5726BC1481C14706F422AAB /* ImageCache.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageCache.swift; path = Sources/ImageCache.swift; sourceTree = "<group>"; };
|
||||
0C9645075E504CEEC1C7CAACE8C8EA53 /* Pods_AltStore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = Pods_AltStore.framework; path = "Pods-AltStore.framework"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
120DDD33C8844BEE1EC96C4C9CD2E821 /* ImageDecoding.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageDecoding.swift; path = Sources/ImageDecoding.swift; sourceTree = "<group>"; };
|
||||
12FBD321A774CE97AC1213C6B240DC9F /* Pods-AltStore.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-AltStore.release.xcconfig"; sourceTree = "<group>"; };
|
||||
184E7D8A7431E86209F239AF275FB5BC /* KeychainAccess-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "KeychainAccess-prefix.pch"; sourceTree = "<group>"; };
|
||||
21C9EC20F1FC5E9BEC2FE69D1708FA1F /* KeychainAccess-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "KeychainAccess-umbrella.h"; sourceTree = "<group>"; };
|
||||
2328FF2674C8B599E2BB09D8C69B279D /* Nuke-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Nuke-Info.plist"; sourceTree = "<group>"; };
|
||||
26B0AFC87F31102D2BE23BCC938B1223 /* Pods-AltStore-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-AltStore-acknowledgements.plist"; sourceTree = "<group>"; };
|
||||
300ED185B63C7F43AB741EBBE581DAF2 /* Nuke.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = Nuke.modulemap; sourceTree = "<group>"; };
|
||||
30E66D199ED15E5478F5AC21DC53D423 /* KeychainAccess-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "KeychainAccess-dummy.m"; sourceTree = "<group>"; };
|
||||
3279D6493D2AA6C8B6E3E2AED8FBD5BD /* Nuke-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Nuke-umbrella.h"; sourceTree = "<group>"; };
|
||||
381EB86F7712B890E418E54C20FB51E3 /* Pods-AltStore-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-AltStore-Info.plist"; sourceTree = "<group>"; };
|
||||
38D1B55A07DBA5790862EE8B2E01C5D1 /* Nuke.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = Nuke.xcconfig; sourceTree = "<group>"; };
|
||||
41B96790E373A18127130405D5122A95 /* ImageProcessing.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageProcessing.swift; path = Sources/ImageProcessing.swift; sourceTree = "<group>"; };
|
||||
4D6A329382B92AAA01398C8A3C5DD70D /* KeychainAccess.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = KeychainAccess.xcconfig; sourceTree = "<group>"; };
|
||||
4DBA4D739ECCF9E52DA7C1AFF75568B0 /* Pods-AltStore.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-AltStore.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
5EA1AF6D01DBBAE8075A59F88B649449 /* KeychainAccess.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = KeychainAccess.framework; path = KeychainAccess.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
70C1D1D165319C9CE56A3D43DDFBF1DC /* KeychainAccess-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "KeychainAccess-Info.plist"; sourceTree = "<group>"; };
|
||||
87BB091C7209D9D07DFA5FFB06428B65 /* KeychainAccess.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = KeychainAccess.xcconfig; sourceTree = "<group>"; };
|
||||
6264F18955EC17CB030280BE362255F7 /* Nuke-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Nuke-prefix.pch"; sourceTree = "<group>"; };
|
||||
66E0693BEDE8CE4AD731BBDA0CAF76B1 /* Internal.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Internal.swift; path = Sources/Internal.swift; sourceTree = "<group>"; };
|
||||
6AB6967B2AEBE949A35F74CD4CC72B2B /* Nuke.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = Nuke.framework; path = Nuke.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
73E94415756FCFA45AAABA95247C470D /* DataLoader.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DataLoader.swift; path = Sources/DataLoader.swift; sourceTree = "<group>"; };
|
||||
7516850C0456BB0775EA8037DA2021E6 /* KeychainAccess.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = KeychainAccess.modulemap; sourceTree = "<group>"; };
|
||||
80200203D381988E2B915E5AD52585C2 /* ImageRequest.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageRequest.swift; path = Sources/ImageRequest.swift; sourceTree = "<group>"; };
|
||||
930F6A819392907A5BB85CC4B725EA47 /* ImagePipeline.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImagePipeline.swift; path = Sources/ImagePipeline.swift; sourceTree = "<group>"; };
|
||||
96AD8FCF302BCD7BCBBEE0FEB1F64ACE /* Pods-AltStore-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-AltStore-acknowledgements.markdown"; sourceTree = "<group>"; };
|
||||
9D940727FF8FB9C785EB98E56350EF41 /* Podfile */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; indentWidth = 2; lastKnownFileType = text; name = Podfile; path = ../Podfile; sourceTree = SOURCE_ROOT; tabWidth = 2; xcLanguageSpecificationIdentifier = xcode.lang.ruby; };
|
||||
A43A8A22652762605F745962AD523D98 /* Keychain.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Keychain.swift; path = Lib/KeychainAccess/Keychain.swift; sourceTree = "<group>"; };
|
||||
A6C251E1905FCD10CB7FAFE104093724 /* Pods-AltStore-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-AltStore-umbrella.h"; sourceTree = "<group>"; };
|
||||
B329E578A66D5B034937A60730C4AEA7 /* KeychainAccess.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = KeychainAccess.modulemap; sourceTree = "<group>"; };
|
||||
B38C6112DA4290AC0E677628F897988F /* Pods-AltStore.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = "Pods-AltStore.modulemap"; sourceTree = "<group>"; };
|
||||
BD6733F0EF9D699F0FFB110363BB951D /* Pods-AltStore-frameworks.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-AltStore-frameworks.sh"; sourceTree = "<group>"; };
|
||||
C1A8D4AA4DF9DABA6B3E0870FA4FF211 /* Keychain.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Keychain.swift; path = Lib/KeychainAccess/Keychain.swift; sourceTree = "<group>"; };
|
||||
C6FE0CABF5936907F3B06C5EC42CC92A /* KeychainAccess-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "KeychainAccess-prefix.pch"; sourceTree = "<group>"; };
|
||||
C9FE5B73BA33889545E29DB12D4DF6CF /* KeychainAccess-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "KeychainAccess-umbrella.h"; sourceTree = "<group>"; };
|
||||
C8760B598757239C1268DD0FFA5F25A8 /* Nuke-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Nuke-dummy.m"; sourceTree = "<group>"; };
|
||||
CB4607EFCA7C5F75397649E792E2AFCB /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; };
|
||||
DC95F84ADCC7EA0D937C9F7E1A13FD3F /* KeychainAccess-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "KeychainAccess-dummy.m"; sourceTree = "<group>"; };
|
||||
F1AAD8D354A54F78205D3DC08E311FA3 /* Pods_AltStore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = Pods_AltStore.framework; path = "Pods-AltStore.framework"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
CE53493439E4F70CF5112624095A1D9B /* ImagePreheater.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImagePreheater.swift; path = Sources/ImagePreheater.swift; sourceTree = "<group>"; };
|
||||
CE86F606176D6FB0C3EE9D15055C3449 /* KeychainAccess.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = KeychainAccess.framework; path = KeychainAccess.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D1A00C297DE9E8B599AD9F1EEF46467B /* DataCache.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DataCache.swift; path = Sources/DataCache.swift; sourceTree = "<group>"; };
|
||||
DB78EC364D47B2F846FE295F4FF03748 /* ImageView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageView.swift; path = Sources/ImageView.swift; sourceTree = "<group>"; };
|
||||
E9852793F007CB834DA358B4D47C32C1 /* KeychainAccess-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "KeychainAccess-Info.plist"; sourceTree = "<group>"; };
|
||||
F5B40162929DC081B191D728E6A63F10 /* Pods-AltStore-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-AltStore-dummy.m"; sourceTree = "<group>"; };
|
||||
FFB8C86DF1618F0847C64F795ED481DC /* ImageTaskMetrics.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageTaskMetrics.swift; path = Sources/ImageTaskMetrics.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
97AFD54D8F68D9249D86AB9692333E16 /* Frameworks */ = {
|
||||
ABDA17563D8ADDC2F0C97AEEC1190FF4 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E508379E4792F37F65760D68B758E547 /* Foundation.framework in Frameworks */,
|
||||
37EF20140753D50E70CF77C62F3728CF /* Foundation.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
DF4A1E02828950F7A135CEFD372F8141 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
BBC205B18AC4F860384AA48EB62E3151 /* Foundation.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -69,19 +116,21 @@
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
1822CF174BA8C3BC1C6A1BCD70C74D9E /* Pods */ = {
|
||||
21AFD5ACB5E55904BC69293508B530A1 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
21FB2FA785D073B5E18E48DDEE0CE582 /* KeychainAccess */,
|
||||
CE86F606176D6FB0C3EE9D15055C3449 /* KeychainAccess.framework */,
|
||||
6AB6967B2AEBE949A35F74CD4CC72B2B /* Nuke.framework */,
|
||||
0C9645075E504CEEC1C7CAACE8C8EA53 /* Pods_AltStore.framework */,
|
||||
);
|
||||
name = Pods;
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
21FB2FA785D073B5E18E48DDEE0CE582 /* KeychainAccess */ = {
|
||||
2B2AE941DB406AD8A970C81548FC9AAF /* KeychainAccess */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C1A8D4AA4DF9DABA6B3E0870FA4FF211 /* Keychain.swift */,
|
||||
B15474ABEC92ADDC00515B28B5193997 /* Support Files */,
|
||||
A43A8A22652762605F745962AD523D98 /* Keychain.swift */,
|
||||
36EE934D53885C39396738E704938F81 /* Support Files */,
|
||||
);
|
||||
name = KeychainAccess;
|
||||
path = KeychainAccess;
|
||||
@@ -104,6 +153,49 @@
|
||||
path = "Target Support Files/Pods-AltStore";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
3295775C592A786F74EE2052B6E3051C /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2B2AE941DB406AD8A970C81548FC9AAF /* KeychainAccess */,
|
||||
774F9F24FAF6CE3286E3A242DB35F8F9 /* Nuke */,
|
||||
);
|
||||
name = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
36EE934D53885C39396738E704938F81 /* Support Files */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7516850C0456BB0775EA8037DA2021E6 /* KeychainAccess.modulemap */,
|
||||
4D6A329382B92AAA01398C8A3C5DD70D /* KeychainAccess.xcconfig */,
|
||||
30E66D199ED15E5478F5AC21DC53D423 /* KeychainAccess-dummy.m */,
|
||||
E9852793F007CB834DA358B4D47C32C1 /* KeychainAccess-Info.plist */,
|
||||
184E7D8A7431E86209F239AF275FB5BC /* KeychainAccess-prefix.pch */,
|
||||
21C9EC20F1FC5E9BEC2FE69D1708FA1F /* KeychainAccess-umbrella.h */,
|
||||
);
|
||||
name = "Support Files";
|
||||
path = "../Target Support Files/KeychainAccess";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
774F9F24FAF6CE3286E3A242DB35F8F9 /* Nuke */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D1A00C297DE9E8B599AD9F1EEF46467B /* DataCache.swift */,
|
||||
73E94415756FCFA45AAABA95247C470D /* DataLoader.swift */,
|
||||
0129EC06D5726BC1481C14706F422AAB /* ImageCache.swift */,
|
||||
120DDD33C8844BEE1EC96C4C9CD2E821 /* ImageDecoding.swift */,
|
||||
930F6A819392907A5BB85CC4B725EA47 /* ImagePipeline.swift */,
|
||||
CE53493439E4F70CF5112624095A1D9B /* ImagePreheater.swift */,
|
||||
41B96790E373A18127130405D5122A95 /* ImageProcessing.swift */,
|
||||
80200203D381988E2B915E5AD52585C2 /* ImageRequest.swift */,
|
||||
FFB8C86DF1618F0847C64F795ED481DC /* ImageTaskMetrics.swift */,
|
||||
DB78EC364D47B2F846FE295F4FF03748 /* ImageView.swift */,
|
||||
66E0693BEDE8CE4AD731BBDA0CAF76B1 /* Internal.swift */,
|
||||
EB150DDBA5B5D2EDA82314D4C277EFD1 /* Support Files */,
|
||||
);
|
||||
name = Nuke;
|
||||
path = Nuke;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9B055D0CFEA43187E72B03DED11F5662 /* iOS */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -112,36 +204,13 @@
|
||||
name = iOS;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B15474ABEC92ADDC00515B28B5193997 /* Support Files */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B329E578A66D5B034937A60730C4AEA7 /* KeychainAccess.modulemap */,
|
||||
87BB091C7209D9D07DFA5FFB06428B65 /* KeychainAccess.xcconfig */,
|
||||
DC95F84ADCC7EA0D937C9F7E1A13FD3F /* KeychainAccess-dummy.m */,
|
||||
70C1D1D165319C9CE56A3D43DDFBF1DC /* KeychainAccess-Info.plist */,
|
||||
C6FE0CABF5936907F3B06C5EC42CC92A /* KeychainAccess-prefix.pch */,
|
||||
C9FE5B73BA33889545E29DB12D4DF6CF /* KeychainAccess-umbrella.h */,
|
||||
);
|
||||
name = "Support Files";
|
||||
path = "../Target Support Files/KeychainAccess";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C503C40C4D61FFD0A7542EA7A5BA8470 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5EA1AF6D01DBBAE8075A59F88B649449 /* KeychainAccess.framework */,
|
||||
F1AAD8D354A54F78205D3DC08E311FA3 /* Pods_AltStore.framework */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CF1408CF629C7361332E53B88F7BD30C = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9D940727FF8FB9C785EB98E56350EF41 /* Podfile */,
|
||||
D210D550F4EA176C3123ED886F8F87F5 /* Frameworks */,
|
||||
1822CF174BA8C3BC1C6A1BCD70C74D9E /* Pods */,
|
||||
C503C40C4D61FFD0A7542EA7A5BA8470 /* Products */,
|
||||
3295775C592A786F74EE2052B6E3051C /* Pods */,
|
||||
21AFD5ACB5E55904BC69293508B530A1 /* Products */,
|
||||
EF5F913657CC5FD110C6B562EDEABE4B /* Targets Support Files */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
@@ -154,6 +223,20 @@
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EB150DDBA5B5D2EDA82314D4C277EFD1 /* Support Files */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
300ED185B63C7F43AB741EBBE581DAF2 /* Nuke.modulemap */,
|
||||
38D1B55A07DBA5790862EE8B2E01C5D1 /* Nuke.xcconfig */,
|
||||
C8760B598757239C1268DD0FFA5F25A8 /* Nuke-dummy.m */,
|
||||
2328FF2674C8B599E2BB09D8C69B279D /* Nuke-Info.plist */,
|
||||
6264F18955EC17CB030280BE362255F7 /* Nuke-prefix.pch */,
|
||||
3279D6493D2AA6C8B6E3E2AED8FBD5BD /* Nuke-umbrella.h */,
|
||||
);
|
||||
name = "Support Files";
|
||||
path = "../Target Support Files/Nuke";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
EF5F913657CC5FD110C6B562EDEABE4B /* Targets Support Files */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -173,11 +256,19 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
511D4143282C493D1B0E8F20439A604C /* Headers */ = {
|
||||
65D299017BD89093B64A6AF1DF738157 /* Headers */ = {
|
||||
isa = PBXHeadersBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
8C461642DA35C9D0EE56FAC4A9B7EB98 /* Pods-AltStore-umbrella.h in Headers */,
|
||||
B84369E539EDE5956657E2ED55F9FA47 /* Nuke-umbrella.h in Headers */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
CA8BA33762FB1F287ACE7B5F5361361E /* Headers */ = {
|
||||
isa = PBXHeadersBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5C563C8FC51EFC2FF0A59C6CD5BE439E /* Pods-AltStore-umbrella.h in Headers */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -199,26 +290,45 @@
|
||||
);
|
||||
name = KeychainAccess;
|
||||
productName = KeychainAccess;
|
||||
productReference = 5EA1AF6D01DBBAE8075A59F88B649449 /* KeychainAccess.framework */;
|
||||
productReference = CE86F606176D6FB0C3EE9D15055C3449 /* KeychainAccess.framework */;
|
||||
productType = "com.apple.product-type.framework";
|
||||
};
|
||||
A24BCCF0615F19EF15C5175F5155059B /* Pods-AltStore */ = {
|
||||
A6293B46682B3506C97B73C333967DDB /* Nuke */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 9DCE5F54F9F65C3281FF75A7483961D6 /* Build configuration list for PBXNativeTarget "Pods-AltStore" */;
|
||||
buildConfigurationList = B7033C3F5480C9769E718E1F13B2716B /* Build configuration list for PBXNativeTarget "Nuke" */;
|
||||
buildPhases = (
|
||||
511D4143282C493D1B0E8F20439A604C /* Headers */,
|
||||
B416FEBA18E397F57E54B00750E42DD5 /* Sources */,
|
||||
97AFD54D8F68D9249D86AB9692333E16 /* Frameworks */,
|
||||
E76406DC9B6AA5D67A3AC649D0F24293 /* Resources */,
|
||||
65D299017BD89093B64A6AF1DF738157 /* Headers */,
|
||||
876A7D7DB025A5564DA9E3E075AF114A /* Sources */,
|
||||
ABDA17563D8ADDC2F0C97AEEC1190FF4 /* Frameworks */,
|
||||
B59C37B929FF009500F79859910E42D5 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
E524462E97292EC1913EB28B4E5A395E /* PBXTargetDependency */,
|
||||
);
|
||||
name = Nuke;
|
||||
productName = Nuke;
|
||||
productReference = 6AB6967B2AEBE949A35F74CD4CC72B2B /* Nuke.framework */;
|
||||
productType = "com.apple.product-type.framework";
|
||||
};
|
||||
ADDE9B18CB22BDBFE81CDA0BD4FDA590 /* Pods-AltStore */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = B8810C5C21F2ADE6A3B505A9C49E78A6 /* Build configuration list for PBXNativeTarget "Pods-AltStore" */;
|
||||
buildPhases = (
|
||||
CA8BA33762FB1F287ACE7B5F5361361E /* Headers */,
|
||||
3A900BB464741FA7A6022FAAD89DF071 /* Sources */,
|
||||
DF4A1E02828950F7A135CEFD372F8141 /* Frameworks */,
|
||||
468C740CFE588F51DAD34AF0BA3320F8 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
449A8AC2DD1D7034AC060D2E9EAB443C /* PBXTargetDependency */,
|
||||
701929F2CB443A5BF858E1C515F0F81A /* PBXTargetDependency */,
|
||||
);
|
||||
name = "Pods-AltStore";
|
||||
productName = "Pods-AltStore";
|
||||
productReference = F1AAD8D354A54F78205D3DC08E311FA3 /* Pods_AltStore.framework */;
|
||||
productReference = 0C9645075E504CEEC1C7CAACE8C8EA53 /* Pods_AltStore.framework */;
|
||||
productType = "com.apple.product-type.framework";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
@@ -238,12 +348,13 @@
|
||||
en,
|
||||
);
|
||||
mainGroup = CF1408CF629C7361332E53B88F7BD30C;
|
||||
productRefGroup = C503C40C4D61FFD0A7542EA7A5BA8470 /* Products */;
|
||||
productRefGroup = 21AFD5ACB5E55904BC69293508B530A1 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
3F895CBA524B654D3F837F25BFBE2262 /* KeychainAccess */,
|
||||
A24BCCF0615F19EF15C5175F5155059B /* Pods-AltStore */,
|
||||
A6293B46682B3506C97B73C333967DDB /* Nuke */,
|
||||
ADDE9B18CB22BDBFE81CDA0BD4FDA590 /* Pods-AltStore */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
@@ -256,7 +367,14 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
E76406DC9B6AA5D67A3AC649D0F24293 /* Resources */ = {
|
||||
468C740CFE588F51DAD34AF0BA3320F8 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
B59C37B929FF009500F79859910E42D5 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
@@ -266,11 +384,30 @@
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
B416FEBA18E397F57E54B00750E42DD5 /* Sources */ = {
|
||||
3A900BB464741FA7A6022FAAD89DF071 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
853F25B704B9C1E0728C8355490C3E8B /* Pods-AltStore-dummy.m in Sources */,
|
||||
57E7588C7E69A7D306360A22C9D6FF5E /* Pods-AltStore-dummy.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
876A7D7DB025A5564DA9E3E075AF114A /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D1AF4602637FA0CCD336D08A60CA142D /* DataCache.swift in Sources */,
|
||||
0A18CFC96BA1E3D7DF11145FABF1E64E /* DataLoader.swift in Sources */,
|
||||
5D68B76EC70C7CD88977C7BBA5C47B1A /* ImageCache.swift in Sources */,
|
||||
CF920935C6D30430A43066B25CC006B1 /* ImageDecoding.swift in Sources */,
|
||||
C19654E5869F26968FE04867EBE0AA93 /* ImagePipeline.swift in Sources */,
|
||||
FA2127BC3DA61718740A7405E7BBE85E /* ImagePreheater.swift in Sources */,
|
||||
96F79411300DB3B1280948E2B165AA03 /* ImageProcessing.swift in Sources */,
|
||||
9B3853F97F020FD13063BBA1688D52FE /* ImageRequest.swift in Sources */,
|
||||
879AADFC0E22BDB7716910D0A3369C77 /* ImageTaskMetrics.swift in Sources */,
|
||||
87A66BE797FE126185C9FEB113AD1AAE /* ImageView.swift in Sources */,
|
||||
4BC2A440F56B2681B7CE91EE8EBFC894 /* Internal.swift in Sources */,
|
||||
8D6722BD6426F3E66CBD894A443832CD /* Nuke-dummy.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -286,20 +423,25 @@
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
E524462E97292EC1913EB28B4E5A395E /* PBXTargetDependency */ = {
|
||||
449A8AC2DD1D7034AC060D2E9EAB443C /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
name = KeychainAccess;
|
||||
target = 3F895CBA524B654D3F837F25BFBE2262 /* KeychainAccess */;
|
||||
targetProxy = 9FFDB3D153C5FA18B6FD59B50C54CAAD /* PBXContainerItemProxy */;
|
||||
targetProxy = 3895BFEFA1F8BAB69DF11C154E9DA338 /* PBXContainerItemProxy */;
|
||||
};
|
||||
701929F2CB443A5BF858E1C515F0F81A /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
name = Nuke;
|
||||
target = A6293B46682B3506C97B73C333967DDB /* Nuke */;
|
||||
targetProxy = C08AA1ABDB4BA9BB2BE4BBD9106AA301 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
0DFF4363645F04A33C4CA0552E2894B6 /* Release */ = {
|
||||
1780628C629B6316E332253A3502E00D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 12FBD321A774CE97AC1213C6B240DC9F /* Pods-AltStore.release.xcconfig */;
|
||||
baseConfigurationReference = 38D1B55A07DBA5790862EE8B2E01C5D1 /* Nuke.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO;
|
||||
CLANG_ENABLE_OBJC_WEAK = NO;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "";
|
||||
@@ -310,29 +452,27 @@
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 1;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
INFOPLIST_FILE = "Target Support Files/Pods-AltStore/Pods-AltStore-Info.plist";
|
||||
GCC_PREFIX_HEADER = "Target Support Files/Nuke/Nuke-prefix.pch";
|
||||
INFOPLIST_FILE = "Target Support Files/Nuke/Nuke-Info.plist";
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MACH_O_TYPE = staticlib;
|
||||
MODULEMAP_FILE = "Target Support Files/Pods-AltStore/Pods-AltStore.modulemap";
|
||||
OTHER_LDFLAGS = "";
|
||||
OTHER_LIBTOOLFLAGS = "";
|
||||
PODS_ROOT = "$(SRCROOT)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}";
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
MODULEMAP_FILE = "Target Support Files/Nuke/Nuke.modulemap";
|
||||
PRODUCT_MODULE_NAME = Nuke;
|
||||
PRODUCT_NAME = Nuke;
|
||||
SDKROOT = iphoneos;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) ";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
VERSION_INFO_PREFIX = "";
|
||||
};
|
||||
name = Release;
|
||||
name = Debug;
|
||||
};
|
||||
4093433A309EFE160336AC736594AB22 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
@@ -398,9 +538,47 @@
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
47D7F07526A878DBCCA39B0583B8981F /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 4DBA4D739ECCF9E52DA7C1AFF75568B0 /* Pods-AltStore.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO;
|
||||
CLANG_ENABLE_OBJC_WEAK = NO;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
|
||||
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "";
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEFINES_MODULE = YES;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 1;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
INFOPLIST_FILE = "Target Support Files/Pods-AltStore/Pods-AltStore-Info.plist";
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MACH_O_TYPE = staticlib;
|
||||
MODULEMAP_FILE = "Target Support Files/Pods-AltStore/Pods-AltStore.modulemap";
|
||||
OTHER_LDFLAGS = "";
|
||||
OTHER_LIBTOOLFLAGS = "";
|
||||
PODS_ROOT = "$(SRCROOT)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}";
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SDKROOT = iphoneos;
|
||||
SKIP_INSTALL = YES;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
VERSION_INFO_PREFIX = "";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
55F18E06825E1BFC1A262F9FC0752F52 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 87BB091C7209D9D07DFA5FFB06428B65 /* KeychainAccess.xcconfig */;
|
||||
baseConfigurationReference = 4D6A329382B92AAA01398C8A3C5DD70D /* KeychainAccess.xcconfig */;
|
||||
buildSettings = {
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "";
|
||||
@@ -433,6 +611,43 @@
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
8BBFD9427B78897F530888A4B7654723 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 38D1B55A07DBA5790862EE8B2E01C5D1 /* Nuke.xcconfig */;
|
||||
buildSettings = {
|
||||
CLANG_ENABLE_OBJC_WEAK = NO;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
|
||||
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "";
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEFINES_MODULE = YES;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 1;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
GCC_PREFIX_HEADER = "Target Support Files/Nuke/Nuke-prefix.pch";
|
||||
INFOPLIST_FILE = "Target Support Files/Nuke/Nuke-Info.plist";
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MODULEMAP_FILE = "Target Support Files/Nuke/Nuke.modulemap";
|
||||
PRODUCT_MODULE_NAME = Nuke;
|
||||
PRODUCT_NAME = Nuke;
|
||||
SDKROOT = iphoneos;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) ";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
VERSION_INFO_PREFIX = "";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
924D56B1D59CD3049300EAEEA256114E /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
@@ -493,9 +708,48 @@
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
945AF1CAB299920118B8BFD9D4B80B75 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 12FBD321A774CE97AC1213C6B240DC9F /* Pods-AltStore.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO;
|
||||
CLANG_ENABLE_OBJC_WEAK = NO;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
|
||||
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "";
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEFINES_MODULE = YES;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 1;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
INFOPLIST_FILE = "Target Support Files/Pods-AltStore/Pods-AltStore-Info.plist";
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MACH_O_TYPE = staticlib;
|
||||
MODULEMAP_FILE = "Target Support Files/Pods-AltStore/Pods-AltStore.modulemap";
|
||||
OTHER_LDFLAGS = "";
|
||||
OTHER_LIBTOOLFLAGS = "";
|
||||
PODS_ROOT = "$(SRCROOT)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}";
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SDKROOT = iphoneos;
|
||||
SKIP_INSTALL = YES;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
VERSION_INFO_PREFIX = "";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
BC334190485B1F4408F03EDF94A692EF /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 87BB091C7209D9D07DFA5FFB06428B65 /* KeychainAccess.xcconfig */;
|
||||
baseConfigurationReference = 4D6A329382B92AAA01398C8A3C5DD70D /* KeychainAccess.xcconfig */;
|
||||
buildSettings = {
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "";
|
||||
@@ -529,44 +783,6 @@
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
F1AB5B87B0E318FCDC88C0E5FF60B9A4 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 4DBA4D739ECCF9E52DA7C1AFF75568B0 /* Pods-AltStore.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO;
|
||||
CLANG_ENABLE_OBJC_WEAK = NO;
|
||||
CODE_SIGN_IDENTITY = "";
|
||||
"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
|
||||
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "";
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEFINES_MODULE = YES;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 1;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
INFOPLIST_FILE = "Target Support Files/Pods-AltStore/Pods-AltStore-Info.plist";
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
MACH_O_TYPE = staticlib;
|
||||
MODULEMAP_FILE = "Target Support Files/Pods-AltStore/Pods-AltStore.modulemap";
|
||||
OTHER_LDFLAGS = "";
|
||||
OTHER_LIBTOOLFLAGS = "";
|
||||
PODS_ROOT = "$(SRCROOT)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}";
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SDKROOT = iphoneos;
|
||||
SKIP_INSTALL = YES;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
VERSION_INFO_PREFIX = "";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
@@ -579,11 +795,20 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
9DCE5F54F9F65C3281FF75A7483961D6 /* Build configuration list for PBXNativeTarget "Pods-AltStore" */ = {
|
||||
B7033C3F5480C9769E718E1F13B2716B /* Build configuration list for PBXNativeTarget "Nuke" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
F1AB5B87B0E318FCDC88C0E5FF60B9A4 /* Debug */,
|
||||
0DFF4363645F04A33C4CA0552E2894B6 /* Release */,
|
||||
1780628C629B6316E332253A3502E00D /* Debug */,
|
||||
8BBFD9427B78897F530888A4B7654723 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
B8810C5C21F2ADE6A3B505A9C49E78A6 /* Build configuration list for PBXNativeTarget "Pods-AltStore" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
47D7F07526A878DBCCA39B0583B8981F /* Debug */,
|
||||
945AF1CAB299920118B8BFD9D4B80B75 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
|
||||
26
Pods/Target Support Files/Nuke/Nuke-Info.plist
generated
Normal file
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${EXECUTABLE_NAME}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>${PRODUCT_BUNDLE_IDENTIFIER}</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>${PRODUCT_NAME}</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>7.6.2</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>${CURRENT_PROJECT_VERSION}</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</plist>
|
||||
5
Pods/Target Support Files/Nuke/Nuke-dummy.m
generated
Normal file
@@ -0,0 +1,5 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
@interface PodsDummy_Nuke : NSObject
|
||||
@end
|
||||
@implementation PodsDummy_Nuke
|
||||
@end
|
||||
12
Pods/Target Support Files/Nuke/Nuke-prefix.pch
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
#ifdef __OBJC__
|
||||
#import <UIKit/UIKit.h>
|
||||
#else
|
||||
#ifndef FOUNDATION_EXPORT
|
||||
#if defined(__cplusplus)
|
||||
#define FOUNDATION_EXPORT extern "C"
|
||||
#else
|
||||
#define FOUNDATION_EXPORT extern
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
|
||||
16
Pods/Target Support Files/Nuke/Nuke-umbrella.h
generated
Normal file
@@ -0,0 +1,16 @@
|
||||
#ifdef __OBJC__
|
||||
#import <UIKit/UIKit.h>
|
||||
#else
|
||||
#ifndef FOUNDATION_EXPORT
|
||||
#if defined(__cplusplus)
|
||||
#define FOUNDATION_EXPORT extern "C"
|
||||
#else
|
||||
#define FOUNDATION_EXPORT extern
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
|
||||
|
||||
FOUNDATION_EXPORT double NukeVersionNumber;
|
||||
FOUNDATION_EXPORT const unsigned char NukeVersionString[];
|
||||
|
||||
6
Pods/Target Support Files/Nuke/Nuke.modulemap
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
framework module Nuke {
|
||||
umbrella header "Nuke-umbrella.h"
|
||||
|
||||
export *
|
||||
module * { export * }
|
||||
}
|
||||
9
Pods/Target Support Files/Nuke/Nuke.xcconfig
generated
Normal file
@@ -0,0 +1,9 @@
|
||||
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/Nuke
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS -suppress-warnings
|
||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
PODS_ROOT = ${SRCROOT}
|
||||
PODS_TARGET_SRCROOT = ${PODS_ROOT}/Nuke
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
|
||||
SKIP_INSTALL = YES
|
||||
@@ -26,4 +26,29 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
|
||||
## Nuke
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2018 Alexander Grebenyuk
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
Generated by CocoaPods - https://cocoapods.org
|
||||
|
||||
@@ -44,6 +44,37 @@ SOFTWARE.
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>FooterText</key>
|
||||
<string>The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2018 Alexander Grebenyuk
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
</string>
|
||||
<key>License</key>
|
||||
<string>MIT</string>
|
||||
<key>Title</key>
|
||||
<string>Nuke</string>
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>FooterText</key>
|
||||
<string>Generated by CocoaPods - https://cocoapods.org</string>
|
||||
|
||||
@@ -154,9 +154,11 @@ strip_invalid_archs() {
|
||||
|
||||
if [[ "$CONFIGURATION" == "Debug" ]]; then
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/KeychainAccess/KeychainAccess.framework"
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/Nuke/Nuke.framework"
|
||||
fi
|
||||
if [[ "$CONFIGURATION" == "Release" ]]; then
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/KeychainAccess/KeychainAccess.framework"
|
||||
install_framework "${BUILT_PRODUCTS_DIR}/Nuke/Nuke.framework"
|
||||
fi
|
||||
if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then
|
||||
wait
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES
|
||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/KeychainAccess"
|
||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/KeychainAccess" "${PODS_CONFIGURATION_BUILD_DIR}/Nuke"
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/KeychainAccess/KeychainAccess.framework/Headers"
|
||||
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/KeychainAccess/KeychainAccess.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Nuke/Nuke.framework/Headers"
|
||||
LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks'
|
||||
OTHER_CFLAGS = $(inherited) -isystem "${PODS_CONFIGURATION_BUILD_DIR}/KeychainAccess/KeychainAccess.framework/Headers" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/KeychainAccess"
|
||||
OTHER_LDFLAGS = $(inherited) -framework "KeychainAccess"
|
||||
OTHER_CFLAGS = $(inherited) -isystem "${PODS_CONFIGURATION_BUILD_DIR}/KeychainAccess/KeychainAccess.framework/Headers" -isystem "${PODS_CONFIGURATION_BUILD_DIR}/Nuke/Nuke.framework/Headers" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/KeychainAccess" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/Nuke"
|
||||
OTHER_LDFLAGS = $(inherited) -framework "KeychainAccess" -framework "Nuke"
|
||||
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
|
||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES
|
||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/KeychainAccess"
|
||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/KeychainAccess" "${PODS_CONFIGURATION_BUILD_DIR}/Nuke"
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/KeychainAccess/KeychainAccess.framework/Headers"
|
||||
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/KeychainAccess/KeychainAccess.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Nuke/Nuke.framework/Headers"
|
||||
LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks'
|
||||
OTHER_CFLAGS = $(inherited) -isystem "${PODS_CONFIGURATION_BUILD_DIR}/KeychainAccess/KeychainAccess.framework/Headers" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/KeychainAccess"
|
||||
OTHER_LDFLAGS = $(inherited) -framework "KeychainAccess"
|
||||
OTHER_CFLAGS = $(inherited) -isystem "${PODS_CONFIGURATION_BUILD_DIR}/KeychainAccess/KeychainAccess.framework/Headers" -isystem "${PODS_CONFIGURATION_BUILD_DIR}/Nuke/Nuke.framework/Headers" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/KeychainAccess" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/Nuke"
|
||||
OTHER_LDFLAGS = $(inherited) -framework "KeychainAccess" -framework "Nuke"
|
||||
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
|
||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
|
||||