├── .gitignore ├── Demo ├── Demo │ └── Tiercel-iOS-Demo │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── resume.imageset │ │ │ ├── Contents.json │ │ │ └── resume.png │ │ ├── suspend.imageset │ │ │ ├── Contents.json │ │ │ └── suspend.png │ │ └── tiercel.imageset │ │ │ ├── Contents.json │ │ │ └── tiercel.png │ │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ │ ├── BaseViewController.swift │ │ ├── DownloadTaskCell.swift │ │ ├── DownloadTaskCell.xib │ │ ├── DownloadViewController.swift │ │ ├── Info.plist │ │ ├── ListViewCell.swift │ │ ├── ListViewController.swift │ │ ├── VideoURLStrings.plist │ │ ├── ViewController1.swift │ │ ├── ViewController2.swift │ │ └── ViewController3.swift └── Tiercel-Demo.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ └── xcschemes │ └── Tiercel-iOS-Demo.xcscheme ├── Images ├── 1.gif ├── 2.gif └── logo.png ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── Extensions │ ├── Array+Safe.swift │ ├── CodingUserInfoKey+Cache.swift │ ├── Data+Hash.swift │ ├── DispatchQueue+Safe.swift │ ├── Double+TaskInfo.swift │ ├── FileManager+AvailableCapacity.swift │ ├── Int64+TaskInfo.swift │ ├── OperationQueue+DispatchQueue.swift │ ├── String+Hash.swift │ └── URLSession+ResumeData.swift ├── General │ ├── Cache.swift │ ├── Common.swift │ ├── DownloadTask.swift │ ├── Executer.swift │ ├── Notifications.swift │ ├── Protected.swift │ ├── SessionConfiguration.swift │ ├── SessionDelegate.swift │ ├── SessionManager.swift │ ├── Task.swift │ ├── TiercelError.swift │ └── URLConvertible.swift ├── Info.plist ├── Tiercel.h └── Utility │ ├── FileChecksumHelper.swift │ └── ResumeDataHelper.swift ├── Tiercel.podspec ├── Tiercel.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Tiercel.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist └── TiercelTests ├── Info.plist └── TiercelTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | .swiftpm/ 9 | # OSX 10 | .DS_Store 11 | 12 | ## Various settings 13 | *.pbxuser 14 | !default.pbxuser 15 | *.mode1v3 16 | !default.mode1v3 17 | *.mode2v3 18 | !default.mode2v3 19 | *.perspectivev3 20 | !default.perspectivev3 21 | xcuserdata/ 22 | 23 | ## Other 24 | *.moved-aside 25 | *.xccheckout 26 | *.xcscmblueprint 27 | 28 | ## Obj-C/Swift specific 29 | *.hmap 30 | *.ipa 31 | *.dSYM.zip 32 | *.dSYM 33 | 34 | ## Playgrounds 35 | timeline.xctimeline 36 | playground.xcworkspace 37 | 38 | # Swift Package Manager 39 | # 40 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 41 | # Packages/ 42 | # Package.pins 43 | # Package.resolved 44 | .build/ 45 | 46 | # CocoaPods 47 | # 48 | # We recommend against adding the Pods directory to your .gitignore. However 49 | # you should judge for yourself, the pros and cons are mentioned at: 50 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 51 | # 52 | Pods/ 53 | 54 | # Carthage 55 | # 56 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 57 | # Carthage/Checkouts 58 | 59 | Carthage/Build 60 | 61 | # fastlane 62 | # 63 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 64 | # screenshots whenever they are needed. 65 | # For more information about the recommended setup visit: 66 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 67 | 68 | fastlane/report.xml 69 | fastlane/Preview.html 70 | fastlane/screenshots 71 | fastlane/test_output 72 | -------------------------------------------------------------------------------- /Demo/Demo/Tiercel-iOS-Demo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Example 4 | // 5 | // Created by Daniels on 2018/3/16. 6 | // Copyright © 2018 Daniels. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Tiercel 11 | 12 | let appDelegate = UIApplication.shared.delegate as! AppDelegate 13 | 14 | @UIApplicationMain 15 | class AppDelegate: UIResponder, UIApplicationDelegate { 16 | 17 | var window: UIWindow? 18 | 19 | let sessionManager1 = SessionManager("ViewController1", configuration: SessionConfiguration()) 20 | 21 | var sessionManager2: SessionManager = { 22 | var configuration = SessionConfiguration() 23 | configuration.allowsCellularAccess = true 24 | let path = Cache.defaultDiskCachePathClosure("Test") 25 | let cacahe = Cache("ViewController2", downloadPath: path) 26 | let manager = SessionManager("ViewController2", configuration: configuration, cache: cacahe, operationQueue: DispatchQueue(label: "com.Tiercel.SessionManager.operationQueue")) 27 | return manager 28 | }() 29 | 30 | let sessionManager3 = SessionManager("ViewController3", configuration: SessionConfiguration()) 31 | 32 | let sessionManager4 = SessionManager("ViewController4", configuration: SessionConfiguration()) 33 | 34 | 35 | 36 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 37 | 38 | return true 39 | } 40 | 41 | func applicationWillResignActive(_ application: UIApplication) { 42 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 43 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 44 | } 45 | 46 | func applicationDidEnterBackground(_ application: UIApplication) { 47 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 48 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 49 | } 50 | 51 | func applicationWillEnterForeground(_ application: UIApplication) { 52 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 53 | } 54 | 55 | func applicationDidBecomeActive(_ application: UIApplication) { 56 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 57 | } 58 | 59 | func applicationWillTerminate(_ application: UIApplication) { 60 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 61 | } 62 | 63 | func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) { 64 | let downloadManagers = [sessionManager1, sessionManager2, sessionManager3, sessionManager4] 65 | for manager in downloadManagers { 66 | if manager.identifier == identifier { 67 | manager.completionHandler = completionHandler 68 | break 69 | } 70 | } 71 | } 72 | 73 | 74 | } 75 | 76 | -------------------------------------------------------------------------------- /Demo/Demo/Tiercel-iOS-Demo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Demo/Demo/Tiercel-iOS-Demo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Demo/Demo/Tiercel-iOS-Demo/Assets.xcassets/resume.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "resume.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Demo/Demo/Tiercel-iOS-Demo/Assets.xcassets/resume.imageset/resume.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Danie1s/Tiercel/7a0c4159651062911651917f892e5dc0a9caf8bb/Demo/Demo/Tiercel-iOS-Demo/Assets.xcassets/resume.imageset/resume.png -------------------------------------------------------------------------------- /Demo/Demo/Tiercel-iOS-Demo/Assets.xcassets/suspend.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "suspend.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Demo/Demo/Tiercel-iOS-Demo/Assets.xcassets/suspend.imageset/suspend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Danie1s/Tiercel/7a0c4159651062911651917f892e5dc0a9caf8bb/Demo/Demo/Tiercel-iOS-Demo/Assets.xcassets/suspend.imageset/suspend.png -------------------------------------------------------------------------------- /Demo/Demo/Tiercel-iOS-Demo/Assets.xcassets/tiercel.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "tiercel.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Demo/Demo/Tiercel-iOS-Demo/Assets.xcassets/tiercel.imageset/tiercel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Danie1s/Tiercel/7a0c4159651062911651917f892e5dc0a9caf8bb/Demo/Demo/Tiercel-iOS-Demo/Assets.xcassets/tiercel.imageset/tiercel.png -------------------------------------------------------------------------------- /Demo/Demo/Tiercel-iOS-Demo/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 32 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /Demo/Demo/Tiercel-iOS-Demo/BaseViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseViewController.swift 3 | // Example 4 | // 5 | // Created by Daniels on 2018/3/20. 6 | // Copyright © 2018 Daniels. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Tiercel 11 | 12 | class BaseViewController: UIViewController { 13 | 14 | @IBOutlet weak var tableView: UITableView! 15 | @IBOutlet weak var totalTasksLabel: UILabel! 16 | @IBOutlet weak var totalSpeedLabel: UILabel! 17 | @IBOutlet weak var timeRemainingLabel: UILabel! 18 | @IBOutlet weak var totalProgressLabel: UILabel! 19 | 20 | 21 | @IBOutlet weak var taskLimitSwitch: UISwitch! 22 | @IBOutlet weak var cellularAccessSwitch: UISwitch! 23 | 24 | 25 | var sessionManager: SessionManager! 26 | 27 | var URLStrings: [String] = [] 28 | 29 | override func viewDidLoad() { 30 | super.viewDidLoad() 31 | 32 | setupUI() 33 | 34 | // 检查磁盘空间 35 | let free = FileManager.default.tr.freeDiskSpaceInBytes / 1024 / 1024 36 | print("手机剩余储存空间为: \(free)MB") 37 | 38 | sessionManager.logger.option = .default 39 | 40 | updateSwicth() 41 | } 42 | 43 | func setupUI() { 44 | // tableView的设置 45 | tableView.dataSource = self 46 | tableView.delegate = self 47 | tableView.tableFooterView = UIView() 48 | tableView.register(UINib(nibName: "\(DownloadTaskCell.self)", bundle: nil), 49 | forCellReuseIdentifier: DownloadTaskCell.reuseIdentifier) 50 | tableView.rowHeight = UITableView.automaticDimension 51 | tableView.estimatedRowHeight = 164 52 | 53 | configureNavigationItem() 54 | } 55 | 56 | func configureNavigationItem() { 57 | navigationItem.rightBarButtonItem = UIBarButtonItem(title: "编辑", 58 | style: .plain, 59 | target: self, 60 | action: #selector(toggleEditing)) 61 | } 62 | 63 | 64 | @objc func toggleEditing() { 65 | tableView.setEditing(!tableView.isEditing, animated: true) 66 | let button = navigationItem.rightBarButtonItem! 67 | button.title = tableView.isEditing ? "完成" : "编辑" 68 | } 69 | 70 | func updateUI() { 71 | totalTasksLabel.text = "总任务:\(sessionManager.succeededTasks.count)/\(sessionManager.tasks.count)" 72 | totalSpeedLabel.text = "总速度:\(sessionManager.speedString)" 73 | timeRemainingLabel.text = "剩余时间: \(sessionManager.timeRemainingString)" 74 | let per = String(format: "%.2f", sessionManager.progress.fractionCompleted) 75 | totalProgressLabel.text = "总进度: \(per)" 76 | } 77 | 78 | func updateSwicth() { 79 | taskLimitSwitch.isOn = sessionManager.configuration.maxConcurrentTasksLimit < 3 80 | cellularAccessSwitch.isOn = sessionManager.configuration.allowsCellularAccess 81 | } 82 | 83 | func setupManager() { 84 | 85 | // 设置 manager 的回调 86 | sessionManager.progress { [weak self] (manager) in 87 | self?.updateUI() 88 | 89 | }.completion { [weak self] manager in 90 | self?.updateUI() 91 | if manager.status == .succeeded { 92 | // 下载成功 93 | } else { 94 | // 其他状态 95 | } 96 | } 97 | } 98 | } 99 | 100 | extension BaseViewController { 101 | @IBAction func totalStart(_ sender: Any) { 102 | sessionManager.totalStart { [weak self] _ in 103 | self?.tableView.reloadData() 104 | } 105 | } 106 | 107 | @IBAction func totalSuspend(_ sender: Any) { 108 | sessionManager.totalSuspend() { [weak self] _ in 109 | self?.tableView.reloadData() 110 | 111 | } 112 | } 113 | 114 | @IBAction func totalCancel(_ sender: Any) { 115 | sessionManager.totalCancel() { [weak self] _ in 116 | self?.tableView.reloadData() 117 | } 118 | } 119 | 120 | @IBAction func totalDelete(_ sender: Any) { 121 | sessionManager.totalRemove(completely: false) { [weak self] _ in 122 | self?.tableView.reloadData() 123 | } 124 | } 125 | 126 | @IBAction func clearDisk(_ sender: Any) { 127 | sessionManager.cache.clearDiskCache() 128 | updateUI() 129 | } 130 | 131 | 132 | @IBAction func taskLimit(_ sender: UISwitch) { 133 | if sender.isOn { 134 | sessionManager.configuration.maxConcurrentTasksLimit = 2 135 | } else { 136 | sessionManager.configuration.maxConcurrentTasksLimit = Int.max 137 | } 138 | } 139 | 140 | @IBAction func cellularAccess(_ sender: UISwitch) { 141 | sessionManager.configuration.allowsCellularAccess = sender.isOn 142 | } 143 | } 144 | 145 | // MARK: - UITableViewDataSource & UITableViewDelegate 146 | extension BaseViewController: UITableViewDataSource, UITableViewDelegate { 147 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 148 | return sessionManager.tasks.count 149 | } 150 | 151 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 152 | let cell = tableView.dequeueReusableCell(withIdentifier: DownloadTaskCell.reuseIdentifier, for: indexPath) as! DownloadTaskCell 153 | return cell 154 | } 155 | 156 | // 每个 cell 中的状态更新,应该在 willDisplay 中执行 157 | func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { 158 | 159 | guard let task = sessionManager.tasks.safeObject(at: indexPath.row), 160 | let cell = cell as? DownloadTaskCell 161 | else { return } 162 | 163 | cell.task?.progress { _ in }.success { _ in }.failure { _ in } 164 | 165 | cell.task = task 166 | 167 | cell.titleLabel.text = task.fileName 168 | 169 | cell.updateProgress(task) 170 | 171 | cell.tapClosure = { [weak self] cell in 172 | guard let task = self?.sessionManager.tasks.safeObject(at: indexPath.row) else { return } 173 | switch task.status { 174 | case .waiting, .running: 175 | self?.sessionManager.suspend(task) 176 | case .suspended, .failed: 177 | self?.sessionManager.start(task) 178 | default: 179 | break 180 | } 181 | } 182 | 183 | task.progress { [weak cell] task in 184 | cell?.updateProgress(task) 185 | } 186 | .success { [weak cell] (task) in 187 | cell?.updateProgress(task) 188 | // 下载任务成功了 189 | 190 | } 191 | .failure { [weak cell] task in 192 | cell?.updateProgress(task) 193 | if task.status == .suspended { 194 | // 下载任务暂停了 195 | } 196 | 197 | if task.status == .failed { 198 | // 下载任务失败了 199 | } 200 | if task.status == .canceled { 201 | // 下载任务取消了 202 | } 203 | if task.status == .removed { 204 | // 下载任务移除了 205 | } 206 | } 207 | } 208 | 209 | 210 | func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { 211 | return true 212 | } 213 | 214 | func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { 215 | if editingStyle == .delete { 216 | guard let task = sessionManager.tasks.safeObject(at: indexPath.row) else { return } 217 | sessionManager.remove(task, completely: false) { [weak self] _ in 218 | self?.tableView.deleteRows(at: [indexPath], with: .automatic) 219 | self?.updateUI() 220 | } 221 | } 222 | } 223 | 224 | func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { 225 | sessionManager.moveTask(at: sourceIndexPath.row, to: destinationIndexPath.row) 226 | } 227 | 228 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 229 | tableView.deselectRow(at: indexPath, animated: true) 230 | } 231 | 232 | } 233 | -------------------------------------------------------------------------------- /Demo/Demo/Tiercel-iOS-Demo/DownloadTaskCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DownloadTaskCell.swift 3 | // Example 4 | // 5 | // Created by Daniels on 2018/3/16. 6 | // Copyright © 2018 Daniels. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Tiercel 11 | 12 | class DownloadTaskCell: UITableViewCell { 13 | 14 | static let reuseIdentifier = "reuseIdentifier" 15 | 16 | @IBOutlet weak var titleLabel: UILabel! 17 | @IBOutlet weak var speedLabel: UILabel! 18 | @IBOutlet weak var bytesLabel: UILabel! 19 | @IBOutlet weak var controlButton: UIButton! 20 | @IBOutlet weak var progressView: UIProgressView! 21 | @IBOutlet weak var timeRemainingLabel: UILabel! 22 | @IBOutlet weak var startDateLabel: UILabel! 23 | @IBOutlet weak var endDateLabel: UILabel! 24 | @IBOutlet weak var statusLabel: UILabel! 25 | 26 | var tapClosure: ((DownloadTaskCell) -> Void)? 27 | 28 | var task: DownloadTask? 29 | 30 | 31 | @IBAction func didTapButton(_ sender: Any) { 32 | tapClosure?(self) 33 | } 34 | 35 | func updateProgress(_ task: DownloadTask) { 36 | progressView.observedProgress = task.progress 37 | bytesLabel.text = "\(task.progress.completedUnitCount.tr.convertBytesToString())/\(task.progress.totalUnitCount.tr.convertBytesToString())" 38 | speedLabel.text = task.speedString 39 | timeRemainingLabel.text = "剩余时间:\(task.timeRemainingString)" 40 | startDateLabel.text = "开始时间:\(task.startDateString)" 41 | endDateLabel.text = "结束时间:\(task.endDateString)" 42 | 43 | var image = #imageLiteral(resourceName: "suspend") 44 | switch task.status { 45 | case .suspended: 46 | statusLabel.text = "暂停" 47 | statusLabel.textColor = .black 48 | case .running: 49 | image = #imageLiteral(resourceName: "resume") 50 | statusLabel.text = "下载中" 51 | statusLabel.textColor = .blue 52 | case .succeeded: 53 | statusLabel.text = "成功" 54 | statusLabel.textColor = .green 55 | case .failed: 56 | statusLabel.text = "失败" 57 | statusLabel.textColor = .red 58 | case .waiting: 59 | statusLabel.text = "等待中" 60 | statusLabel.textColor = .orange 61 | default: 62 | image = controlButton.imageView?.image ?? #imageLiteral(resourceName: "suspend") 63 | break 64 | } 65 | controlButton.setImage(image, for: .normal) 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /Demo/Demo/Tiercel-iOS-Demo/DownloadTaskCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 25 | 31 | 32 | 33 | 34 | 45 | 51 | 57 | 63 | 69 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /Demo/Demo/Tiercel-iOS-Demo/DownloadViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DownloadViewController.swift 3 | // Example 4 | // 5 | // Created by Daniels on 2018/3/16. 6 | // Copyright © 2018 Daniels. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class DownloadViewController: BaseViewController { 12 | 13 | 14 | override func viewDidLoad() { 15 | 16 | sessionManager = appDelegate.sessionManager4 17 | 18 | super.viewDidLoad() 19 | 20 | setupManager() 21 | } 22 | 23 | override func viewWillAppear(_ animated: Bool) { 24 | super.viewWillAppear(animated) 25 | 26 | updateUI() 27 | tableView.reloadData() 28 | } 29 | 30 | } 31 | 32 | 33 | // MARK: - tap event 34 | extension DownloadViewController { 35 | 36 | @IBAction func deleteDownloadTask(_ sender: UIButton) { 37 | let count = sessionManager.tasks.count 38 | guard count > 0 else { return } 39 | let index = count - 1 40 | guard let task = sessionManager.tasks.safeObject(at: index) else { return } 41 | // tableView 刷新和删除 task 都是异步的,如果操作过快会导致数据不一致,所以需要限制 button 的点击 42 | sender.isEnabled = false 43 | sessionManager.remove(task, completely: false) { [weak self] _ in 44 | self?.tableView.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic) 45 | self?.updateUI() 46 | sender.isEnabled = true 47 | } 48 | } 49 | 50 | } 51 | 52 | 53 | -------------------------------------------------------------------------------- /Demo/Demo/Tiercel-iOS-Demo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | $(PRODUCT_NAME) 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | NSAppTransportSecurity 26 | 27 | NSAllowsArbitraryLoads 28 | 29 | 30 | UIBackgroundModes 31 | 32 | fetch 33 | 34 | UILaunchStoryboardName 35 | LaunchScreen 36 | UIMainStoryboardFile 37 | Main 38 | UIRequiredDeviceCapabilities 39 | 40 | armv7 41 | 42 | UISupportedInterfaceOrientations 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationLandscapeLeft 46 | UIInterfaceOrientationLandscapeRight 47 | 48 | UISupportedInterfaceOrientations~ipad 49 | 50 | UIInterfaceOrientationPortrait 51 | UIInterfaceOrientationPortraitUpsideDown 52 | UIInterfaceOrientationLandscapeLeft 53 | UIInterfaceOrientationLandscapeRight 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /Demo/Demo/Tiercel-iOS-Demo/ListViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListViewCell.swift 3 | // Example 4 | // 5 | // Created by Daniels on 2018/3/16. 6 | // Copyright © 2018 Daniels. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ListViewCell: UITableViewCell { 12 | 13 | static let reuseIdentifier = "reuseIdentifier" 14 | 15 | @IBOutlet weak var URLStringLabel: UILabel! 16 | 17 | var downloadClosure: ((ListViewCell) -> ())? 18 | 19 | override func awakeFromNib() { 20 | super.awakeFromNib() 21 | // Initialization code 22 | } 23 | 24 | @IBAction func download(_ sender: Any) { 25 | downloadClosure?(self) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Demo/Demo/Tiercel-iOS-Demo/ListViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListViewController.swift 3 | // Example 4 | // 5 | // Created by Daniels on 2018/3/16. 6 | // Copyright © 2018 Daniels. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Tiercel 11 | 12 | class ListViewController: UITableViewController { 13 | 14 | 15 | lazy var URLStrings: [String] = { 16 | return [ 17 | "https://officecdn-microsoft-com.akamaized.net/pr/C1297A47-86C4-4C1F-97FA-950631F94777/MacAutoupdate/Microsoft_Office_16.24.19041401_Installer.pkg", 18 | "http://dldir1.qq.com/qqfile/QQforMac/QQ_V6.5.2.dmg", 19 | "http://issuecdn.baidupcs.com/issue/netdisk/MACguanjia/BaiduNetdisk_mac_2.2.3.dmg", 20 | "http://m4.pc6.com/cjh3/VicomsoftFTPClient.dmg", 21 | "https://qd.myapp.com/myapp/qqteam/pcqq/QQ9.0.8_2.exe", 22 | "http://gxiami.alicdn.com/xiami-desktop/update/XiamiMac-03051058.dmg", 23 | "http://113.113.73.41/r/baiducdnct-gd.inter.iqiyi.com/cdn/pcclient/20190413/13/25/iQIYIMedia_005.dmg?dis_dz=CT-GuangDong_GuangZhou&dis_st=36", 24 | "http://pcclient.download.youku.com/ikumac/youkumac_1.6.7.04093.dmg?spm=a2hpd.20022519.m_235549.5!2~5~5~5!2~P!3~A&file=youkumac_1.6.7.04093.dmg", 25 | "http://api.gfs100.cn/upload/20180126/201801261545095005.mp4", 26 | "http://dldir1.qq.com/qqfile/QQforMac/QQ_V4.2.4.dmg", 27 | ] 28 | }() 29 | 30 | 31 | 32 | override func viewDidLoad() { 33 | super.viewDidLoad() 34 | 35 | } 36 | 37 | 38 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 39 | return URLStrings.count 40 | } 41 | 42 | 43 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 44 | let cell = tableView.dequeueReusableCell(withIdentifier: ListViewCell.reuseIdentifier, for: indexPath) as! ListViewCell 45 | cell.URLStringLabel.text = "文件\(indexPath.row + 1).mp4" 46 | let URLStirng = URLStrings[indexPath.row] 47 | cell.downloadClosure = { cell in 48 | appDelegate.sessionManager4.download(URLStirng, fileName: cell.URLStringLabel.text) 49 | } 50 | 51 | return cell 52 | } 53 | 54 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 55 | tableView.deselectRow(at: indexPath, animated: true) 56 | } 57 | 58 | 59 | } 60 | -------------------------------------------------------------------------------- /Demo/Demo/Tiercel-iOS-Demo/ViewController1.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController1.swift 3 | // Example 4 | // 5 | // Created by Daniels on 2018/3/16. 6 | // Copyright © 2018 Daniels. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Tiercel 11 | 12 | class ViewController1: UIViewController { 13 | 14 | @IBOutlet weak var speedLabel: UILabel! 15 | @IBOutlet weak var progressLabel: UILabel! 16 | @IBOutlet weak var progressView: UIProgressView! 17 | @IBOutlet weak var timeRemainingLabel: UILabel! 18 | @IBOutlet weak var startDateLabel: UILabel! 19 | @IBOutlet weak var endDateLabel: UILabel! 20 | @IBOutlet weak var validationLabel: UILabel! 21 | 22 | 23 | // lazy var URLString = "https://officecdn-microsoft-com.akamaized.net/pr/C1297A47-86C4-4C1F-97FA-950631F94777/OfficeMac/Microsoft_Office_2016_16.10.18021001_Installer.pkg" 24 | lazy var URLString = "http://dldir1.qq.com/qqfile/QQforMac/QQ_V4.2.4.dmg" 25 | var sessionManager = appDelegate.sessionManager1 26 | 27 | override func viewDidLoad() { 28 | super.viewDidLoad() 29 | 30 | sessionManager.tasks.safeObject(at: 0)?.progress { [weak self] (task) in 31 | self?.updateUI(task) 32 | }.completion { [weak self] task in 33 | self?.updateUI(task) 34 | if task.status == .succeeded { 35 | // 下载成功 36 | } else { 37 | // 其他状态 38 | } 39 | }.validateFile(code: "9e2a3650530b563da297c9246acaad5c", type: .md5) { [weak self] task in 40 | self?.updateUI(task) 41 | if task.validation == .correct { 42 | // 文件正确 43 | } else { 44 | // 文件错误 45 | } 46 | } 47 | } 48 | 49 | private func updateUI(_ task: DownloadTask) { 50 | let per = task.progress.fractionCompleted 51 | progressLabel.text = "progress: \(String(format: "%.2f", per * 100))%" 52 | progressView.observedProgress = task.progress 53 | speedLabel.text = "speed: \(task.speedString)" 54 | timeRemainingLabel.text = "剩余时间: \(task.timeRemainingString)" 55 | startDateLabel.text = "开始时间: \(task.startDateString)" 56 | endDateLabel.text = "结束时间: \(task.endDateString)" 57 | var validation: String 58 | switch task.validation { 59 | case .unkown: 60 | validationLabel.textColor = UIColor.blue 61 | validation = "未知" 62 | case .correct: 63 | validationLabel.textColor = UIColor.green 64 | validation = "正确" 65 | case .incorrect: 66 | validationLabel.textColor = UIColor.red 67 | validation = "错误" 68 | } 69 | validationLabel.text = "文件验证: \(validation)" 70 | } 71 | 72 | @IBAction func start(_ sender: UIButton) { 73 | sessionManager.download(URLString)?.progress { [weak self] (task) in 74 | self?.updateUI(task) 75 | }.completion { [weak self] task in 76 | self?.updateUI(task) 77 | if task.status == .succeeded { 78 | // 下载成功 79 | } else { 80 | // 其他状态 81 | } 82 | }.validateFile(code: "9e2a3650530b563da297c9246acaad5c", type: .md5) { [weak self] (task) in 83 | self?.updateUI(task) 84 | if task.validation == .correct { 85 | // 文件正确 86 | } else { 87 | // 文件错误 88 | } 89 | } 90 | } 91 | 92 | @IBAction func suspend(_ sender: UIButton) { 93 | sessionManager.suspend(URLString) 94 | } 95 | 96 | 97 | @IBAction func cancel(_ sender: UIButton) { 98 | sessionManager.cancel(URLString) 99 | } 100 | 101 | @IBAction func deleteTask(_ sender: UIButton) { 102 | sessionManager.remove(URLString, completely: false) 103 | } 104 | 105 | @IBAction func clearDisk(_ sender: Any) { 106 | sessionManager.cache.clearDiskCache() 107 | } 108 | } 109 | 110 | -------------------------------------------------------------------------------- /Demo/Demo/Tiercel-iOS-Demo/ViewController2.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController2.swift 3 | // Example 4 | // 5 | // Created by Daniels on 2018/3/16. 6 | // Copyright © 2018 Daniels. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Tiercel 11 | 12 | class ViewController2: BaseViewController { 13 | 14 | override func viewDidLoad() { 15 | 16 | sessionManager = appDelegate.sessionManager2 17 | 18 | super.viewDidLoad() 19 | 20 | 21 | URLStrings = [ 22 | "https://officecdn-microsoft-com.akamaized.net/pr/C1297A47-86C4-4C1F-97FA-950631F94777/MacAutoupdate/Microsoft_Office_16.24.19041401_Installer.pkg", 23 | "http://dldir1.qq.com/qqfile/QQforMac/QQ_V6.5.2.dmg", 24 | "http://issuecdn.baidupcs.com/issue/netdisk/MACguanjia/BaiduNetdisk_mac_2.2.3.dmg", 25 | "http://m4.pc6.com/cjh3/VicomsoftFTPClient.dmg", 26 | "https://qd.myapp.com/myapp/qqteam/pcqq/QQ9.0.8_2.exe", 27 | "http://gxiami.alicdn.com/xiami-desktop/update/XiamiMac-03051058.dmg", 28 | "http://113.113.73.41/r/baiducdnct-gd.inter.iqiyi.com/cdn/pcclient/20190413/13/25/iQIYIMedia_005.dmg?dis_dz=CT-GuangDong_GuangZhou&dis_st=36", 29 | "http://pcclient.download.youku.com/ikumac/youkumac_1.6.7.04093.dmg?spm=a2hpd.20022519.m_235549.5!2~5~5~5!2~P!3~A&file=youkumac_1.6.7.04093.dmg", 30 | "http://dldir1.qq.com/qqfile/QQforMac/QQ_V4.2.4.dmg" 31 | ] 32 | 33 | 34 | setupManager() 35 | 36 | updateUI() 37 | tableView.reloadData() 38 | 39 | } 40 | } 41 | 42 | 43 | // MARK: - tap event 44 | extension ViewController2 { 45 | 46 | @IBAction func addDownloadTask(_ sender: Any) { 47 | let downloadURLStrings = sessionManager.tasks.map { $0.url.absoluteString } 48 | guard let URLString = URLStrings.first(where: { !downloadURLStrings.contains($0) }) else { return } 49 | 50 | sessionManager.download(URLString) { [weak self] _ in 51 | guard let self = self else { return } 52 | let index = self.sessionManager.tasks.count - 1 53 | self.tableView.insertRows(at: [IndexPath(row: index, section: 0)], with: .automatic) 54 | self.updateUI() 55 | } 56 | } 57 | 58 | @IBAction func deleteDownloadTask(_ sender: UIButton) { 59 | let count = sessionManager.tasks.count 60 | guard count > 0 else { return } 61 | let index = count - 1 62 | guard let task = sessionManager.tasks.safeObject(at: index) else { return } 63 | // tableView 刷新、 删除 task 都是异步的,如果操作过快会导致数据不一致,所以需要限制 button 的点击 64 | sender.isEnabled = false 65 | sessionManager.remove(task, completely: false) { [weak self] _ in 66 | self?.tableView.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic) 67 | self?.updateUI() 68 | sender.isEnabled = true 69 | } 70 | } 71 | 72 | 73 | @IBAction func sort(_ sender: Any) { 74 | sessionManager.tasksSort { (task1, task2) -> Bool in 75 | if task1.startDate < task2.startDate { 76 | return task1.startDate < task2.startDate 77 | } else { 78 | return task2.startDate < task1.startDate 79 | } 80 | } 81 | tableView.reloadData() 82 | } 83 | } 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /Demo/Demo/Tiercel-iOS-Demo/ViewController3.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController3.swift 3 | // Example 4 | // 5 | // Created by Daniels on 2018/3/16. 6 | // Copyright © 2018 Daniels. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Tiercel 11 | 12 | class ViewController3: BaseViewController { 13 | 14 | 15 | override func viewDidLoad() { 16 | 17 | sessionManager = appDelegate.sessionManager3 18 | 19 | super.viewDidLoad() 20 | 21 | URLStrings = (NSArray(contentsOfFile: Bundle.main.path(forResource: "VideoURLStrings.plist", ofType: nil)!) as! [String]) 22 | 23 | setupManager() 24 | 25 | sessionManager.logger.option = .none 26 | 27 | updateUI() 28 | tableView.reloadData() 29 | } 30 | } 31 | 32 | 33 | // MARK: - tap event 34 | extension ViewController3 { 35 | 36 | 37 | @IBAction func multiDownload(_ sender: Any) { 38 | guard sessionManager.tasks.count < URLStrings.count else { return } 39 | 40 | // 如果同时开启的下载任务过多,会阻塞主线程,所以可以在子线程中开启 41 | DispatchQueue.global().async { 42 | self.sessionManager?.multiDownload(self.URLStrings) { [weak self] _ in 43 | self?.updateUI() 44 | self?.tableView.reloadData() 45 | } 46 | } 47 | } 48 | } 49 | 50 | -------------------------------------------------------------------------------- /Demo/Tiercel-Demo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | E1EBF4F324E9993300697485 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1EBF4F224E9993300697485 /* AppDelegate.swift */; }; 11 | E1EBF4FA24E9993300697485 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E1EBF4F824E9993300697485 /* Main.storyboard */; }; 12 | E1EBF4FC24E9993500697485 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E1EBF4FB24E9993500697485 /* Assets.xcassets */; }; 13 | E1EBF4FF24E9993500697485 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E1EBF4FD24E9993500697485 /* LaunchScreen.storyboard */; }; 14 | E1EBF50824E99BAB00697485 /* VideoURLStrings.plist in Resources */ = {isa = PBXBuildFile; fileRef = E1EBF50724E99BAB00697485 /* VideoURLStrings.plist */; }; 15 | E1EBF51224E99BBC00697485 /* BaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1EBF50924E99BBB00697485 /* BaseViewController.swift */; }; 16 | E1EBF51324E99BBC00697485 /* DownloadViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1EBF50A24E99BBB00697485 /* DownloadViewController.swift */; }; 17 | E1EBF51424E99BBC00697485 /* DownloadTaskCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = E1EBF50B24E99BBB00697485 /* DownloadTaskCell.xib */; }; 18 | E1EBF51524E99BBC00697485 /* ViewController1.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1EBF50C24E99BBB00697485 /* ViewController1.swift */; }; 19 | E1EBF51624E99BBC00697485 /* ListViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1EBF50D24E99BBB00697485 /* ListViewCell.swift */; }; 20 | E1EBF51724E99BBC00697485 /* DownloadTaskCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1EBF50E24E99BBB00697485 /* DownloadTaskCell.swift */; }; 21 | E1EBF51824E99BBC00697485 /* ListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1EBF50F24E99BBB00697485 /* ListViewController.swift */; }; 22 | E1EBF51924E99BBC00697485 /* ViewController2.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1EBF51024E99BBB00697485 /* ViewController2.swift */; }; 23 | E1EBF51A24E99BBC00697485 /* ViewController3.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1EBF51124E99BBC00697485 /* ViewController3.swift */; }; 24 | E1EBF51D24E99C9900697485 /* Tiercel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E1EBF51C24E99C9900697485 /* Tiercel.framework */; }; 25 | E1EBF51E24E99C9900697485 /* Tiercel.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = E1EBF51C24E99C9900697485 /* Tiercel.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 26 | /* End PBXBuildFile section */ 27 | 28 | /* Begin PBXCopyFilesBuildPhase section */ 29 | E1EBF51F24E99C9900697485 /* Embed Frameworks */ = { 30 | isa = PBXCopyFilesBuildPhase; 31 | buildActionMask = 2147483647; 32 | dstPath = ""; 33 | dstSubfolderSpec = 10; 34 | files = ( 35 | E1EBF51E24E99C9900697485 /* Tiercel.framework in Embed Frameworks */, 36 | ); 37 | name = "Embed Frameworks"; 38 | runOnlyForDeploymentPostprocessing = 0; 39 | }; 40 | /* End PBXCopyFilesBuildPhase section */ 41 | 42 | /* Begin PBXFileReference section */ 43 | E1EBF4EF24E9993300697485 /* Tiercel-iOS-Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Tiercel-iOS-Demo.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 44 | E1EBF4F224E9993300697485 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 45 | E1EBF4F924E9993300697485 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 46 | E1EBF4FB24E9993500697485 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 47 | E1EBF4FE24E9993500697485 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 48 | E1EBF50024E9993500697485 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 49 | E1EBF50724E99BAB00697485 /* VideoURLStrings.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = VideoURLStrings.plist; sourceTree = ""; }; 50 | E1EBF50924E99BBB00697485 /* BaseViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseViewController.swift; sourceTree = ""; }; 51 | E1EBF50A24E99BBB00697485 /* DownloadViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadViewController.swift; sourceTree = ""; }; 52 | E1EBF50B24E99BBB00697485 /* DownloadTaskCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = DownloadTaskCell.xib; sourceTree = ""; }; 53 | E1EBF50C24E99BBB00697485 /* ViewController1.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController1.swift; sourceTree = ""; }; 54 | E1EBF50D24E99BBB00697485 /* ListViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListViewCell.swift; sourceTree = ""; }; 55 | E1EBF50E24E99BBB00697485 /* DownloadTaskCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadTaskCell.swift; sourceTree = ""; }; 56 | E1EBF50F24E99BBB00697485 /* ListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListViewController.swift; sourceTree = ""; }; 57 | E1EBF51024E99BBB00697485 /* ViewController2.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController2.swift; sourceTree = ""; }; 58 | E1EBF51124E99BBC00697485 /* ViewController3.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController3.swift; sourceTree = ""; }; 59 | E1EBF51C24E99C9900697485 /* Tiercel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Tiercel.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 60 | /* End PBXFileReference section */ 61 | 62 | /* Begin PBXFrameworksBuildPhase section */ 63 | E1EBF4EC24E9993300697485 /* Frameworks */ = { 64 | isa = PBXFrameworksBuildPhase; 65 | buildActionMask = 2147483647; 66 | files = ( 67 | E1EBF51D24E99C9900697485 /* Tiercel.framework in Frameworks */, 68 | ); 69 | runOnlyForDeploymentPostprocessing = 0; 70 | }; 71 | /* End PBXFrameworksBuildPhase section */ 72 | 73 | /* Begin PBXGroup section */ 74 | E1EBF4E624E9993300697485 = { 75 | isa = PBXGroup; 76 | children = ( 77 | E1EBF50624E9993C00697485 /* Demo */, 78 | E1EBF4F024E9993300697485 /* Products */, 79 | E1EBF51B24E99C9900697485 /* Frameworks */, 80 | ); 81 | sourceTree = ""; 82 | }; 83 | E1EBF4F024E9993300697485 /* Products */ = { 84 | isa = PBXGroup; 85 | children = ( 86 | E1EBF4EF24E9993300697485 /* Tiercel-iOS-Demo.app */, 87 | ); 88 | name = Products; 89 | sourceTree = ""; 90 | }; 91 | E1EBF4F124E9993300697485 /* Tiercel-iOS-Demo */ = { 92 | isa = PBXGroup; 93 | children = ( 94 | E1EBF4F224E9993300697485 /* AppDelegate.swift */, 95 | E1EBF50924E99BBB00697485 /* BaseViewController.swift */, 96 | E1EBF50C24E99BBB00697485 /* ViewController1.swift */, 97 | E1EBF51024E99BBB00697485 /* ViewController2.swift */, 98 | E1EBF51124E99BBC00697485 /* ViewController3.swift */, 99 | E1EBF50F24E99BBB00697485 /* ListViewController.swift */, 100 | E1EBF50D24E99BBB00697485 /* ListViewCell.swift */, 101 | E1EBF50A24E99BBB00697485 /* DownloadViewController.swift */, 102 | E1EBF50E24E99BBB00697485 /* DownloadTaskCell.swift */, 103 | E1EBF50B24E99BBB00697485 /* DownloadTaskCell.xib */, 104 | E1EBF4F824E9993300697485 /* Main.storyboard */, 105 | E1EBF4FB24E9993500697485 /* Assets.xcassets */, 106 | E1EBF4FD24E9993500697485 /* LaunchScreen.storyboard */, 107 | E1EBF50024E9993500697485 /* Info.plist */, 108 | E1EBF50724E99BAB00697485 /* VideoURLStrings.plist */, 109 | ); 110 | path = "Tiercel-iOS-Demo"; 111 | sourceTree = ""; 112 | }; 113 | E1EBF50624E9993C00697485 /* Demo */ = { 114 | isa = PBXGroup; 115 | children = ( 116 | E1EBF4F124E9993300697485 /* Tiercel-iOS-Demo */, 117 | ); 118 | path = Demo; 119 | sourceTree = ""; 120 | }; 121 | E1EBF51B24E99C9900697485 /* Frameworks */ = { 122 | isa = PBXGroup; 123 | children = ( 124 | E1EBF51C24E99C9900697485 /* Tiercel.framework */, 125 | ); 126 | name = Frameworks; 127 | sourceTree = ""; 128 | }; 129 | /* End PBXGroup section */ 130 | 131 | /* Begin PBXNativeTarget section */ 132 | E1EBF4EE24E9993300697485 /* Tiercel-iOS-Demo */ = { 133 | isa = PBXNativeTarget; 134 | buildConfigurationList = E1EBF50324E9993500697485 /* Build configuration list for PBXNativeTarget "Tiercel-iOS-Demo" */; 135 | buildPhases = ( 136 | E1EBF4EB24E9993300697485 /* Sources */, 137 | E1EBF4EC24E9993300697485 /* Frameworks */, 138 | E1EBF4ED24E9993300697485 /* Resources */, 139 | E1EBF51F24E99C9900697485 /* Embed Frameworks */, 140 | ); 141 | buildRules = ( 142 | ); 143 | dependencies = ( 144 | ); 145 | name = "Tiercel-iOS-Demo"; 146 | productName = "Tiercel-Demo"; 147 | productReference = E1EBF4EF24E9993300697485 /* Tiercel-iOS-Demo.app */; 148 | productType = "com.apple.product-type.application"; 149 | }; 150 | /* End PBXNativeTarget section */ 151 | 152 | /* Begin PBXProject section */ 153 | E1EBF4E724E9993300697485 /* Project object */ = { 154 | isa = PBXProject; 155 | attributes = { 156 | LastSwiftUpdateCheck = 1160; 157 | LastUpgradeCheck = 1160; 158 | ORGANIZATIONNAME = Daniels; 159 | TargetAttributes = { 160 | E1EBF4EE24E9993300697485 = { 161 | CreatedOnToolsVersion = 11.6; 162 | }; 163 | }; 164 | }; 165 | buildConfigurationList = E1EBF4EA24E9993300697485 /* Build configuration list for PBXProject "Tiercel-Demo" */; 166 | compatibilityVersion = "Xcode 9.3"; 167 | developmentRegion = en; 168 | hasScannedForEncodings = 0; 169 | knownRegions = ( 170 | en, 171 | Base, 172 | ); 173 | mainGroup = E1EBF4E624E9993300697485; 174 | productRefGroup = E1EBF4F024E9993300697485 /* Products */; 175 | projectDirPath = ""; 176 | projectRoot = ""; 177 | targets = ( 178 | E1EBF4EE24E9993300697485 /* Tiercel-iOS-Demo */, 179 | ); 180 | }; 181 | /* End PBXProject section */ 182 | 183 | /* Begin PBXResourcesBuildPhase section */ 184 | E1EBF4ED24E9993300697485 /* Resources */ = { 185 | isa = PBXResourcesBuildPhase; 186 | buildActionMask = 2147483647; 187 | files = ( 188 | E1EBF51424E99BBC00697485 /* DownloadTaskCell.xib in Resources */, 189 | E1EBF4FF24E9993500697485 /* LaunchScreen.storyboard in Resources */, 190 | E1EBF4FC24E9993500697485 /* Assets.xcassets in Resources */, 191 | E1EBF4FA24E9993300697485 /* Main.storyboard in Resources */, 192 | E1EBF50824E99BAB00697485 /* VideoURLStrings.plist in Resources */, 193 | ); 194 | runOnlyForDeploymentPostprocessing = 0; 195 | }; 196 | /* End PBXResourcesBuildPhase section */ 197 | 198 | /* Begin PBXSourcesBuildPhase section */ 199 | E1EBF4EB24E9993300697485 /* Sources */ = { 200 | isa = PBXSourcesBuildPhase; 201 | buildActionMask = 2147483647; 202 | files = ( 203 | E1EBF51524E99BBC00697485 /* ViewController1.swift in Sources */, 204 | E1EBF51924E99BBC00697485 /* ViewController2.swift in Sources */, 205 | E1EBF51A24E99BBC00697485 /* ViewController3.swift in Sources */, 206 | E1EBF4F324E9993300697485 /* AppDelegate.swift in Sources */, 207 | E1EBF51724E99BBC00697485 /* DownloadTaskCell.swift in Sources */, 208 | E1EBF51624E99BBC00697485 /* ListViewCell.swift in Sources */, 209 | E1EBF51324E99BBC00697485 /* DownloadViewController.swift in Sources */, 210 | E1EBF51224E99BBC00697485 /* BaseViewController.swift in Sources */, 211 | E1EBF51824E99BBC00697485 /* ListViewController.swift in Sources */, 212 | ); 213 | runOnlyForDeploymentPostprocessing = 0; 214 | }; 215 | /* End PBXSourcesBuildPhase section */ 216 | 217 | /* Begin PBXVariantGroup section */ 218 | E1EBF4F824E9993300697485 /* Main.storyboard */ = { 219 | isa = PBXVariantGroup; 220 | children = ( 221 | E1EBF4F924E9993300697485 /* Base */, 222 | ); 223 | name = Main.storyboard; 224 | sourceTree = ""; 225 | }; 226 | E1EBF4FD24E9993500697485 /* LaunchScreen.storyboard */ = { 227 | isa = PBXVariantGroup; 228 | children = ( 229 | E1EBF4FE24E9993500697485 /* Base */, 230 | ); 231 | name = LaunchScreen.storyboard; 232 | sourceTree = ""; 233 | }; 234 | /* End PBXVariantGroup section */ 235 | 236 | /* Begin XCBuildConfiguration section */ 237 | E1EBF50124E9993500697485 /* Debug */ = { 238 | isa = XCBuildConfiguration; 239 | buildSettings = { 240 | ALWAYS_SEARCH_USER_PATHS = NO; 241 | CLANG_ANALYZER_NONNULL = YES; 242 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 243 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 244 | CLANG_CXX_LIBRARY = "libc++"; 245 | CLANG_ENABLE_MODULES = YES; 246 | CLANG_ENABLE_OBJC_ARC = YES; 247 | CLANG_ENABLE_OBJC_WEAK = YES; 248 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 249 | CLANG_WARN_BOOL_CONVERSION = YES; 250 | CLANG_WARN_COMMA = YES; 251 | CLANG_WARN_CONSTANT_CONVERSION = YES; 252 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 253 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 254 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 255 | CLANG_WARN_EMPTY_BODY = YES; 256 | CLANG_WARN_ENUM_CONVERSION = YES; 257 | CLANG_WARN_INFINITE_RECURSION = YES; 258 | CLANG_WARN_INT_CONVERSION = YES; 259 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 260 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 261 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 262 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 263 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 264 | CLANG_WARN_STRICT_PROTOTYPES = YES; 265 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 266 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 267 | CLANG_WARN_UNREACHABLE_CODE = YES; 268 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 269 | COPY_PHASE_STRIP = NO; 270 | DEBUG_INFORMATION_FORMAT = dwarf; 271 | ENABLE_STRICT_OBJC_MSGSEND = YES; 272 | ENABLE_TESTABILITY = YES; 273 | GCC_C_LANGUAGE_STANDARD = gnu11; 274 | GCC_DYNAMIC_NO_PIC = NO; 275 | GCC_NO_COMMON_BLOCKS = YES; 276 | GCC_OPTIMIZATION_LEVEL = 0; 277 | GCC_PREPROCESSOR_DEFINITIONS = ( 278 | "DEBUG=1", 279 | "$(inherited)", 280 | ); 281 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 282 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 283 | GCC_WARN_UNDECLARED_SELECTOR = YES; 284 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 285 | GCC_WARN_UNUSED_FUNCTION = YES; 286 | GCC_WARN_UNUSED_VARIABLE = YES; 287 | IPHONEOS_DEPLOYMENT_TARGET = 13.6; 288 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 289 | MTL_FAST_MATH = YES; 290 | ONLY_ACTIVE_ARCH = YES; 291 | SDKROOT = iphoneos; 292 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 293 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 294 | }; 295 | name = Debug; 296 | }; 297 | E1EBF50224E9993500697485 /* Release */ = { 298 | isa = XCBuildConfiguration; 299 | buildSettings = { 300 | ALWAYS_SEARCH_USER_PATHS = NO; 301 | CLANG_ANALYZER_NONNULL = YES; 302 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 303 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 304 | CLANG_CXX_LIBRARY = "libc++"; 305 | CLANG_ENABLE_MODULES = YES; 306 | CLANG_ENABLE_OBJC_ARC = YES; 307 | CLANG_ENABLE_OBJC_WEAK = YES; 308 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 309 | CLANG_WARN_BOOL_CONVERSION = YES; 310 | CLANG_WARN_COMMA = YES; 311 | CLANG_WARN_CONSTANT_CONVERSION = YES; 312 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 313 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 314 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 315 | CLANG_WARN_EMPTY_BODY = YES; 316 | CLANG_WARN_ENUM_CONVERSION = YES; 317 | CLANG_WARN_INFINITE_RECURSION = YES; 318 | CLANG_WARN_INT_CONVERSION = YES; 319 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 320 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 321 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 322 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 323 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 324 | CLANG_WARN_STRICT_PROTOTYPES = YES; 325 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 326 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 327 | CLANG_WARN_UNREACHABLE_CODE = YES; 328 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 329 | COPY_PHASE_STRIP = NO; 330 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 331 | ENABLE_NS_ASSERTIONS = NO; 332 | ENABLE_STRICT_OBJC_MSGSEND = YES; 333 | GCC_C_LANGUAGE_STANDARD = gnu11; 334 | GCC_NO_COMMON_BLOCKS = YES; 335 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 336 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 337 | GCC_WARN_UNDECLARED_SELECTOR = YES; 338 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 339 | GCC_WARN_UNUSED_FUNCTION = YES; 340 | GCC_WARN_UNUSED_VARIABLE = YES; 341 | IPHONEOS_DEPLOYMENT_TARGET = 13.6; 342 | MTL_ENABLE_DEBUG_INFO = NO; 343 | MTL_FAST_MATH = YES; 344 | SDKROOT = iphoneos; 345 | SWIFT_COMPILATION_MODE = wholemodule; 346 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 347 | VALIDATE_PRODUCT = YES; 348 | }; 349 | name = Release; 350 | }; 351 | E1EBF50424E9993500697485 /* Debug */ = { 352 | isa = XCBuildConfiguration; 353 | buildSettings = { 354 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 355 | CODE_SIGN_STYLE = Automatic; 356 | DEVELOPMENT_TEAM = Y3437T5EGX; 357 | INFOPLIST_FILE = "$(SRCROOT)/Demo/Tiercel-iOS-Demo/Info.plist"; 358 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 359 | LD_RUNPATH_SEARCH_PATHS = ( 360 | "$(inherited)", 361 | "@executable_path/Frameworks", 362 | ); 363 | PRODUCT_BUNDLE_IDENTIFIER = "com.Daniels.Tiercel-iOS-Demo"; 364 | PRODUCT_NAME = "Tiercel-iOS-Demo"; 365 | SWIFT_VERSION = 5.0; 366 | TARGETED_DEVICE_FAMILY = "1,2"; 367 | }; 368 | name = Debug; 369 | }; 370 | E1EBF50524E9993500697485 /* Release */ = { 371 | isa = XCBuildConfiguration; 372 | buildSettings = { 373 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 374 | CODE_SIGN_STYLE = Automatic; 375 | DEVELOPMENT_TEAM = Y3437T5EGX; 376 | INFOPLIST_FILE = "$(SRCROOT)/Demo/Tiercel-iOS-Demo/Info.plist"; 377 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 378 | LD_RUNPATH_SEARCH_PATHS = ( 379 | "$(inherited)", 380 | "@executable_path/Frameworks", 381 | ); 382 | PRODUCT_BUNDLE_IDENTIFIER = "com.Daniels.Tiercel-iOS-Demo"; 383 | PRODUCT_NAME = "Tiercel-iOS-Demo"; 384 | SWIFT_VERSION = 5.0; 385 | TARGETED_DEVICE_FAMILY = "1,2"; 386 | }; 387 | name = Release; 388 | }; 389 | /* End XCBuildConfiguration section */ 390 | 391 | /* Begin XCConfigurationList section */ 392 | E1EBF4EA24E9993300697485 /* Build configuration list for PBXProject "Tiercel-Demo" */ = { 393 | isa = XCConfigurationList; 394 | buildConfigurations = ( 395 | E1EBF50124E9993500697485 /* Debug */, 396 | E1EBF50224E9993500697485 /* Release */, 397 | ); 398 | defaultConfigurationIsVisible = 0; 399 | defaultConfigurationName = Release; 400 | }; 401 | E1EBF50324E9993500697485 /* Build configuration list for PBXNativeTarget "Tiercel-iOS-Demo" */ = { 402 | isa = XCConfigurationList; 403 | buildConfigurations = ( 404 | E1EBF50424E9993500697485 /* Debug */, 405 | E1EBF50524E9993500697485 /* Release */, 406 | ); 407 | defaultConfigurationIsVisible = 0; 408 | defaultConfigurationName = Release; 409 | }; 410 | /* End XCConfigurationList section */ 411 | }; 412 | rootObject = E1EBF4E724E9993300697485 /* Project object */; 413 | } 414 | -------------------------------------------------------------------------------- /Demo/Tiercel-Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo/Tiercel-Demo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Demo/Tiercel-Demo.xcodeproj/xcshareddata/xcschemes/Tiercel-iOS-Demo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Images/1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Danie1s/Tiercel/7a0c4159651062911651917f892e5dc0a9caf8bb/Images/1.gif -------------------------------------------------------------------------------- /Images/2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Danie1s/Tiercel/7a0c4159651062911651917f892e5dc0a9caf8bb/Images/2.gif -------------------------------------------------------------------------------- /Images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Danie1s/Tiercel/7a0c4159651062911651917f892e5dc0a9caf8bb/Images/logo.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 176516837@qq.com <176516837@qq.com> 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Tiercel", 8 | platforms: [.iOS(.v12)], 9 | products: [ 10 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 11 | .library( 12 | name: "Tiercel", 13 | targets: ["Tiercel"]), 14 | ], 15 | targets: [ 16 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 17 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 18 | .target( 19 | name: "Tiercel", 20 | path: "Sources") 21 | ], 22 | swiftLanguageVersions: [.v5] 23 | ) 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | [![Version](https://img.shields.io/cocoapods/v/Tiercel.svg?style=flat)](http://cocoapods.org/pods/Tiercel) 6 | [![Platform](https://img.shields.io/cocoapods/p/Tiercel.svg?style=flat)](http://cocoapods.org/pods/Tiercel) 7 | [![Language](https://img.shields.io/badge/language-swift-red.svg?style=flat)]() 8 | [![SPM](https://img.shields.io/badge/SPM-supported-DE5C43.svg?style=flat)](https://swift.org/package-manager/) 9 | [![Support](https://img.shields.io/badge/support-iOS%2010%2B%20-brightgreen.svg?style=flat)](https://www.apple.com/nl/ios/) 10 | [![License](https://img.shields.io/cocoapods/l/Tiercel.svg?style=flat)](http://cocoapods.org/pods/Tiercel) 11 | 12 | Tiercel 是一个简单易用、功能丰富的纯 Swift 下载框架,支持原生级别后台下载,拥有强大的任务管理功能,可以满足下载类 APP 的大部分需求。 13 | 14 | 如果你使用的开发语言是 Objective-C ,可以使用 [TiercelObjCBridge](https://github.com/Danie1s/TiercelObjCBridge) 进行桥接 15 | 16 | - [Tiercel 3.0](#tiercel-30) 17 | - [特性](#特性) 18 | - [环境要求](#环境要求) 19 | - [集成](#集成) 20 | - [Demo](#demo) 21 | - [用法](#用法) 22 | - [基本用法](#基本用法) 23 | - [后台下载](#后台下载) 24 | - [文件校验](#文件校验) 25 | - [更多](#更多) 26 | - [License](#license) 27 | 28 | 29 | 30 | ## Tiercel 3.0 31 | 32 | Tiercel 3.0 大幅提高了性能,拥有更完善的错误处理,提供了更多方便的 API。从 Tiercel 2.0 升级到 Tiercel 3.0 是很简单的,强烈推荐所有开发者都进行升级,具体请查看 [Tiercel 3.0 迁移指南](https://github.com/Danie1s/Tiercel/wiki/Tiercel-3.0-%E8%BF%81%E7%A7%BB%E6%8C%87%E5%8D%97) 33 | 34 | ## 特性 35 | 36 | - [x] 支持原生级别的后台下载 37 | - [x] 支持离线断点续传,App 无论 crash 还是被手动 Kill 都可以恢复下载 38 | - [x] 拥有精细的任务管理,每个下载任务都可以单独操作和管理 39 | - [x] 支持创建多个下载模块,每个模块互不影响 40 | - [x] 每个下载模块拥有单独的管理者,可以对总任务进行操作和管理 41 | - [x] 支持批量操作 42 | - [x] 内置了下载速度、剩余时间等常见的下载信息 43 | - [x] 支持自定义日志 44 | - [x] 支持下载任务排序 45 | - [x] 链式语法调用 46 | - [x] 支持控制下载任务的最大并发数 47 | - [x] 支持文件校验 48 | - [x] 线程安全 49 | 50 | 51 | 52 | ## 环境要求 53 | 54 | - iOS 12.0+ 55 | - Xcode 15.0+ 56 | - Swift 5.0+ 57 | 58 | 59 | 60 | ## 安装 61 | 62 | ### CocoaPods 63 | 64 | Tiercel 支持 CocoaPods 集成,首先需要使用以下命令安装 CocoaPod: 65 | 66 | ```bash 67 | $ gem install cocoapods 68 | ``` 69 | 70 | 在`Podfile`文件中 71 | 72 | ```ruby 73 | source 'https://github.com/CocoaPods/Specs.git' 74 | platform :ios, '10.0' 75 | use_frameworks! 76 | 77 | target '' do 78 | pod 'Tiercel' 79 | end 80 | ``` 81 | 82 | 最后运行命令 83 | 84 | ```bash 85 | $ pod install 86 | ``` 87 | 88 | ### Swift Package Manager 89 | 90 | 从 Xcode 11 开始,集成了 Swift Package Manager,使用起来非常方便。Tiercel 也支持通过 Swift Package Manager 集成。 91 | 92 | 在 Xcode 的菜单栏中选择 `File > Swift Packages > Add Pacakage Dependency`,然后在搜索栏输入 93 | 94 | `git@github.com:Danie1s/Tiercel.git`,即可完成集成 95 | 96 | ### 手动集成 97 | 98 | Tiercel 也支持手动集成,只需把本项目文件夹中的`Tiercel`文件夹拖进需要集成的项目即可 99 | 100 | 101 | 102 | ## Demo 103 | 104 | 打开本项目文件夹中 `Tiercel.xcodeproj` ,可以直接运行 Demo 105 | 106 | 107 | 108 | 109 | 110 | ## 用法 111 | 112 | ### 基本用法 113 | 114 | 一行代码开启下载 115 | 116 | ```swift 117 | // 创建下载任务并且开启下载,同时返回可选类型的DownloadTask实例,如果url无效,则返回nil 118 | let task = sessionManager.download("http://dldir1.qq.com/qqfile/QQforMac/QQ_V4.2.4.dmg") 119 | 120 | // 批量创建下载任务并且开启下载,返回有效url对应的任务数组,urls需要跟fileNames一一对应 121 | let tasks = sessionManager.multiDownload(URLStrings) 122 | ``` 123 | 124 | 可以对任务设置状态回调 125 | 126 | ```swift 127 | let task = sessionManager.download("http://dldir1.qq.com/qqfile/QQforMac/QQ_V4.2.4.dmg") 128 | 129 | task?.progress(onMainQueue: true) { (task) in 130 | let progress = task.progress.fractionCompleted 131 | print("下载中, 进度:\(progress)") 132 | }.success { (task) in 133 | print("下载完成") 134 | }.failure { (task) in 135 | print("下载失败") 136 | } 137 | ``` 138 | 139 | 可以通过 URL 对下载任务进行操作,也可以直接操作下载任务 140 | 141 | ```swift 142 | let URLString = "http://dldir1.qq.com/qqfile/QQforMac/QQ_V4.2.4.dmg" 143 | 144 | // 通过 URL 对下载任务进行操作 145 | sessionManager.start(URLString) 146 | sessionManager.suspend(URLString) 147 | sessionManager.cancel(URLString) 148 | sessionManager.remove(URLString, completely: false) 149 | 150 | // 直接对下载任务进行操作 151 | sessionManager.start(task) 152 | sessionManager.suspend(task) 153 | sessionManager.cancel(task) 154 | sessionManager.remove(task, completely: false) 155 | ``` 156 | 157 | 158 | 159 | ### 后台下载 160 | 161 | 从 Tiercel 2.0 开始支持原生的后台下载,只要使用 Tiercel 开启了下载任务: 162 | 163 | - 手动 Kill App,任务会暂停,重启 App 后可以恢复进度,继续下载 164 | - 只要不是手动 Kill App,任务都会一直在下载,例如: 165 | - App 退回后台 166 | - App 崩溃或者被系统关闭 167 | - 重启手机 168 | 169 | 如果想了解后台下载的细节和注意事项,可以查看:[iOS 原生级别后台下载详解](https://github.com/Danie1s/Tiercel/wiki/iOS-%E5%8E%9F%E7%94%9F%E7%BA%A7%E5%88%AB%E5%90%8E%E5%8F%B0%E4%B8%8B%E8%BD%BD%E8%AF%A6%E8%A7%A3) 170 | 171 | 172 | 173 | ### 文件校验 174 | 175 | Tiercel 提供了文件校验功能,可以根据需要添加,校验结果在回调的`task.validation`里 176 | 177 | ```swift 178 | 179 | let task = sessionManager.download("http://dldir1.qq.com/qqfile/QQforMac/QQ_V4.2.4.dmg") 180 | // 回调闭包可以选择是否在主线程上执行 181 | task?.validateFile(code: "9e2a3650530b563da297c9246acaad5c", 182 | type: .md5, 183 | onMainQueue: true) 184 | { (task) in 185 | if task.validation == .correct { 186 | // 文件正确 187 | } else { 188 | // 文件错误 189 | } 190 | } 191 | ``` 192 | 193 | 194 | 195 | ### 更多 196 | 197 | 有关 Tiercel 3.0 的详细使用方法和升级迁移,请查看 [Wiki](https://github.com/Danie1s/Tiercel/wiki) 198 | 199 | 200 | 201 | 202 | ## License 203 | 204 | Tiercel is available under the MIT license. See the LICENSE file for more info. 205 | 206 | -------------------------------------------------------------------------------- /Sources/Extensions/Array+Safe.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Array+Safe.swift 3 | // Tiercel 4 | // 5 | // Created by Daniels on 2019/1/22. 6 | // Copyright © 2019 Daniels. All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | 29 | 30 | extension Array { 31 | public func safeObject(at index: Int) -> Element? { 32 | if (0..()) { 33 | if Thread.isMainThread { 34 | block() 35 | } else { 36 | DispatchQueue.main.async { 37 | block() 38 | } 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /Sources/Extensions/Double+TaskInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Double+TaskInfo.swift 3 | // Tiercel 4 | // 5 | // Created by Daniels on 2019/1/22. 6 | // Copyright © 2019 Daniels. All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | 29 | 30 | extension Double: TiercelCompatible {} 31 | extension TiercelWrapper where Base == Double { 32 | /// 返回 yyyy-MM-dd HH:mm:ss格式的字符串 33 | /// 34 | /// - Returns: 35 | public func convertTimeToDateString() -> String { 36 | let date = Date(timeIntervalSince1970: base) 37 | let formatter = DateFormatter() 38 | formatter.dateFormat = "yyyy-MM-dd HH:mm:ss" 39 | return formatter.string(from: date) 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /Sources/Extensions/FileManager+AvailableCapacity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileManager+AvailableCapacity.swift 3 | // Tiercel 4 | // 5 | // Created by Daniels on 2019/1/22. 6 | // Copyright © 2019 Daniels. All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | 29 | extension FileManager: TiercelCompatible {} 30 | extension TiercelWrapper where Base: FileManager { 31 | public var freeDiskSpaceInBytes: Int64 { 32 | if #available(macOS 10.13, iOS 11.0, *) { 33 | if let space = try? URL(fileURLWithPath: NSHomeDirectory()).resourceValues(forKeys: [URLResourceKey.volumeAvailableCapacityForImportantUsageKey]).volumeAvailableCapacityForImportantUsage { 34 | return space 35 | } else { 36 | return 0 37 | } 38 | } else { 39 | if let systemAttributes = try? base.attributesOfFileSystem(forPath: NSHomeDirectory()), 40 | let freeSpace = (systemAttributes[FileAttributeKey.systemFreeSize] as? NSNumber)?.int64Value { 41 | return freeSpace 42 | } else { 43 | return 0 44 | } 45 | } 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /Sources/Extensions/Int64+TaskInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Int64+TaskInfo.swift 3 | // Tiercel 4 | // 5 | // Created by Daniels on 2019/1/22. 6 | // Copyright © 2019 Daniels. All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | 29 | extension Int64: TiercelCompatible {} 30 | extension TiercelWrapper where Base == Int64 { 31 | 32 | /// 返回下载速度的字符串,如:1MB/s 33 | /// 34 | /// - Returns: 35 | public func convertSpeedToString() -> String { 36 | let size = convertBytesToString() 37 | return [size, "s"].joined(separator: "/") 38 | } 39 | 40 | /// 返回 00:00格式的字符串 41 | /// 42 | /// - Returns: 43 | public func convertTimeToString() -> String { 44 | let formatter = DateComponentsFormatter() 45 | 46 | formatter.unitsStyle = .positional 47 | 48 | return formatter.string(from: TimeInterval(base)) ?? "" 49 | } 50 | 51 | /// 返回字节大小的字符串 52 | /// 53 | /// - Returns: 54 | public func convertBytesToString() -> String { 55 | return ByteCountFormatter.string(fromByteCount: base, countStyle: .file) 56 | } 57 | 58 | 59 | } 60 | -------------------------------------------------------------------------------- /Sources/Extensions/OperationQueue+DispatchQueue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OperationQueue+DispatchQueue.swift 3 | // Tiercel 4 | // 5 | // Created by Daniels on 2019/4/30. 6 | // Copyright © 2019 Daniels. All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | 29 | extension OperationQueue { 30 | convenience init(qualityOfService: QualityOfService = .default, 31 | maxConcurrentOperationCount: Int = OperationQueue.defaultMaxConcurrentOperationCount, 32 | underlyingQueue: DispatchQueue? = nil, 33 | name: String? = nil) { 34 | self.init() 35 | self.qualityOfService = qualityOfService 36 | self.maxConcurrentOperationCount = maxConcurrentOperationCount 37 | self.underlyingQueue = underlyingQueue 38 | self.name = name 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/Extensions/String+Hash.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Hash.swift 3 | // Tiercel 4 | // 5 | // Created by Daniels on 2019/1/22. 6 | // Copyright © 2019 Daniels. All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | 29 | 30 | extension String: TiercelCompatible { } 31 | extension TiercelWrapper where Base == String { 32 | public var md5: String { 33 | guard let data = base.data(using: .utf8) else { 34 | return base 35 | } 36 | return data.tr.md5 37 | } 38 | 39 | public var sha1: String { 40 | guard let data = base.data(using: .utf8) else { 41 | return base 42 | } 43 | return data.tr.sha1 44 | } 45 | 46 | public var sha256: String { 47 | guard let data = base.data(using: .utf8) else { 48 | return base 49 | } 50 | return data.tr.sha256 51 | } 52 | 53 | public var sha512: String { 54 | guard let data = base.data(using: .utf8) else { 55 | return base 56 | } 57 | return data.tr.sha512 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/Extensions/URLSession+ResumeData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLSession+ResumeData.swift 3 | // Tiercel 4 | // 5 | // Created by Daniels on 2019/1/22. 6 | // Copyright © 2019 Daniels. All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | 29 | extension URLSession { 30 | 31 | /// 把有bug的resumeData修复,然后创建task 32 | /// 33 | /// - Parameter resumeData: 34 | /// - Returns: 35 | internal func correctedDownloadTask(withResumeData resumeData: Data) -> URLSessionDownloadTask { 36 | 37 | let task = downloadTask(withResumeData: resumeData) 38 | 39 | if let resumeDictionary = ResumeDataHelper.getResumeDictionary(resumeData) { 40 | if task.originalRequest == nil, let originalReqData = resumeDictionary[ResumeDataHelper.originalRequestKey] as? Data, let originalRequest = NSKeyedUnarchiver.unarchiveObject(with: originalReqData) as? NSURLRequest { 41 | task.setValue(originalRequest, forKey: "originalRequest") 42 | } 43 | if task.currentRequest == nil, let currentReqData = resumeDictionary[ResumeDataHelper.currentRequestKey] as? Data, let currentRequest = NSKeyedUnarchiver.unarchiveObject(with: currentReqData) as? NSURLRequest { 44 | task.setValue(currentRequest, forKey: "currentRequest") 45 | } 46 | } 47 | 48 | return task 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/General/Cache.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Cache.swift 3 | // Tiercel 4 | // 5 | // Created by Daniels on 2018/3/16. 6 | // Copyright © 2018 Daniels. All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | 29 | public class Cache { 30 | 31 | private let ioQueue: DispatchQueue 32 | 33 | private var debouncer: Debouncer 34 | 35 | public let downloadPath: String 36 | 37 | public let downloadTmpPath: String 38 | 39 | public let downloadFilePath: String 40 | 41 | public let identifier: String 42 | 43 | private let fileManager = FileManager.default 44 | 45 | private let encoder = PropertyListEncoder() 46 | 47 | internal weak var manager: SessionManager? 48 | 49 | private let decoder = PropertyListDecoder() 50 | 51 | public static func defaultDiskCachePathClosure(_ cacheName: String) -> String { 52 | let dstPath = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first! 53 | return (dstPath as NSString).appendingPathComponent(cacheName) 54 | } 55 | 56 | 57 | /// 初始化方法 58 | /// - Parameters: 59 | /// - identifier: 不同的identifier代表不同的下载模块。如果没有自定义下载目录,Cache会提供默认的目录,这些目录跟identifier相关 60 | /// - downloadPath: 存放用于DownloadTask持久化的数据,默认提供的downloadTmpPath、downloadFilePath也是在里面 61 | /// - downloadTmpPath: 存放下载中的临时文件 62 | /// - downloadFilePath: 存放下载完成后的文件 63 | public init(_ identifier: String, downloadPath: String? = nil, downloadTmpPath: String? = nil, downloadFilePath: String? = nil) { 64 | self.identifier = identifier 65 | 66 | let ioQueueName = "com.Tiercel.Cache.ioQueue.\(identifier)" 67 | ioQueue = DispatchQueue(label: ioQueueName, autoreleaseFrequency: .workItem) 68 | 69 | debouncer = Debouncer(queue: ioQueue) 70 | 71 | let cacheName = "com.Daniels.Tiercel.Cache.\(identifier)" 72 | 73 | let diskCachePath = Cache.defaultDiskCachePathClosure(cacheName) 74 | 75 | let path = downloadPath ?? (diskCachePath as NSString).appendingPathComponent("Downloads") 76 | 77 | self.downloadPath = path 78 | 79 | self.downloadTmpPath = downloadTmpPath ?? (path as NSString).appendingPathComponent("Tmp") 80 | 81 | self.downloadFilePath = downloadFilePath ?? (path as NSString).appendingPathComponent("File") 82 | 83 | createDirectory() 84 | 85 | decoder.userInfo[.cache] = self 86 | 87 | } 88 | 89 | public func invalidate() { 90 | decoder.userInfo[.cache] = nil 91 | } 92 | } 93 | 94 | 95 | // MARK: - file 96 | extension Cache { 97 | internal func createDirectory() { 98 | 99 | if !fileManager.fileExists(atPath: downloadPath) { 100 | do { 101 | try fileManager.createDirectory(atPath: downloadPath, withIntermediateDirectories: true, attributes: nil) 102 | } catch { 103 | manager?.log(.error("create directory failed", 104 | error: TiercelError.cacheError(reason: .cannotCreateDirectory(path: downloadPath, 105 | error: error)))) 106 | } 107 | } 108 | 109 | if !fileManager.fileExists(atPath: downloadTmpPath) { 110 | do { 111 | try fileManager.createDirectory(atPath: downloadTmpPath, withIntermediateDirectories: true, attributes: nil) 112 | } catch { 113 | manager?.log(.error("create directory failed", 114 | error: TiercelError.cacheError(reason: .cannotCreateDirectory(path: downloadTmpPath, 115 | error: error)))) 116 | } 117 | } 118 | 119 | if !fileManager.fileExists(atPath: downloadFilePath) { 120 | do { 121 | try fileManager.createDirectory(atPath: downloadFilePath, withIntermediateDirectories: true, attributes: nil) 122 | } catch { 123 | manager?.log(.error("create directory failed", 124 | error: TiercelError.cacheError(reason: .cannotCreateDirectory(path: downloadFilePath, 125 | error: error)))) 126 | } 127 | } 128 | } 129 | 130 | 131 | public func filePath(fileName: String) -> String? { 132 | if fileName.isEmpty { 133 | return nil 134 | } 135 | let path = (downloadFilePath as NSString).appendingPathComponent(fileName) 136 | return path 137 | } 138 | 139 | public func fileURL(fileName: String) -> URL? { 140 | guard let path = filePath(fileName: fileName) else { return nil } 141 | return URL(fileURLWithPath: path) 142 | } 143 | 144 | public func fileExists(fileName: String) -> Bool { 145 | guard let path = filePath(fileName: fileName) else { return false } 146 | return fileManager.fileExists(atPath: path) 147 | } 148 | 149 | public func filePath(url: URLConvertible) -> String? { 150 | do { 151 | let validURL = try url.asURL() 152 | let fileName = validURL.tr.fileName 153 | return filePath(fileName: fileName) 154 | } catch { 155 | return nil 156 | } 157 | } 158 | 159 | public func fileURL(url: URLConvertible) -> URL? { 160 | guard let path = filePath(url: url) else { return nil } 161 | return URL(fileURLWithPath: path) 162 | } 163 | 164 | public func fileExists(url: URLConvertible) -> Bool { 165 | guard let path = filePath(url: url) else { return false } 166 | return fileManager.fileExists(atPath: path) 167 | } 168 | 169 | 170 | 171 | public func clearDiskCache(onMainQueue: Bool = true, handler: Handler? = nil) { 172 | ioQueue.async { 173 | guard self.fileManager.fileExists(atPath: self.downloadPath) else { return } 174 | do { 175 | try self.fileManager.removeItem(atPath: self.downloadPath) 176 | } catch { 177 | self.manager?.log(.error("clear disk cache failed", 178 | error: TiercelError.cacheError(reason: .cannotRemoveItem(path: self.downloadPath, 179 | error: error)))) 180 | } 181 | self.createDirectory() 182 | if let handler = handler { 183 | Executer(onMainQueue: onMainQueue, handler: handler).execute(self) 184 | } 185 | } 186 | } 187 | } 188 | 189 | 190 | // MARK: - retrieve 191 | extension Cache { 192 | internal func retrieveAllTasks() -> [DownloadTask] { 193 | return ioQueue.sync { 194 | let path = (downloadPath as NSString).appendingPathComponent("\(identifier)_Tasks.plist") 195 | if fileManager.fileExists(atPath: path) { 196 | do { 197 | let url = URL(fileURLWithPath: path) 198 | let data = try Data(contentsOf: url) 199 | let tasks = try decoder.decode([DownloadTask].self, from: data) 200 | tasks.forEach { (task) in 201 | task.cache = self 202 | if task.status == .waiting { 203 | task.protectedState.write { $0.status = .suspended } 204 | } 205 | } 206 | return tasks 207 | } catch { 208 | manager?.log(.error("retrieve all tasks failed", error: TiercelError.cacheError(reason: .cannotRetrieveAllTasks(path: path, error: error)))) 209 | return [DownloadTask]() 210 | } 211 | } else { 212 | return [DownloadTask]() 213 | } 214 | } 215 | } 216 | 217 | internal func retrieveTmpFile(_ tmpFileName: String?) -> Bool { 218 | return ioQueue.sync { 219 | guard let tmpFileName = tmpFileName, !tmpFileName.isEmpty else { return false } 220 | let backupFilePath = (downloadTmpPath as NSString).appendingPathComponent(tmpFileName) 221 | let originFilePath = (NSTemporaryDirectory() as NSString).appendingPathComponent(tmpFileName) 222 | let backupFileExists = fileManager.fileExists(atPath: backupFilePath) 223 | let originFileExists = fileManager.fileExists(atPath: originFilePath) 224 | guard backupFileExists || originFileExists else { return false } 225 | 226 | if originFileExists { 227 | do { 228 | try fileManager.removeItem(atPath: backupFilePath) 229 | } catch { 230 | self.manager?.log(.error("retrieve tmpFile failed", 231 | error: TiercelError.cacheError(reason: .cannotRemoveItem(path: backupFilePath, 232 | error: error)))) 233 | } 234 | } else { 235 | do { 236 | try fileManager.moveItem(atPath: backupFilePath, toPath: originFilePath) 237 | } catch { 238 | self.manager?.log(.error("retrieve tmpFile failed", 239 | error: TiercelError.cacheError(reason: .cannotMoveItem(atPath: backupFilePath, 240 | toPath: originFilePath, 241 | error: error)))) 242 | } 243 | } 244 | return true 245 | } 246 | 247 | } 248 | 249 | 250 | } 251 | 252 | 253 | // MARK: - store 254 | extension Cache { 255 | internal func storeTasks(_ tasks: [DownloadTask]) { 256 | debouncer.execute(label: "storeTasks", wallDeadline: .now() + 0.2) { 257 | var path = (self.downloadPath as NSString).appendingPathComponent("\(self.identifier)_Tasks.plist") 258 | do { 259 | let data = try self.encoder.encode(tasks) 260 | let url = URL(fileURLWithPath: path) 261 | try data.write(to: url) 262 | } catch { 263 | self.manager?.log(.error("store tasks failed", 264 | error: TiercelError.cacheError(reason: .cannotEncodeTasks(path: path, 265 | error: error)))) 266 | } 267 | path = (self.downloadPath as NSString).appendingPathComponent("\(self.identifier)Tasks.plist") 268 | try? self.fileManager.removeItem(atPath: path) 269 | } 270 | } 271 | 272 | internal func storeFile(at srcURL: URL, to dstURL: URL) { 273 | ioQueue.sync { 274 | do { 275 | try fileManager.moveItem(at: srcURL, to: dstURL) 276 | } catch { 277 | self.manager?.log(.error("store file failed", 278 | error: TiercelError.cacheError(reason: .cannotMoveItem(atPath: srcURL.absoluteString, 279 | toPath: dstURL.absoluteString, 280 | error: error)))) 281 | } 282 | } 283 | } 284 | 285 | internal func storeTmpFile(_ tmpFileName: String?) { 286 | ioQueue.sync { 287 | guard let tmpFileName = tmpFileName, !tmpFileName.isEmpty else { return } 288 | let tmpPath = (NSTemporaryDirectory() as NSString).appendingPathComponent(tmpFileName) 289 | let destination = (downloadTmpPath as NSString).appendingPathComponent(tmpFileName) 290 | if fileManager.fileExists(atPath: destination) { 291 | do { 292 | try fileManager.removeItem(atPath: destination) 293 | } catch { 294 | self.manager?.log(.error("store tmpFile failed", 295 | error: TiercelError.cacheError(reason: .cannotRemoveItem(path: destination, 296 | error: error)))) 297 | } 298 | } 299 | if fileManager.fileExists(atPath: tmpPath) { 300 | do { 301 | try fileManager.copyItem(atPath: tmpPath, toPath: destination) 302 | } catch { 303 | self.manager?.log(.error("store tmpFile failed", 304 | error: TiercelError.cacheError(reason: .cannotCopyItem(atPath: tmpPath, 305 | toPath: destination, 306 | error: error)))) 307 | } 308 | } 309 | } 310 | } 311 | 312 | internal func updateFileName(_ filePath: String, _ newFileName: String) { 313 | ioQueue.sync { 314 | if fileManager.fileExists(atPath: filePath) { 315 | let newFilePath = self.filePath(fileName: newFileName)! 316 | do { 317 | try fileManager.moveItem(atPath: filePath, toPath: newFilePath) 318 | } catch { 319 | self.manager?.log(.error("update fileName failed", 320 | error: TiercelError.cacheError(reason: .cannotMoveItem(atPath: filePath, 321 | toPath: newFilePath, 322 | error: error)))) 323 | } 324 | } 325 | } 326 | } 327 | } 328 | 329 | 330 | // MARK: - remove 331 | extension Cache { 332 | internal func remove(_ task: DownloadTask, completely: Bool) { 333 | removeTmpFile(task.tmpFileName) 334 | 335 | if completely { 336 | removeFile(task.filePath) 337 | } 338 | } 339 | 340 | internal func removeFile(_ filePath: String) { 341 | ioQueue.async { 342 | if self.fileManager.fileExists(atPath: filePath) { 343 | do { 344 | try self.fileManager.removeItem(atPath: filePath) 345 | } catch { 346 | self.manager?.log(.error("remove file failed", 347 | error: TiercelError.cacheError(reason: .cannotRemoveItem(path: filePath, 348 | error: error)))) 349 | } 350 | } 351 | } 352 | } 353 | 354 | 355 | 356 | /// 删除保留在本地的缓存文件 357 | /// 358 | /// - Parameter task: 359 | internal func removeTmpFile(_ tmpFileName: String?) { 360 | ioQueue.async { 361 | guard let tmpFileName = tmpFileName, !tmpFileName.isEmpty else { return } 362 | let path1 = (self.downloadTmpPath as NSString).appendingPathComponent(tmpFileName) 363 | let path2 = (NSTemporaryDirectory() as NSString).appendingPathComponent(tmpFileName) 364 | [path1, path2].forEach { (path) in 365 | if self.fileManager.fileExists(atPath: path) { 366 | do { 367 | try self.fileManager.removeItem(atPath: path) 368 | } catch { 369 | self.manager?.log(.error("remove tmpFile failed", 370 | error: TiercelError.cacheError(reason: .cannotRemoveItem(path: path, 371 | error: error)))) 372 | } 373 | } 374 | } 375 | 376 | } 377 | } 378 | } 379 | 380 | extension URL: TiercelCompatible { } 381 | extension TiercelWrapper where Base == URL { 382 | public var fileName: String { 383 | var fileName = base.absoluteString.tr.md5 384 | if !base.pathExtension.isEmpty { 385 | fileName += ".\(base.pathExtension)" 386 | } 387 | return fileName 388 | } 389 | } 390 | -------------------------------------------------------------------------------- /Sources/General/Common.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Common.swift 3 | // Tiercel 4 | // 5 | // Created by Daniels on 2018/3/16. 6 | // Copyright © 2018 Daniels. All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | 29 | 30 | public enum LogOption { 31 | case `default` 32 | case none 33 | } 34 | 35 | public enum LogType { 36 | case sessionManager(_ message: String, manager: SessionManager) 37 | case downloadTask(_ message: String, task: DownloadTask) 38 | case error(_ message: String, error: Error) 39 | } 40 | 41 | public protocol Logable { 42 | var identifier: String { get } 43 | 44 | var option: LogOption { get set } 45 | 46 | func log(_ type: LogType) 47 | } 48 | 49 | public struct Logger: Logable { 50 | 51 | public let identifier: String 52 | 53 | public var option: LogOption 54 | 55 | public func log(_ type: LogType) { 56 | guard option == .default else { return } 57 | var strings = ["************************ TiercelLog ************************"] 58 | strings.append("identifier : \(identifier)") 59 | switch type { 60 | case let .sessionManager(message, manager): 61 | strings.append("Message : [SessionManager] \(message), tasks.count: \(manager.tasks.count)") 62 | case let .downloadTask(message, task): 63 | strings.append("Message : [DownloadTask] \(message)") 64 | strings.append("Task URL : \(task.url.absoluteString)") 65 | if let error = task.error, task.status == .failed { 66 | strings.append("Error : \(error)") 67 | } 68 | case let .error(message, error): 69 | strings.append("Message : [Error] \(message)") 70 | strings.append("Description : \(error)") 71 | } 72 | strings.append("") 73 | print(strings.joined(separator: "\n")) 74 | } 75 | } 76 | 77 | public enum Status: String { 78 | case waiting 79 | case running 80 | case suspended 81 | case canceled 82 | case failed 83 | case removed 84 | case succeeded 85 | 86 | case willSuspend 87 | case willCancel 88 | case willRemove 89 | } 90 | 91 | public struct TiercelWrapper { 92 | internal let base: Base 93 | internal init(_ base: Base) { 94 | self.base = base 95 | } 96 | } 97 | 98 | 99 | public protocol TiercelCompatible { 100 | 101 | } 102 | 103 | extension TiercelCompatible { 104 | public var tr: TiercelWrapper { 105 | get { TiercelWrapper(self) } 106 | } 107 | public static var tr: TiercelWrapper.Type { 108 | get { TiercelWrapper.self } 109 | } 110 | } 111 | 112 | -------------------------------------------------------------------------------- /Sources/General/DownloadTask.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DownloadTask.swift 3 | // Tiercel 4 | // 5 | // Created by Daniels on 2018/3/16. 6 | // Copyright © 2018 Daniels. All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | public class DownloadTask: Task { 30 | 31 | private enum CodingKeys: CodingKey { 32 | case resumeData 33 | case response 34 | } 35 | 36 | private var acceptableStatusCodes: Range { return 200..<300 } 37 | 38 | private var _sessionTask: URLSessionDownloadTask? { 39 | willSet { 40 | _sessionTask?.removeObserver(self, forKeyPath: "currentRequest") 41 | } 42 | didSet { 43 | _sessionTask?.addObserver(self, forKeyPath: "currentRequest", options: [.new], context: nil) 44 | } 45 | } 46 | 47 | internal var sessionTask: URLSessionDownloadTask? { 48 | get { protectedDownloadState.read { _ in _sessionTask }} 49 | set { protectedDownloadState.write { _ in _sessionTask = newValue }} 50 | } 51 | 52 | 53 | public private(set) var response: HTTPURLResponse? { 54 | get { protectedDownloadState.wrappedValue.response } 55 | set { protectedDownloadState.write { $0.response = newValue } } 56 | } 57 | 58 | 59 | public var filePath: String { 60 | return cache.filePath(fileName: fileName)! 61 | } 62 | 63 | public var pathExtension: String? { 64 | let pathExtension = (filePath as NSString).pathExtension 65 | return pathExtension.isEmpty ? nil : pathExtension 66 | } 67 | 68 | 69 | private struct DownloadState { 70 | var resumeData: Data? { 71 | didSet { 72 | guard let resumeData = resumeData else { return } 73 | tmpFileName = ResumeDataHelper.getTmpFileName(resumeData) 74 | } 75 | } 76 | var response: HTTPURLResponse? 77 | var tmpFileName: String? 78 | var shouldValidateFile: Bool = false 79 | } 80 | 81 | private let protectedDownloadState: Protected = Protected(DownloadState()) 82 | 83 | 84 | private var resumeData: Data? { 85 | get { protectedDownloadState.wrappedValue.resumeData } 86 | set { protectedDownloadState.write { $0.resumeData = newValue } } 87 | } 88 | 89 | internal var tmpFileName: String? { 90 | protectedDownloadState.wrappedValue.tmpFileName 91 | } 92 | 93 | private var shouldValidateFile: Bool { 94 | get { protectedDownloadState.wrappedValue.shouldValidateFile } 95 | set { protectedDownloadState.write { $0.shouldValidateFile = newValue } } 96 | } 97 | 98 | 99 | internal init(_ url: URL, 100 | headers: [String: String]? = nil, 101 | fileName: String? = nil, 102 | cache: Cache, 103 | operationQueue: DispatchQueue) { 104 | super.init(url, 105 | headers: headers, 106 | cache: cache, 107 | operationQueue: operationQueue) 108 | if let fileName = fileName, !fileName.isEmpty { 109 | self.fileName = fileName 110 | } 111 | NotificationCenter.default.addObserver(self, 112 | selector: #selector(fixDelegateMethodError), 113 | name: UIApplication.didBecomeActiveNotification, 114 | object: nil) 115 | } 116 | 117 | public override func encode(to encoder: Encoder) throws { 118 | var container = encoder.container(keyedBy: CodingKeys.self) 119 | let superEncoder = container.superEncoder() 120 | try super.encode(to: superEncoder) 121 | try container.encodeIfPresent(resumeData, forKey: .resumeData) 122 | if let response = response { 123 | let responseData: Data 124 | if #available(iOS 11.0, *) { 125 | responseData = try NSKeyedArchiver.archivedData(withRootObject: (response as HTTPURLResponse), requiringSecureCoding: true) 126 | } else { 127 | responseData = NSKeyedArchiver.archivedData(withRootObject: (response as HTTPURLResponse)) 128 | } 129 | try container.encode(responseData, forKey: .response) 130 | } 131 | } 132 | 133 | internal required init(from decoder: Decoder) throws { 134 | let container = try decoder.container(keyedBy: CodingKeys.self) 135 | let superDecoder = try container.superDecoder() 136 | try super.init(from: superDecoder) 137 | resumeData = try container.decodeIfPresent(Data.self, forKey: .resumeData) 138 | if let responseData = try container.decodeIfPresent(Data.self, forKey: .response) { 139 | if #available(iOS 11.0, *) { 140 | response = try? NSKeyedUnarchiver.unarchivedObject(ofClass: HTTPURLResponse.self, from: responseData) 141 | } else { 142 | response = NSKeyedUnarchiver.unarchiveObject(with: responseData) as? HTTPURLResponse 143 | } 144 | } 145 | } 146 | 147 | 148 | deinit { 149 | sessionTask?.removeObserver(self, forKeyPath: "currentRequest") 150 | NotificationCenter.default.removeObserver(self) 151 | } 152 | 153 | @objc private func fixDelegateMethodError() { 154 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { 155 | self.sessionTask?.suspend() 156 | self.sessionTask?.resume() 157 | } 158 | } 159 | 160 | 161 | internal override func execute(_ executer: Executer?) { 162 | executer?.execute(self) 163 | } 164 | 165 | 166 | } 167 | 168 | 169 | // MARK: - control 170 | extension DownloadTask { 171 | 172 | internal func download() { 173 | cache.createDirectory() 174 | guard let manager = manager else { return } 175 | switch status { 176 | case .waiting, .suspended, .failed: 177 | if cache.fileExists(fileName: fileName) { 178 | prepareForDownload(fileExists: true) 179 | } else { 180 | if manager.shouldRun { 181 | prepareForDownload(fileExists: false) 182 | } else { 183 | status = .waiting 184 | progressExecuter?.execute(self) 185 | executeControl() 186 | } 187 | } 188 | case .succeeded: 189 | executeControl() 190 | succeeded(fromRunning: false, immediately: false) 191 | case .running: 192 | status = .running 193 | executeControl() 194 | default: break 195 | } 196 | } 197 | 198 | private func prepareForDownload(fileExists: Bool) { 199 | status = .running 200 | protectedState.write { 201 | $0.speed = 0 202 | if $0.startDate == 0 { 203 | $0.startDate = Date().timeIntervalSince1970 204 | } 205 | } 206 | error = nil 207 | response = nil 208 | start(fileExists: fileExists) 209 | } 210 | 211 | private func start(fileExists: Bool) { 212 | if fileExists { 213 | manager?.log(.downloadTask("file already exists", task: self)) 214 | if let fileInfo = try? FileManager.default.attributesOfItem(atPath: cache.filePath(fileName: fileName)!), 215 | let length = fileInfo[.size] as? Int64 { 216 | progress.totalUnitCount = length 217 | } 218 | executeControl() 219 | operationQueue.async { 220 | self.didComplete(.local) 221 | } 222 | } else { 223 | if let resumeData = resumeData, 224 | cache.retrieveTmpFile(tmpFileName) { 225 | if #available(iOS 10.2, *) { 226 | sessionTask = session?.downloadTask(withResumeData: resumeData) 227 | } else if #available(iOS 10.0, *) { 228 | sessionTask = session?.correctedDownloadTask(withResumeData: resumeData) 229 | } else { 230 | sessionTask = session?.downloadTask(withResumeData: resumeData) 231 | } 232 | } else { 233 | var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 0) 234 | if let headers = headers { 235 | request.allHTTPHeaderFields = headers 236 | } 237 | sessionTask = session?.downloadTask(with: request) 238 | progress.completedUnitCount = 0 239 | progress.totalUnitCount = 0 240 | } 241 | progress.setUserInfoObject(progress.completedUnitCount, forKey: .fileCompletedCountKey) 242 | sessionTask?.resume() 243 | manager?.maintainTasks(with: .appendRunningTasks(self)) 244 | manager?.storeTasks() 245 | executeControl() 246 | } 247 | } 248 | 249 | 250 | internal func suspend(onMainQueue: Bool = true, handler: Handler? = nil) { 251 | guard status == .running || status == .waiting else { return } 252 | controlExecuter = Executer(onMainQueue: onMainQueue, handler: handler) 253 | if status == .running { 254 | status = .willSuspend 255 | sessionTask?.cancel(byProducingResumeData: { _ in }) 256 | } else { 257 | status = .willSuspend 258 | operationQueue.async { 259 | self.didComplete(.local) 260 | } 261 | } 262 | } 263 | 264 | internal func cancel(onMainQueue: Bool = true, handler: Handler? = nil) { 265 | guard status != .succeeded else { return } 266 | controlExecuter = Executer(onMainQueue: onMainQueue, handler: handler) 267 | if status == .running { 268 | status = .willCancel 269 | sessionTask?.cancel() 270 | } else { 271 | status = .willCancel 272 | operationQueue.async { 273 | self.didComplete(.local) 274 | } 275 | } 276 | } 277 | 278 | 279 | 280 | internal func remove(completely: Bool = false, onMainQueue: Bool = true, handler: Handler? = nil) { 281 | isRemoveCompletely = completely 282 | controlExecuter = Executer(onMainQueue: onMainQueue, handler: handler) 283 | if status == .running { 284 | status = .willRemove 285 | sessionTask?.cancel() 286 | } else { 287 | status = .willRemove 288 | operationQueue.async { 289 | self.didComplete(.local) 290 | } 291 | } 292 | } 293 | 294 | 295 | internal func update(_ newHeaders: [String: String]? = nil, newFileName: String? = nil) { 296 | headers = newHeaders 297 | if let newFileName = newFileName, !newFileName.isEmpty { 298 | cache.updateFileName(filePath, newFileName) 299 | fileName = newFileName 300 | } 301 | } 302 | 303 | private func validateFile() { 304 | guard let validateHandler = self.validateExecuter else { return } 305 | 306 | if !shouldValidateFile { 307 | validateHandler.execute(self) 308 | return 309 | } 310 | 311 | guard let verificationCode = verificationCode else { return } 312 | 313 | FileChecksumHelper.validateFile(filePath, code: verificationCode, type: verificationType) { [weak self] (result) in 314 | guard let self = self else { return } 315 | self.shouldValidateFile = false 316 | if case let .failure(error) = result { 317 | self.validation = .incorrect 318 | self.manager?.log(.error("file validation failed, url: \(self.url)", error: error)) 319 | } else { 320 | self.validation = .correct 321 | self.manager?.log(.downloadTask("file validation successful", task: self)) 322 | } 323 | self.manager?.storeTasks() 324 | validateHandler.execute(self) 325 | } 326 | } 327 | 328 | } 329 | 330 | 331 | 332 | // MARK: - status handle 333 | extension DownloadTask { 334 | 335 | private func didCancelOrRemove() { 336 | // 把预操作的状态改成完成操作的状态 337 | if status == .willCancel { 338 | status = .canceled 339 | } 340 | if status == .willRemove { 341 | status = .removed 342 | } 343 | cache.remove(self, completely: isRemoveCompletely) 344 | 345 | manager?.didCancelOrRemove(self) 346 | } 347 | 348 | 349 | internal func succeeded(fromRunning: Bool, immediately: Bool) { 350 | if endDate == 0 { 351 | protectedState.write { 352 | $0.endDate = Date().timeIntervalSince1970 353 | $0.timeRemaining = 0 354 | } 355 | } 356 | status = .succeeded 357 | progress.completedUnitCount = progress.totalUnitCount 358 | progressExecuter?.execute(self) 359 | if immediately { 360 | executeCompletion(true) 361 | } 362 | validateFile() 363 | manager?.maintainTasks(with: .succeeded(self)) 364 | manager?.determineStatus(fromRunningTask: fromRunning) 365 | } 366 | 367 | 368 | private func determineStatus(with interruptType: InterruptType) { 369 | var fromRunning = true 370 | switch interruptType { 371 | case let .error(error): 372 | self.error = error 373 | var tempStatus = status 374 | if let resumeData = (error as NSError).userInfo[NSURLSessionDownloadTaskResumeData] as? Data { 375 | self.resumeData = ResumeDataHelper.handleResumeData(resumeData) 376 | cache.storeTmpFile(tmpFileName) 377 | } 378 | if let _ = (error as NSError).userInfo[NSURLErrorBackgroundTaskCancelledReasonKey] as? Int { 379 | tempStatus = .suspended 380 | } 381 | if let urlError = error as? URLError, urlError.code != URLError.cancelled { 382 | tempStatus = .failed 383 | } 384 | status = tempStatus 385 | case let .statusCode(statusCode): 386 | self.error = TiercelError.unacceptableStatusCode(code: statusCode) 387 | status = .failed 388 | case let .manual(fromRunningTask): 389 | fromRunning = fromRunningTask 390 | } 391 | 392 | switch status { 393 | case .willSuspend: 394 | status = .suspended 395 | progressExecuter?.execute(self) 396 | executeControl() 397 | executeCompletion(false) 398 | case .willCancel, .willRemove: 399 | didCancelOrRemove() 400 | executeControl() 401 | executeCompletion(false) 402 | case .suspended, .failed: 403 | progressExecuter?.execute(self) 404 | executeCompletion(false) 405 | default: 406 | status = .failed 407 | progressExecuter?.execute(self) 408 | executeCompletion(false) 409 | } 410 | manager?.determineStatus(fromRunningTask: fromRunning) 411 | } 412 | } 413 | 414 | // MARK: - closure 415 | extension DownloadTask { 416 | @discardableResult 417 | public func validateFile(code: String, 418 | type: FileChecksumHelper.VerificationType, 419 | onMainQueue: Bool = true, 420 | handler: @escaping Handler) -> Self { 421 | operationQueue.async { 422 | let (verificationCode, verificationType) = self.protectedState.read { 423 | ($0.verificationCode, $0.verificationType) 424 | } 425 | if verificationCode == code && 426 | verificationType == type && 427 | self.validation != .unkown { 428 | self.shouldValidateFile = false 429 | } else { 430 | self.shouldValidateFile = true 431 | self.protectedState.write { 432 | $0.verificationCode = code 433 | $0.verificationType = type 434 | } 435 | self.manager?.storeTasks() 436 | } 437 | self.validateExecuter = Executer(onMainQueue: onMainQueue, handler: handler) 438 | if self.status == .succeeded { 439 | self.validateFile() 440 | } 441 | } 442 | return self 443 | } 444 | 445 | private func executeCompletion(_ isSucceeded: Bool) { 446 | if let completionExecuter = completionExecuter { 447 | completionExecuter.execute(self) 448 | } else if isSucceeded { 449 | successExecuter?.execute(self) 450 | } else { 451 | failureExecuter?.execute(self) 452 | } 453 | NotificationCenter.default.postNotification(name: DownloadTask.didCompleteNotification, downloadTask: self) 454 | } 455 | 456 | private func executeControl() { 457 | controlExecuter?.execute(self) 458 | controlExecuter = nil 459 | } 460 | } 461 | 462 | 463 | 464 | // MARK: - KVO 465 | extension DownloadTask { 466 | override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { 467 | if let change = change, let newRequest = change[NSKeyValueChangeKey.newKey] as? URLRequest, let url = newRequest.url { 468 | currentURL = url 469 | manager?.updateUrlMapper(with: self) 470 | } 471 | } 472 | } 473 | 474 | // MARK: - info 475 | extension DownloadTask { 476 | 477 | internal func updateSpeedAndTimeRemaining() { 478 | 479 | let dataCount = progress.completedUnitCount 480 | let lastData: Int64 = progress.userInfo[.fileCompletedCountKey] as? Int64 ?? 0 481 | 482 | if dataCount > lastData { 483 | let speed = dataCount - lastData 484 | updateTimeRemaining(speed) 485 | } 486 | progress.setUserInfoObject(dataCount, forKey: .fileCompletedCountKey) 487 | 488 | } 489 | 490 | private func updateTimeRemaining(_ speed: Int64) { 491 | var timeRemaining: Double 492 | if speed != 0 { 493 | timeRemaining = (Double(progress.totalUnitCount) - Double(progress.completedUnitCount)) / Double(speed) 494 | if timeRemaining >= 0.8 && timeRemaining < 1 { 495 | timeRemaining += 1 496 | } 497 | } else { 498 | timeRemaining = 0 499 | } 500 | protectedState.write { 501 | $0.speed = speed 502 | $0.timeRemaining = Int64(timeRemaining) 503 | } 504 | } 505 | } 506 | 507 | // MARK: - callback 508 | extension DownloadTask { 509 | internal func didWriteData(downloadTask: URLSessionDownloadTask, bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { 510 | progress.completedUnitCount = totalBytesWritten 511 | progress.totalUnitCount = totalBytesExpectedToWrite 512 | response = downloadTask.response as? HTTPURLResponse 513 | progressExecuter?.execute(self) 514 | manager?.updateProgress() 515 | NotificationCenter.default.postNotification(name: DownloadTask.runningNotification, downloadTask: self) 516 | } 517 | 518 | 519 | internal func didFinishDownloading(task: URLSessionDownloadTask, to location: URL) { 520 | guard let statusCode = (task.response as? HTTPURLResponse)?.statusCode, 521 | acceptableStatusCodes.contains(statusCode) 522 | else { return } 523 | cache.storeFile(at: location, to: URL(fileURLWithPath: filePath)) 524 | cache.removeTmpFile(tmpFileName) 525 | 526 | } 527 | 528 | internal func didComplete(_ type: CompletionType) { 529 | switch type { 530 | case .local: 531 | 532 | switch status { 533 | case .willSuspend,.willCancel, .willRemove: 534 | determineStatus(with: .manual(false)) 535 | case .running: 536 | succeeded(fromRunning: false, immediately: true) 537 | default: 538 | return 539 | } 540 | 541 | case let .network(task, error): 542 | manager?.maintainTasks(with: .removeRunningTasks(self)) 543 | sessionTask = nil 544 | 545 | switch status { 546 | case .willCancel, .willRemove: 547 | determineStatus(with: .manual(true)) 548 | return 549 | case .willSuspend, .running: 550 | progress.totalUnitCount = task.countOfBytesExpectedToReceive 551 | progress.completedUnitCount = task.countOfBytesReceived 552 | progress.setUserInfoObject(task.countOfBytesReceived, forKey: .fileCompletedCountKey) 553 | 554 | let statusCode = (task.response as? HTTPURLResponse)?.statusCode ?? -1 555 | let isAcceptable = acceptableStatusCodes.contains(statusCode) 556 | 557 | if error != nil { 558 | response = task.response as? HTTPURLResponse 559 | determineStatus(with: .error(error!)) 560 | } else if !isAcceptable { 561 | response = task.response as? HTTPURLResponse 562 | determineStatus(with: .statusCode(statusCode)) 563 | } else { 564 | resumeData = nil 565 | succeeded(fromRunning: true, immediately: true) 566 | } 567 | default: 568 | return 569 | } 570 | } 571 | } 572 | 573 | } 574 | 575 | 576 | 577 | extension Array where Element == DownloadTask { 578 | @discardableResult 579 | public func progress(onMainQueue: Bool = true, handler: @escaping Handler) -> [Element] { 580 | self.forEach { $0.progress(onMainQueue: onMainQueue, handler: handler) } 581 | return self 582 | } 583 | 584 | @discardableResult 585 | public func success(onMainQueue: Bool = true, handler: @escaping Handler) -> [Element] { 586 | self.forEach { $0.success(onMainQueue: onMainQueue, handler: handler) } 587 | return self 588 | } 589 | 590 | @discardableResult 591 | public func failure(onMainQueue: Bool = true, handler: @escaping Handler) -> [Element] { 592 | self.forEach { $0.failure(onMainQueue: onMainQueue, handler: handler) } 593 | return self 594 | } 595 | 596 | public func validateFile(codes: [String], 597 | type: FileChecksumHelper.VerificationType, 598 | onMainQueue: Bool = true, 599 | handler: @escaping Handler) -> [Element] { 600 | for (index, task) in self.enumerated() { 601 | guard let code = codes.safeObject(at: index) else { continue } 602 | task.validateFile(code: code, type: type, onMainQueue: onMainQueue, handler: handler) 603 | } 604 | return self 605 | } 606 | } 607 | -------------------------------------------------------------------------------- /Sources/General/Executer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Executer.swift 3 | // Tiercel 4 | // 5 | // Created by Daniels on 2019/4/30. 6 | // Copyright © 2019 Daniels. All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | 29 | public typealias Handler = (T) -> () 30 | 31 | public class Executer { 32 | private let onMainQueue: Bool 33 | private let handler: Handler? 34 | 35 | public init(onMainQueue: Bool = true, handler: Handler?) { 36 | self.onMainQueue = onMainQueue 37 | self.handler = handler 38 | } 39 | 40 | 41 | public func execute(_ object: T) { 42 | if let handler = handler { 43 | if onMainQueue { 44 | DispatchQueue.tr.executeOnMain { 45 | handler(object) 46 | } 47 | } else { 48 | handler(object) 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/General/Notifications.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Notifications.swift 3 | // Tiercel 4 | // 5 | // Created by Daniels on 2020/1/20. 6 | // Copyright © 2020 Daniels. All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | 29 | public extension DownloadTask { 30 | 31 | static let runningNotification = Notification.Name(rawValue: "com.Tiercel.notification.name.downloadTask.running") 32 | static let didCompleteNotification = Notification.Name(rawValue: "com.Tiercel.notification.name.downloadTask.didComplete") 33 | 34 | } 35 | 36 | public extension SessionManager { 37 | 38 | static let runningNotification = Notification.Name(rawValue: "com.Tiercel.notification.name.sessionManager.running") 39 | static let didCompleteNotification = Notification.Name(rawValue: "com.Tiercel.notification.name.sessionManager.didComplete") 40 | 41 | } 42 | 43 | extension Notification: TiercelCompatible { } 44 | extension TiercelWrapper where Base == Notification { 45 | public var downloadTask: DownloadTask? { 46 | return base.userInfo?[String.downloadTaskKey] as? DownloadTask 47 | } 48 | 49 | public var sessionManager: SessionManager? { 50 | return base.userInfo?[String.sessionManagerKey] as? SessionManager 51 | } 52 | } 53 | 54 | extension Notification { 55 | init(name: Notification.Name, downloadTask: DownloadTask) { 56 | self.init(name: name, object: nil, userInfo: [String.downloadTaskKey: downloadTask]) 57 | } 58 | 59 | init(name: Notification.Name, sessionManager: SessionManager) { 60 | self.init(name: name, object: nil, userInfo: [String.sessionManagerKey: sessionManager]) 61 | } 62 | } 63 | 64 | extension NotificationCenter { 65 | 66 | func postNotification(name: Notification.Name, downloadTask: DownloadTask) { 67 | let notification = Notification(name: name, downloadTask: downloadTask) 68 | post(notification) 69 | } 70 | 71 | func postNotification(name: Notification.Name, sessionManager: SessionManager) { 72 | let notification = Notification(name: name, sessionManager: sessionManager) 73 | post(notification) 74 | } 75 | } 76 | 77 | extension String { 78 | 79 | fileprivate static let downloadTaskKey = "com.Tiercel.notification.key.downloadTask" 80 | fileprivate static let sessionManagerKey = "com.Tiercel.notification.key.sessionManagerKey" 81 | 82 | } 83 | -------------------------------------------------------------------------------- /Sources/General/Protected.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Protected.swift 3 | // Tiercel 4 | // 5 | // Created by Daniels on 2020/1/9. 6 | // Copyright © 2020 Daniels. All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | // 26 | 27 | 28 | import Foundation 29 | 30 | 31 | final public class UnfairLock { 32 | private let unfairLock: os_unfair_lock_t 33 | 34 | public init() { 35 | 36 | unfairLock = .allocate(capacity: 1) 37 | unfairLock.initialize(to: os_unfair_lock()) 38 | } 39 | 40 | deinit { 41 | unfairLock.deinitialize(count: 1) 42 | unfairLock.deallocate() 43 | } 44 | 45 | private func lock() { 46 | os_unfair_lock_lock(unfairLock) 47 | } 48 | 49 | private func unlock() { 50 | os_unfair_lock_unlock(unfairLock) 51 | } 52 | 53 | 54 | public func around(_ closure: () throws -> T) rethrows -> T { 55 | lock(); defer { unlock() } 56 | return try closure() 57 | } 58 | 59 | public func around(_ closure: () throws -> Void) rethrows -> Void { 60 | lock(); defer { unlock() } 61 | return try closure() 62 | } 63 | } 64 | 65 | @propertyWrapper 66 | final public class Protected { 67 | 68 | private let lock = UnfairLock() 69 | 70 | private var value: T 71 | 72 | public var wrappedValue: T { 73 | get { lock.around { value } } 74 | set { lock.around { value = newValue } } 75 | } 76 | 77 | public var projectedValue: Protected { self } 78 | 79 | 80 | public init(_ value: T) { 81 | self.value = value 82 | } 83 | 84 | public init(wrappedValue: T) { 85 | value = wrappedValue 86 | } 87 | 88 | public func read(_ closure: (T) throws -> U) rethrows -> U { 89 | return try lock.around { try closure(self.value) } 90 | } 91 | 92 | 93 | @discardableResult 94 | public func write(_ closure: (inout T) throws -> U) rethrows -> U { 95 | return try lock.around { try closure(&self.value) } 96 | } 97 | } 98 | 99 | final public class Debouncer { 100 | 101 | private let lock = UnfairLock() 102 | 103 | private let queue: DispatchQueue 104 | 105 | @Protected 106 | private var workItems = [String: DispatchWorkItem]() 107 | 108 | public init(queue: DispatchQueue) { 109 | self.queue = queue 110 | } 111 | 112 | 113 | public func execute(label: String, deadline: DispatchTime, execute work: @escaping @convention(block) () -> Void) { 114 | execute(label: label, time: deadline, execute: work) 115 | } 116 | 117 | 118 | public func execute(label: String, wallDeadline: DispatchWallTime, execute work: @escaping @convention(block) () -> Void) { 119 | execute(label: label, time: wallDeadline, execute: work) 120 | } 121 | 122 | 123 | private func execute(label: String, time: T, execute work: @escaping @convention(block) () -> Void) { 124 | lock.around { 125 | workItems[label]?.cancel() 126 | let workItem = DispatchWorkItem { [weak self] in 127 | work() 128 | self?.workItems.removeValue(forKey: label) 129 | } 130 | workItems[label] = workItem 131 | if let time = time as? DispatchTime { 132 | queue.asyncAfter(deadline: time, execute: workItem) 133 | } else if let time = time as? DispatchWallTime { 134 | queue.asyncAfter(wallDeadline: time, execute: workItem) 135 | } 136 | } 137 | } 138 | } 139 | 140 | final public class Throttler { 141 | 142 | private let lock = UnfairLock() 143 | 144 | private let queue: DispatchQueue 145 | 146 | private var workItems = [String: DispatchWorkItem]() 147 | 148 | private let latest: Bool 149 | 150 | public init(queue: DispatchQueue, latest: Bool) { 151 | self.queue = queue 152 | self.latest = latest 153 | } 154 | 155 | 156 | public func execute(label: String, deadline: DispatchTime, execute work: @escaping @convention(block) () -> Void) { 157 | execute(label: label, time: deadline, execute: work) 158 | } 159 | 160 | 161 | public func execute(label: String, wallDeadline: DispatchWallTime, execute work: @escaping @convention(block) () -> Void) { 162 | execute(label: label, time: wallDeadline, execute: work) 163 | } 164 | 165 | private func execute(label: String, time: T, execute work: @escaping @convention(block) () -> Void) { 166 | lock.around { 167 | let workItem = workItems[label] 168 | 169 | guard workItem == nil || latest else { return } 170 | workItem?.cancel() 171 | workItems[label] = DispatchWorkItem { [weak self] in 172 | self?.workItems.removeValue(forKey: label) 173 | work() 174 | } 175 | 176 | guard workItem == nil else { return } 177 | if let time = time as? DispatchTime { 178 | queue.asyncAfter(deadline: time) { [weak self] in 179 | self?.workItems[label]?.perform() 180 | } 181 | } else if let time = time as? DispatchWallTime { 182 | queue.asyncAfter(wallDeadline: time) { [weak self] in 183 | self?.workItems[label]?.perform() 184 | } 185 | } 186 | } 187 | } 188 | } 189 | 190 | 191 | -------------------------------------------------------------------------------- /Sources/General/SessionConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SessionConfiguration.swift 3 | // Tiercel 4 | // 5 | // Created by Daniels on 2019/1/3. 6 | // Copyright © 2019 Daniels. All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | 29 | public struct SessionConfiguration { 30 | // 请求超时时间 31 | public var timeoutIntervalForRequest: TimeInterval = 60.0 32 | 33 | private static var MaxConcurrentTasksLimit: Int = { 34 | if #available(iOS 11.0, *) { 35 | return 6 36 | } else { 37 | return 3 38 | } 39 | }() 40 | 41 | // 最大并发数 42 | private var _maxConcurrentTasksLimit: Int = MaxConcurrentTasksLimit 43 | public var maxConcurrentTasksLimit: Int { 44 | get { _maxConcurrentTasksLimit } 45 | set { 46 | let limit = min(newValue, Self.MaxConcurrentTasksLimit) 47 | _maxConcurrentTasksLimit = max(limit, 1) 48 | } 49 | } 50 | 51 | public var allowsExpensiveNetworkAccess: Bool = true 52 | 53 | 54 | public var allowsConstrainedNetworkAccess: Bool = true 55 | 56 | // 是否允许蜂窝网络下载 57 | public var allowsCellularAccess: Bool = false 58 | 59 | public init() { 60 | 61 | } 62 | } 63 | 64 | 65 | -------------------------------------------------------------------------------- /Sources/General/SessionDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SessionDelegate.swift 3 | // Tiercel 4 | // 5 | // Created by Daniels on 2018/3/16. 6 | // Copyright © 2018 Daniels. All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | 29 | internal class SessionDelegate: NSObject { 30 | internal weak var manager: SessionManager? 31 | 32 | } 33 | 34 | 35 | extension SessionDelegate: URLSessionDownloadDelegate { 36 | public func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) { 37 | manager?.didBecomeInvalidation(withError: error) 38 | } 39 | 40 | 41 | public func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) { 42 | manager?.didFinishEvents(forBackgroundURLSession: session) 43 | } 44 | 45 | 46 | public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { 47 | guard let manager = manager else { return } 48 | guard let currentURL = downloadTask.currentRequest?.url else { return } 49 | guard let task = manager.mapTask(currentURL) else { 50 | manager.log(.error("urlSession(_:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:)", 51 | error: TiercelError.fetchDownloadTaskFailed(url: currentURL)) 52 | ) 53 | return 54 | } 55 | task.didWriteData(downloadTask: downloadTask, bytesWritten: bytesWritten, totalBytesWritten: totalBytesWritten, totalBytesExpectedToWrite: totalBytesExpectedToWrite) 56 | } 57 | 58 | 59 | public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { 60 | guard let manager = manager else { return } 61 | guard let currentURL = downloadTask.currentRequest?.url else { return } 62 | guard let task = manager.mapTask(currentURL) else { 63 | manager.log(.error("urlSession(_:downloadTask:didFinishDownloadingTo:)", error: TiercelError.fetchDownloadTaskFailed(url: currentURL))) 64 | return 65 | } 66 | task.didFinishDownloading(task: downloadTask, to: location) 67 | } 68 | 69 | public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { 70 | guard let manager = manager else { return } 71 | if let currentURL = task.currentRequest?.url { 72 | guard let downloadTask = manager.mapTask(currentURL) else { 73 | manager.log(.error("urlSession(_:task:didCompleteWithError:)", error: TiercelError.fetchDownloadTaskFailed(url: currentURL))) 74 | return 75 | } 76 | downloadTask.didComplete(.network(task: task, error: error)) 77 | } else { 78 | if let error = error { 79 | if let urlError = error as? URLError, 80 | let errorURL = urlError.userInfo[NSURLErrorFailingURLErrorKey] as? URL { 81 | guard let downloadTask = manager.mapTask(errorURL) else { 82 | manager.log(.error("urlSession(_:task:didCompleteWithError:)", error: TiercelError.fetchDownloadTaskFailed(url: errorURL))) 83 | manager.log(.error("urlSession(_:task:didCompleteWithError:)", error: error)) 84 | return 85 | } 86 | downloadTask.didComplete(.network(task: task, error: error)) 87 | } else { 88 | manager.log(.error("urlSession(_:task:didCompleteWithError:)", error: error)) 89 | return 90 | } 91 | } else { 92 | manager.log(.error("urlSession(_:task:didCompleteWithError:)", error: TiercelError.unknown)) 93 | } 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Sources/General/Task.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Task.swift 3 | // Tiercel 4 | // 5 | // Created by Daniels on 2018/3/16. 6 | // Copyright © 2018 Daniels. All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | 29 | extension Task { 30 | public enum Validation: Int { 31 | case unkown 32 | case correct 33 | case incorrect 34 | } 35 | } 36 | 37 | public class Task: NSObject, Codable { 38 | 39 | private enum CodingKeys: CodingKey { 40 | case url 41 | case currentURL 42 | case fileName 43 | case headers 44 | case startDate 45 | case endDate 46 | case totalBytes 47 | case completedBytes 48 | case verificationCode 49 | case status 50 | case verificationType 51 | case validation 52 | case error 53 | } 54 | 55 | enum CompletionType { 56 | case local 57 | case network(task: URLSessionTask, error: Error?) 58 | } 59 | 60 | enum InterruptType { 61 | case manual(_ fromRunningTask: Bool) 62 | case error(_ error: Error) 63 | case statusCode(_ statusCode: Int) 64 | } 65 | 66 | public internal(set) weak var manager: SessionManager? 67 | 68 | internal var cache: Cache 69 | 70 | internal var operationQueue: DispatchQueue 71 | 72 | public let url: URL 73 | 74 | public let progress: Progress = Progress() 75 | 76 | internal struct State { 77 | var session: URLSession? 78 | var headers: [String: String]? 79 | var verificationCode: String? 80 | var verificationType: FileChecksumHelper.VerificationType = .md5 81 | var isRemoveCompletely: Bool = false 82 | var status: Status = .waiting 83 | var validation: Validation = .unkown 84 | var currentURL: URL 85 | var startDate: Double = 0 86 | var endDate: Double = 0 87 | var speed: Int64 = 0 88 | var fileName: String 89 | var timeRemaining: Int64 = 0 90 | var error: Error? 91 | 92 | var progressExecuter: Executer? 93 | var successExecuter: Executer? 94 | var failureExecuter: Executer? 95 | var controlExecuter: Executer? 96 | var completionExecuter: Executer? 97 | var validateExecuter: Executer? 98 | } 99 | 100 | 101 | internal let protectedState: Protected 102 | 103 | internal var session: URLSession? { 104 | get { protectedState.wrappedValue.session } 105 | set { protectedState.write { $0.session = newValue } } 106 | } 107 | 108 | internal var headers: [String: String]? { 109 | get { protectedState.wrappedValue.headers } 110 | set { protectedState.write { $0.headers = newValue } } 111 | } 112 | 113 | internal var verificationCode: String? { 114 | get { protectedState.wrappedValue.verificationCode } 115 | set { protectedState.write { $0.verificationCode = newValue } } 116 | } 117 | 118 | internal var verificationType: FileChecksumHelper.VerificationType { 119 | get { protectedState.wrappedValue.verificationType } 120 | set { protectedState.write { $0.verificationType = newValue } } 121 | } 122 | 123 | internal var isRemoveCompletely: Bool { 124 | get { protectedState.wrappedValue.isRemoveCompletely } 125 | set { protectedState.write { $0.isRemoveCompletely = newValue } } 126 | } 127 | 128 | public internal(set) var status: Status { 129 | get { protectedState.wrappedValue.status } 130 | set { 131 | protectedState.write { $0.status = newValue } 132 | if newValue == .willSuspend || newValue == .willCancel || newValue == .willRemove { 133 | return 134 | } 135 | if self is DownloadTask { 136 | manager?.log(.downloadTask(newValue.rawValue, task: self as! DownloadTask)) 137 | } 138 | } 139 | } 140 | 141 | public internal(set) var validation: Validation { 142 | get { protectedState.wrappedValue.validation } 143 | set { protectedState.write { $0.validation = newValue } } 144 | } 145 | 146 | internal var currentURL: URL { 147 | get { protectedState.wrappedValue.currentURL } 148 | set { protectedState.write { $0.currentURL = newValue } } 149 | } 150 | 151 | 152 | public internal(set) var startDate: Double { 153 | get { protectedState.wrappedValue.startDate } 154 | set { protectedState.write { $0.startDate = newValue } } 155 | } 156 | 157 | public var startDateString: String { 158 | startDate.tr.convertTimeToDateString() 159 | } 160 | 161 | public internal(set) var endDate: Double { 162 | get { protectedState.wrappedValue.endDate } 163 | set { protectedState.write { $0.endDate = newValue } } 164 | } 165 | 166 | public var endDateString: String { 167 | endDate.tr.convertTimeToDateString() 168 | } 169 | 170 | 171 | public internal(set) var speed: Int64 { 172 | get { protectedState.wrappedValue.speed } 173 | set { protectedState.write { $0.speed = newValue } } 174 | } 175 | 176 | public var speedString: String { 177 | speed.tr.convertSpeedToString() 178 | } 179 | 180 | /// 默认为url的md5加上文件扩展名 181 | public internal(set) var fileName: String { 182 | get { protectedState.wrappedValue.fileName } 183 | set { protectedState.write { $0.fileName = newValue } } 184 | } 185 | 186 | public internal(set) var timeRemaining: Int64 { 187 | get { protectedState.wrappedValue.timeRemaining } 188 | set { protectedState.write { $0.timeRemaining = newValue } } 189 | } 190 | 191 | public var timeRemainingString: String { 192 | timeRemaining.tr.convertTimeToString() 193 | } 194 | 195 | public internal(set) var error: Error? { 196 | get { protectedState.wrappedValue.error } 197 | set { protectedState.write { $0.error = newValue } } 198 | } 199 | 200 | 201 | internal var progressExecuter: Executer? { 202 | get { protectedState.wrappedValue.progressExecuter } 203 | set { protectedState.write { $0.progressExecuter = newValue } } 204 | } 205 | 206 | internal var successExecuter: Executer? { 207 | get { protectedState.wrappedValue.successExecuter } 208 | set { protectedState.write { $0.successExecuter = newValue } } 209 | } 210 | 211 | internal var failureExecuter: Executer? { 212 | get { protectedState.wrappedValue.failureExecuter } 213 | set { protectedState.write { $0.failureExecuter = newValue } } 214 | } 215 | 216 | internal var completionExecuter: Executer? { 217 | get { protectedState.wrappedValue.completionExecuter } 218 | set { protectedState.write { $0.completionExecuter = newValue } } 219 | } 220 | 221 | internal var controlExecuter: Executer? { 222 | get { protectedState.wrappedValue.controlExecuter } 223 | set { protectedState.write { $0.controlExecuter = newValue } } 224 | } 225 | 226 | internal var validateExecuter: Executer? { 227 | get { protectedState.wrappedValue.validateExecuter } 228 | set { protectedState.write { $0.validateExecuter = newValue } } 229 | } 230 | 231 | 232 | 233 | internal init(_ url: URL, 234 | headers: [String: String]? = nil, 235 | cache: Cache, 236 | operationQueue:DispatchQueue) { 237 | self.cache = cache 238 | self.url = url 239 | self.operationQueue = operationQueue 240 | protectedState = Protected(State(currentURL: url, fileName: url.tr.fileName)) 241 | super.init() 242 | self.headers = headers 243 | } 244 | 245 | public func encode(to encoder: Encoder) throws { 246 | var container = encoder.container(keyedBy: CodingKeys.self) 247 | try container.encode(url, forKey: .url) 248 | try container.encode(currentURL, forKey: .currentURL) 249 | try container.encode(fileName, forKey: .fileName) 250 | try container.encodeIfPresent(headers, forKey: .headers) 251 | try container.encode(startDate, forKey: .startDate) 252 | try container.encode(endDate, forKey: .endDate) 253 | try container.encode(progress.totalUnitCount, forKey: .totalBytes) 254 | try container.encode(progress.completedUnitCount, forKey: .completedBytes) 255 | try container.encode(status.rawValue, forKey: .status) 256 | try container.encodeIfPresent(verificationCode, forKey: .verificationCode) 257 | try container.encode(verificationType.rawValue, forKey: .verificationType) 258 | try container.encode(validation.rawValue, forKey: .validation) 259 | if let error = error { 260 | let errorData: Data 261 | if #available(iOS 11.0, *) { 262 | errorData = try NSKeyedArchiver.archivedData(withRootObject: (error as NSError), requiringSecureCoding: true) 263 | } else { 264 | errorData = NSKeyedArchiver.archivedData(withRootObject: (error as NSError)) 265 | } 266 | try container.encode(errorData, forKey: .error) 267 | } 268 | } 269 | 270 | public required init(from decoder: Decoder) throws { 271 | let container = try decoder.container(keyedBy: CodingKeys.self) 272 | url = try container.decode(URL.self, forKey: .url) 273 | let currentURL = try container.decode(URL.self, forKey: .currentURL) 274 | let fileName = try container.decode(String.self, forKey: .fileName) 275 | protectedState = Protected(State(currentURL: currentURL, fileName: fileName)) 276 | cache = decoder.userInfo[.cache] as? Cache ?? Cache("default") 277 | operationQueue = decoder.userInfo[.operationQueue] as? DispatchQueue ?? DispatchQueue(label: "com.Tiercel.SessionManager.operationQueue") 278 | super.init() 279 | 280 | progress.totalUnitCount = try container.decode(Int64.self, forKey: .totalBytes) 281 | progress.completedUnitCount = try container.decode(Int64.self, forKey: .completedBytes) 282 | 283 | let statusString = try container.decode(String.self, forKey: .status) 284 | let verificationTypeInt = try container.decode(Int.self, forKey: .verificationType) 285 | let validationType = try container.decode(Int.self, forKey: .validation) 286 | 287 | try protectedState.write { 288 | $0.headers = try container.decodeIfPresent([String: String].self, forKey: .headers) 289 | $0.startDate = try container.decode(Double.self, forKey: .startDate) 290 | $0.endDate = try container.decode(Double.self, forKey: .endDate) 291 | $0.verificationCode = try container.decodeIfPresent(String.self, forKey: .verificationCode) 292 | $0.status = Status(rawValue: statusString)! 293 | $0.verificationType = FileChecksumHelper.VerificationType(rawValue: verificationTypeInt)! 294 | $0.validation = Validation(rawValue: validationType)! 295 | if let errorData = try container.decodeIfPresent(Data.self, forKey: .error) { 296 | if #available(iOS 11.0, *) { 297 | $0.error = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSError.self, from: errorData) 298 | } else { 299 | $0.error = NSKeyedUnarchiver.unarchiveObject(with: errorData) as? NSError 300 | } 301 | } 302 | } 303 | } 304 | 305 | internal func execute(_ Executer: Executer?) { 306 | 307 | } 308 | 309 | } 310 | 311 | 312 | extension Task { 313 | @discardableResult 314 | public func progress(onMainQueue: Bool = true, handler: @escaping Handler) -> Self { 315 | progressExecuter = Executer(onMainQueue: onMainQueue, handler: handler) 316 | return self 317 | } 318 | 319 | @discardableResult 320 | public func success(onMainQueue: Bool = true, handler: @escaping Handler) -> Self { 321 | successExecuter = Executer(onMainQueue: onMainQueue, handler: handler) 322 | if status == .succeeded && completionExecuter == nil{ 323 | operationQueue.async { 324 | self.execute(self.successExecuter) 325 | } 326 | } 327 | return self 328 | 329 | } 330 | 331 | @discardableResult 332 | public func failure(onMainQueue: Bool = true, handler: @escaping Handler) -> Self { 333 | failureExecuter = Executer(onMainQueue: onMainQueue, handler: handler) 334 | if completionExecuter == nil && 335 | (status == .suspended || 336 | status == .canceled || 337 | status == .removed || 338 | status == .failed) { 339 | operationQueue.async { 340 | self.execute(self.failureExecuter) 341 | } 342 | } 343 | return self 344 | } 345 | 346 | @discardableResult 347 | public func completion(onMainQueue: Bool = true, handler: @escaping Handler) -> Self { 348 | completionExecuter = Executer(onMainQueue: onMainQueue, handler: handler) 349 | if status == .suspended || 350 | status == .canceled || 351 | status == .removed || 352 | status == .succeeded || 353 | status == .failed { 354 | operationQueue.async { 355 | self.execute(self.completionExecuter) 356 | } 357 | } 358 | return self 359 | } 360 | 361 | } 362 | 363 | 364 | -------------------------------------------------------------------------------- /Sources/General/TiercelError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TiercelError.swift 3 | // Tiercel 4 | // 5 | // Created by Daniels on 2019/5/14. 6 | // Copyright © 2019 Daniels. All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | 29 | public enum TiercelError: Error { 30 | 31 | public enum CacheErrorReason { 32 | case cannotCreateDirectory(path: String, error: Error) 33 | case cannotRemoveItem(path: String, error: Error) 34 | case cannotCopyItem(atPath: String, toPath: String, error: Error) 35 | case cannotMoveItem(atPath: String, toPath: String, error: Error) 36 | case cannotRetrieveAllTasks(path: String, error: Error) 37 | case cannotEncodeTasks(path: String, error: Error) 38 | case fileDoesnotExist(path: String) 39 | case readDataFailed(path: String) 40 | } 41 | 42 | case unknown 43 | case invalidURL(url: URLConvertible) 44 | case duplicateURL(url: URLConvertible) 45 | case indexOutOfRange 46 | case fetchDownloadTaskFailed(url: URLConvertible) 47 | case headersMatchFailed 48 | case fileNamesMatchFailed 49 | case unacceptableStatusCode(code: Int) 50 | case cacheError(reason: CacheErrorReason) 51 | } 52 | 53 | extension TiercelError: LocalizedError { 54 | public var errorDescription: String? { 55 | switch self { 56 | case .unknown: 57 | return "unkown error" 58 | case let .invalidURL(url): 59 | return "URL is not valid: \(url)" 60 | case let .duplicateURL(url): 61 | return "URL is duplicate: \(url)" 62 | case .indexOutOfRange: 63 | return "index out of range" 64 | case let .fetchDownloadTaskFailed(url): 65 | return "did not find downloadTask in sessionManager: \(url)" 66 | case .headersMatchFailed: 67 | return "HeaderArray.count != urls.count" 68 | case .fileNamesMatchFailed: 69 | return "FileNames.count != urls.count" 70 | case let .unacceptableStatusCode(code): 71 | return "Response status code was unacceptable: \(code)" 72 | case let .cacheError(reason): 73 | return reason.errorDescription 74 | } 75 | } 76 | } 77 | 78 | extension TiercelError: CustomNSError { 79 | 80 | public static let errorDomain: String = "com.Daniels.Tiercel.Error" 81 | 82 | public var errorCode: Int { 83 | if case .unacceptableStatusCode = self { 84 | return 1001 85 | } else { 86 | return -1 87 | } 88 | } 89 | 90 | public var errorUserInfo: [String: Any] { 91 | if let errorDescription = errorDescription { 92 | return [NSLocalizedDescriptionKey: errorDescription] 93 | } else { 94 | return [String: Any]() 95 | } 96 | 97 | } 98 | } 99 | 100 | extension TiercelError.CacheErrorReason { 101 | 102 | public var errorDescription: String? { 103 | switch self { 104 | case let .cannotCreateDirectory(path, error): 105 | return "can not create directory, path: \(path), underlying: \(error)" 106 | case let .cannotRemoveItem(path, error): 107 | return "can not remove item, path: \(path), underlying: \(error)" 108 | case let .cannotCopyItem(atPath, toPath, error): 109 | return "can not copy item, atPath: \(atPath), toPath: \(toPath), underlying: \(error)" 110 | case let .cannotMoveItem(atPath, toPath, error): 111 | return "can not move item atPath: \(atPath), toPath: \(toPath), underlying: \(error)" 112 | case let .cannotRetrieveAllTasks(path, error): 113 | return "can not retrieve all tasks, path: \(path), underlying: \(error)" 114 | case let .cannotEncodeTasks(path, error): 115 | return "can not encode tasks, path: \(path), underlying: \(error)" 116 | case let .fileDoesnotExist(path): 117 | return "file does not exist, path: \(path)" 118 | case let .readDataFailed(path): 119 | return "read data failed, path: \(path)" 120 | } 121 | } 122 | 123 | 124 | } 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /Sources/General/URLConvertible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLConvertible.swift 3 | // Tiercel 4 | // 5 | // Created by Daniels on 2019/5/14. 6 | // Copyright © 2019 Daniels. All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | 29 | public protocol URLConvertible { 30 | 31 | func asURL() throws -> URL 32 | } 33 | 34 | extension String: URLConvertible { 35 | 36 | public func asURL() throws -> URL { 37 | guard let url = URL(string: self) else { throw TiercelError.invalidURL(url: self) } 38 | 39 | return url 40 | } 41 | } 42 | 43 | extension URL: URLConvertible { 44 | 45 | public func asURL() throws -> URL { return self } 46 | } 47 | 48 | extension URLComponents: URLConvertible { 49 | 50 | public func asURL() throws -> URL { 51 | guard let url = url else { throw TiercelError.invalidURL(url: self) } 52 | 53 | return url 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /Sources/Tiercel.h: -------------------------------------------------------------------------------- 1 | // 2 | // Tiercel.h 3 | // Tiercel 4 | // 5 | // Created by Daniels on 2018/3/29. 6 | // Copyright © 2018 Daniels. All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | // 26 | 27 | #import 28 | 29 | //! Project version number for Tiercel. 30 | FOUNDATION_EXPORT double TiercelVersionNumber; 31 | 32 | //! Project version string for Tiercel. 33 | FOUNDATION_EXPORT const unsigned char TiercelVersionString[]; 34 | 35 | // In this header, you should import all the public headers of your framework using statements like #import 36 | 37 | 38 | -------------------------------------------------------------------------------- /Sources/Utility/FileChecksumHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileChecksumHelper.swift 3 | // Tiercel 4 | // 5 | // Created by Daniels on 2019/1/22. 6 | // Copyright © 2019 Daniels. All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | 29 | 30 | public enum FileChecksumHelper { 31 | 32 | public enum VerificationType : Int { 33 | case md5 34 | case sha1 35 | case sha256 36 | case sha512 37 | } 38 | 39 | public enum FileVerificationError: Error { 40 | case codeEmpty 41 | case codeMismatch(code: String) 42 | case fileDoesnotExist(path: String) 43 | case readDataFailed(path: String) 44 | } 45 | 46 | private static let ioQueue: DispatchQueue = DispatchQueue(label: "com.Tiercel.FileChecksumHelper.ioQueue", 47 | attributes: .concurrent) 48 | 49 | 50 | public static func validateFile(_ filePath: String, 51 | code: String, 52 | type: VerificationType, 53 | completion: @escaping (Result) -> ()) { 54 | if code.isEmpty { 55 | completion(.failure(FileVerificationError.codeEmpty)) 56 | return 57 | } 58 | ioQueue.async { 59 | guard FileManager.default.fileExists(atPath: filePath) else { 60 | completion(.failure(FileVerificationError.fileDoesnotExist(path: filePath))) 61 | return 62 | } 63 | let url = URL(fileURLWithPath: filePath) 64 | 65 | do { 66 | let data = try Data(contentsOf: url, options: .mappedIfSafe) 67 | var string: String 68 | switch type { 69 | case .md5: 70 | string = data.tr.md5 71 | case .sha1: 72 | string = data.tr.sha1 73 | case .sha256: 74 | string = data.tr.sha256 75 | case .sha512: 76 | string = data.tr.sha512 77 | } 78 | let isCorrect = string.lowercased() == code.lowercased() 79 | if isCorrect { 80 | completion(.success(true)) 81 | } else { 82 | completion(.failure(FileVerificationError.codeMismatch(code: code))) 83 | } 84 | } catch { 85 | completion(.failure(FileVerificationError.readDataFailed(path: filePath))) 86 | } 87 | } 88 | } 89 | } 90 | 91 | 92 | 93 | extension FileChecksumHelper.FileVerificationError: LocalizedError { 94 | public var errorDescription: String? { 95 | switch self { 96 | case .codeEmpty: 97 | return "verification code is empty" 98 | case let .codeMismatch(code): 99 | return "verification code mismatch, code: \(code)" 100 | case let .fileDoesnotExist(path): 101 | return "file does not exist, path: \(path)" 102 | case let .readDataFailed(path): 103 | return "read data failed, path: \(path)" 104 | } 105 | } 106 | 107 | } 108 | 109 | 110 | -------------------------------------------------------------------------------- /Sources/Utility/ResumeDataHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ResumeDataHelper.swift 3 | // Tiercel 4 | // 5 | // Created by Daniels on 2019/1/7. 6 | // Copyright © 2019 Daniels. All rights reserved. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | 29 | 30 | internal enum ResumeDataHelper { 31 | 32 | static let infoVersionKey = "NSURLSessionResumeInfoVersion" 33 | static let currentRequestKey = "NSURLSessionResumeCurrentRequest" 34 | static let originalRequestKey = "NSURLSessionResumeOriginalRequest" 35 | static let resumeByteRangeKey = "NSURLSessionResumeByteRange" 36 | static let infoTempFileNameKey = "NSURLSessionResumeInfoTempFileName" 37 | static let infoLocalPathKey = "NSURLSessionResumeInfoLocalPath" 38 | static let bytesReceivedKey = "NSURLSessionResumeBytesReceived" 39 | static let archiveRootObjectKey = "NSKeyedArchiveRootObjectKey" 40 | 41 | internal static func handleResumeData(_ data: Data) -> Data? { 42 | if #available(iOS 11.3, *) { 43 | return data 44 | } else if #available(iOS 11.0, *) { 45 | // 修复 11.0 - 11.2 bug 46 | return deleteResumeByteRange(data) 47 | } else if #available(iOS 10.2, *) { 48 | return data 49 | } else if #available(iOS 10.0, *) { 50 | // 修复 10.0 - 10.1 bug 51 | return correctResumeData(data) 52 | } else { 53 | return data 54 | } 55 | } 56 | 57 | 58 | /// 修复 11.0 - 11.2 resumeData bug 59 | /// 60 | /// - Parameter data: 61 | /// - Returns: 62 | private static func deleteResumeByteRange(_ data: Data) -> Data? { 63 | guard let resumeDictionary = getResumeDictionary(data) else { return nil } 64 | resumeDictionary.removeObject(forKey: resumeByteRangeKey) 65 | return try? PropertyListSerialization.data(fromPropertyList: resumeDictionary, 66 | format: PropertyListSerialization.PropertyListFormat.xml, 67 | options: PropertyListSerialization.WriteOptions()) 68 | } 69 | 70 | 71 | /// 修复 10.0 - 10.1 resumeData bug 72 | /// 73 | /// - Parameter data: 74 | /// - Returns: 75 | private static func correctResumeData(_ data: Data) -> Data? { 76 | guard let resumeDictionary = getResumeDictionary(data) else { return nil } 77 | 78 | if let currentRequest = resumeDictionary[currentRequestKey] as? Data { 79 | resumeDictionary[currentRequestKey] = correct(with: currentRequest) 80 | } 81 | if let originalRequest = resumeDictionary[originalRequestKey] as? Data { 82 | resumeDictionary[originalRequestKey] = correct(with: originalRequest) 83 | } 84 | 85 | return try? PropertyListSerialization.data(fromPropertyList: resumeDictionary, 86 | format: PropertyListSerialization.PropertyListFormat.xml, 87 | options: PropertyListSerialization.WriteOptions()) 88 | } 89 | 90 | 91 | /// 把resumeData解析成字典 92 | /// 93 | /// - Parameter data: 94 | /// - Returns: 95 | internal static func getResumeDictionary(_ data: Data) -> NSMutableDictionary? { 96 | // In beta versions, resumeData is NSKeyedArchive encoded instead of plist 97 | var object: NSDictionary? 98 | if #available(OSX 10.11, iOS 9.0, *) { 99 | let keyedUnarchiver = NSKeyedUnarchiver(forReadingWith: data) 100 | 101 | do { 102 | object = try keyedUnarchiver.decodeTopLevelObject(of: NSDictionary.self, forKey: archiveRootObjectKey) 103 | if object == nil { 104 | object = try keyedUnarchiver.decodeTopLevelObject(of: NSDictionary.self, forKey: NSKeyedArchiveRootObjectKey) 105 | } 106 | } catch {} 107 | keyedUnarchiver.finishDecoding() 108 | } 109 | 110 | if object == nil { 111 | do { 112 | object = try PropertyListSerialization.propertyList(from: data, 113 | options: .mutableContainersAndLeaves, 114 | format: nil) as? NSDictionary 115 | } catch {} 116 | } 117 | 118 | if let resumeDictionary = object as? NSMutableDictionary { 119 | return resumeDictionary 120 | } 121 | 122 | guard let resumeDictionary = object else { return nil } 123 | return NSMutableDictionary(dictionary: resumeDictionary) 124 | 125 | } 126 | 127 | internal static func getTmpFileName(_ data: Data) -> String? { 128 | guard let resumeDictionary = ResumeDataHelper.getResumeDictionary(data), 129 | let version = resumeDictionary[infoVersionKey] as? Int 130 | else { return nil } 131 | if version > 1 { 132 | return resumeDictionary[infoTempFileNameKey] as? String 133 | } else { 134 | guard let path = resumeDictionary[infoLocalPathKey] as? String else { return nil } 135 | let url = URL(fileURLWithPath: path) 136 | return url.lastPathComponent 137 | } 138 | 139 | } 140 | 141 | 142 | /// 修复resumeData中的当前请求数据和原始请求数据 143 | /// 144 | /// - Parameter data: 145 | /// - Returns: 146 | private static func correct(with data: Data) -> Data? { 147 | if NSKeyedUnarchiver.unarchiveObject(with: data) != nil { 148 | return data 149 | } 150 | guard let resumeDictionary = try? PropertyListSerialization.propertyList(from: data, 151 | options: .mutableContainersAndLeaves, 152 | format: nil) as? NSMutableDictionary 153 | else { return nil } 154 | // Rectify weird __nsurlrequest_proto_props objects to $number pattern 155 | var k = 0 156 | while ((resumeDictionary["$objects"] as? NSArray)?[1] as? NSDictionary)?.object(forKey: "$\(k)") != nil { 157 | k += 1 158 | } 159 | var i = 0 160 | while ((resumeDictionary["$objects"] as? NSArray)?[1] as? NSDictionary)?.object(forKey: "__nsurlrequest_proto_prop_obj_\(i)") != nil { 161 | let arr = resumeDictionary["$objects"] as? NSMutableArray 162 | if let dic = arr?[1] as? NSMutableDictionary, let obj = dic["__nsurlrequest_proto_prop_obj_\(i)"] { 163 | dic.setObject(obj, forKey: "$\(i + k)" as NSString) 164 | dic.removeObject(forKey: "__nsurlrequest_proto_prop_obj_\(i)") 165 | arr?[1] = dic 166 | resumeDictionary["$objects"] = arr 167 | } 168 | i += 1 169 | } 170 | if ((resumeDictionary["$objects"] as? NSArray)?[1] as? NSDictionary)?.object(forKey: "__nsurlrequest_proto_props") != nil { 171 | let arr = resumeDictionary["$objects"] as? NSMutableArray 172 | if let dic = arr?[1] as? NSMutableDictionary, let obj = dic["__nsurlrequest_proto_props"] { 173 | dic.setObject(obj, forKey: "$\(i + k)" as NSString) 174 | dic.removeObject(forKey: "__nsurlrequest_proto_props") 175 | arr?[1] = dic 176 | resumeDictionary["$objects"] = arr 177 | } 178 | } 179 | 180 | if let obj = (resumeDictionary["$top"] as? NSMutableDictionary)?.object(forKey: archiveRootObjectKey) as AnyObject? { 181 | (resumeDictionary["$top"] as? NSMutableDictionary)?.setObject(obj, forKey: NSKeyedArchiveRootObjectKey as NSString) 182 | (resumeDictionary["$top"] as? NSMutableDictionary)?.removeObject(forKey: archiveRootObjectKey) 183 | } 184 | // Reencode archived object 185 | return try? PropertyListSerialization.data(fromPropertyList: resumeDictionary, 186 | format: PropertyListSerialization.PropertyListFormat.binary, 187 | options: PropertyListSerialization.WriteOptions()) 188 | } 189 | 190 | } 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | -------------------------------------------------------------------------------- /Tiercel.podspec: -------------------------------------------------------------------------------- 1 | 2 | Pod::Spec.new do |s| 3 | s.name = 'Tiercel' 4 | s.version = '3.2.6' 5 | s.swift_version = '5.0' 6 | s.summary = 'Tiercel is a lightweight, pure-Swift download framework.' 7 | 8 | 9 | s.homepage = 'https://github.com/Danie1s/Tiercel' 10 | s.license = { :type => 'MIT', :file => 'LICENSE' } 11 | s.author = { 'Daniels' => '176516837@qq.com' } 12 | s.source = { :git => 'https://github.com/Danie1s/Tiercel.git', :tag => s.version.to_s } 13 | 14 | s.ios.deployment_target = '12.0' 15 | 16 | s.source_files = 'Sources/**/*.swift' 17 | s.requires_arc = true 18 | 19 | end 20 | -------------------------------------------------------------------------------- /Tiercel.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Tiercel.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Tiercel.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Tiercel.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /TiercelTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /TiercelTests/TiercelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TiercelTests.swift 3 | // TiercelTests 4 | // 5 | // Created by Daniels on 2020/8/17. 6 | // Copyright © 2020 Daniels. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Tiercel 11 | 12 | class TiercelTests: XCTestCase { 13 | 14 | override func setUpWithError() throws { 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDownWithError() throws { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | } 21 | 22 | func testExample() throws { 23 | // This is an example of a functional test case. 24 | // Use XCTAssert and related functions to verify your tests produce the correct results. 25 | } 26 | 27 | func testPerformanceExample() throws { 28 | // This is an example of a performance test case. 29 | self.measure { 30 | // Put the code you want to measure the time of here. 31 | } 32 | } 33 | 34 | } 35 | --------------------------------------------------------------------------------