├── .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 | [](http://cocoapods.org/pods/Tiercel)
6 | [](http://cocoapods.org/pods/Tiercel)
7 | []()
8 | [](https://swift.org/package-manager/)
9 | [](https://www.apple.com/nl/ios/)
10 | [](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 |
--------------------------------------------------------------------------------