Download Progress

This commit is contained in:
Chris Li 2016-08-28 20:40:59 -04:00
parent 971dc53ab9
commit 4ff054f0e5
8 changed files with 78 additions and 146 deletions

View File

@ -105,7 +105,14 @@ class DownloadTasksController: UITableViewController, NSFetchedResultsController
guard let progress = Network.shared.operations[id]?.progress else {return} guard let progress = Network.shared.operations[id]?.progress else {return}
cell.progressLabel.text = progress.fractionCompletedDescription cell.progressLabel.text = progress.fractionCompletedDescription
cell.progressView.setProgress(Float(progress.fractionCompleted), animated: animated) cell.progressView.setProgress(Float(progress.fractionCompleted), animated: animated)
cell.detailLabel.text = progress.localizedAdditionalDescription.stringByReplacingOccurrencesOfString(" ", withString: "\n") cell.detailLabel.text = {
let string = progress.progressAndSpeedDescription
if string.containsString("") {
return string.stringByReplacingOccurrencesOfString("", withString: "\n")
} else {
return string + "\n" + NSLocalizedString("Estimating Speed and Remaining Time", comment: "")
}
}()
} }
// MARK: Other Data Source // MARK: Other Data Source
@ -147,13 +154,13 @@ class DownloadTasksController: UITableViewController, NSFetchedResultsController
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {} override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {}
override func tableView(tableView: UITableView, editActionsForRowAtIndexPath indexPath: NSIndexPath) -> [UITableViewRowAction]? { override func tableView(tableView: UITableView, editActionsForRowAtIndexPath indexPath: NSIndexPath) -> [UITableViewRowAction]? {
let remove = UITableViewRowAction(style: .Destructive, title: LocalizedStrings.remove) { (action, indexPath) -> Void in let cancel = UITableViewRowAction(style: .Destructive, title: LocalizedStrings.Common.cancel) { (action, indexPath) -> Void in
guard let downloadTask = self.fetchedResultController.objectAtIndexPath(indexPath) as? DownloadTask, guard let downloadTask = self.fetchedResultController.objectAtIndexPath(indexPath) as? DownloadTask,
let bookID = downloadTask.book?.id else {return} let bookID = downloadTask.book?.id else {return}
let operation = CancelBookDownloadOperation(bookID: bookID) let operation = CancelBookDownloadOperation(bookID: bookID)
GlobalOperationQueue.sharedInstance.addOperation(operation) GlobalOperationQueue.sharedInstance.addOperation(operation)
} }
return [remove] return [cancel]
} }
// MARK: - Fetched Results Controller // MARK: - Fetched Results Controller

View File

@ -49,7 +49,7 @@
</dict> </dict>
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1.7.1441</string> <string>1.7.1481</string>
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>
<false/> <false/>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>

View File

@ -1,129 +0,0 @@
//
// DownloadProgress.swift
// Kiwix
//
// Created by Chris Li on 3/23/16.
// Copyright © 2016 Chris. All rights reserved.
//
import UIKit
class DownloadProgress: NSProgress {
let book: Book
private let observationCount = 9
private let sampleFrequency: NSTimeInterval = 0.3
private var speeds = [Double]()
private var timer: NSTimer?
private weak var task: NSURLSessionDownloadTask?
init(book: Book) {
self.book = book
super.init(parent: nil, userInfo: [NSProgressFileOperationKindKey: NSProgressFileOperationKindDownloading])
self.kind = NSProgressKindFile
self.totalUnitCount = book.fileSize
self.completedUnitCount = book.downloadTask?.totalBytesWritten ?? 0
print("totalBytesWritten: \(book.downloadTask?.totalBytesWritten)")
}
deinit {
timer?.invalidate()
}
func downloadStarted(task: NSURLSessionDownloadTask) {
self.task = task
recordSpeed()
timer = NSTimer.scheduledTimerWithTimeInterval(sampleFrequency, target: self, selector: #selector(DownloadProgress.recordSpeed), userInfo: nil, repeats: true)
}
func downloadTerminated() {
timer?.invalidate()
speeds.removeAll()
setUserInfoObject(nil, forKey: NSProgressThroughputKey)
setUserInfoObject(nil, forKey: NSProgressEstimatedTimeRemainingKey)
}
func recordSpeed() {
guard let task = task else {return}
/*
Check if the countOfBytesReceived and countOfBytesExpectedToReceive in NSURLSessionDownloadTask
object is both zero. When a NSURLSessionDownloadTask resumes, these two value will be zero at first.
We don't want to accidently set these two values in this progress object to be all 0.
*/
guard task.countOfBytesReceived != 0 && task.countOfBytesExpectedToReceive != 0 else {return}
let previousCompletedUnitCount = completedUnitCount
completedUnitCount = task.countOfBytesReceived
totalUnitCount = task.countOfBytesExpectedToReceive
let speed = Double(completedUnitCount - previousCompletedUnitCount) / sampleFrequency
speeds.insert(speed, atIndex: 0)
if speeds.count > observationCount {speeds.popLast()}
}
var maSpeed: Double? {
let alpha = 0.5
var remainingWeight = 1.0
var speedMA = 0.0
guard speeds.count >= observationCount else {return nil}
for index in 0..<speeds.count {
let weight = alpha * pow(1.0 - alpha, Double(index))
remainingWeight -= weight
speedMA += weight * speeds[index]
}
speedMA += remainingWeight * (speeds.last ?? 0.0)
return speedMA > 0.0 ? speedMA : nil
}
// MARK: - Descriptions
var sizeDescription: String {
return localizedAdditionalDescription.componentsSeparatedByString("").first ?? ""
}
var speedAndRemainingTimeDescription: String? {
guard let maSpeed = self.maSpeed else {return nil}
setUserInfoObject(NSNumber(double: maSpeed), forKey: NSProgressThroughputKey)
let remainingSeconds = Double(totalUnitCount - completedUnitCount) / maSpeed
setUserInfoObject(NSNumber(double: remainingSeconds), forKey: NSProgressEstimatedTimeRemainingKey)
let components = localizedAdditionalDescription.componentsSeparatedByString("")
return components.count > 1 ? components.last : nil
}
var percentDescription: String {
let formatter = NSNumberFormatter()
formatter.numberStyle = .PercentStyle
formatter.maximumIntegerDigits = 3
formatter.maximumFractionDigits = 0
formatter.locale = NSLocale.currentLocale()
return formatter.stringFromNumber(NSNumber(double: fractionCompleted)) ?? ""
}
var sizeAndPercentDescription: String {
var strings = [String]()
strings.append(sizeDescription)
strings.append(percentDescription)
return strings.joinWithSeparator(" - ")
}
override var description: String {
guard let state = book.downloadTask?.state else {return " \n "}
switch state {
case .Queued: return sizeAndPercentDescription + "\n" + LocalizedStrings.queued
case .Downloading: return sizeAndPercentDescription + "\n" + (speedAndRemainingTimeDescription ?? LocalizedStrings.estimatingSpeedAndRemainingTime)
case .Paused: return sizeAndPercentDescription + "\n" + LocalizedStrings.paused
case .Error: return sizeDescription + "\n" + LocalizedStrings.downloadError
}
}
}
extension LocalizedStrings {
class var starting: String {return NSLocalizedString("Starting", comment: "Library: Download task description")}
class var resuming: String {return NSLocalizedString("Resuming", comment: "Library: Download task description")}
class var paused: String {return NSLocalizedString("Paused", comment: "Library: Download task description")}
class var downloadError: String {return NSLocalizedString("Download Error", comment: "Library: Download task description")}
class var queued: String {return NSLocalizedString("Queued", comment: "Library: Download task description")}
class var estimatingSpeedAndRemainingTime: String {return NSLocalizedString("Estimating speed and remaining time", comment: "Library: Download task description")}
}

View File

@ -469,11 +469,11 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/> <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<prototypes> <prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" rowHeight="92" id="ekT-ed-PU9" customClass="DownloadBookCell" customModule="Kiwix" customModuleProvider="target"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" rowHeight="82" id="ekT-ed-PU9" customClass="DownloadBookCell" customModule="Kiwix" customModuleProvider="target">
<rect key="frame" x="0.0" y="92" width="375" height="92"/> <rect key="frame" x="0.0" y="92" width="375" height="82"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="ekT-ed-PU9" id="oM4-Hy-Mkf"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="ekT-ed-PU9" id="oM4-Hy-Mkf">
<frame key="frameInset" width="375" height="91"/> <frame key="frameInset" width="375" height="81.5"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" lineBreakMode="tailTruncation" minimumFontSize="8" adjustsLetterSpacingToFitWidth="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Hji-3G-yaJ"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" lineBreakMode="tailTruncation" minimumFontSize="8" adjustsLetterSpacingToFitWidth="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Hji-3G-yaJ">
@ -506,7 +506,7 @@
<constraint firstItem="6Zg-Xf-xgS" firstAttribute="leading" secondItem="Hji-3G-yaJ" secondAttribute="trailing" constant="2" id="5lO-Sp-t2G"/> <constraint firstItem="6Zg-Xf-xgS" firstAttribute="leading" secondItem="Hji-3G-yaJ" secondAttribute="trailing" constant="2" id="5lO-Sp-t2G"/>
<constraint firstItem="v8H-ZV-HNV" firstAttribute="leading" secondItem="oM4-Hy-Mkf" secondAttribute="leadingMargin" id="7FW-0t-ljT"/> <constraint firstItem="v8H-ZV-HNV" firstAttribute="leading" secondItem="oM4-Hy-Mkf" secondAttribute="leadingMargin" id="7FW-0t-ljT"/>
<constraint firstAttribute="leadingMargin" secondItem="Too-68-SzG" secondAttribute="leading" constant="-2" id="9Vd-3e-m5f"/> <constraint firstAttribute="leadingMargin" secondItem="Too-68-SzG" secondAttribute="leading" constant="-2" id="9Vd-3e-m5f"/>
<constraint firstAttribute="bottomMargin" secondItem="v8H-ZV-HNV" secondAttribute="bottom" id="9ac-Vl-xk9"/> <constraint firstAttribute="bottomMargin" secondItem="v8H-ZV-HNV" secondAttribute="bottom" constant="-8" id="9ac-Vl-xk9"/>
<constraint firstAttribute="trailingMargin" secondItem="6Zg-Xf-xgS" secondAttribute="trailing" id="D9Q-Dz-SXA"/> <constraint firstAttribute="trailingMargin" secondItem="6Zg-Xf-xgS" secondAttribute="trailing" id="D9Q-Dz-SXA"/>
<constraint firstItem="g0o-rT-qxm" firstAttribute="top" secondItem="Too-68-SzG" secondAttribute="bottom" constant="4" id="IJR-yJ-4xs"/> <constraint firstItem="g0o-rT-qxm" firstAttribute="top" secondItem="Too-68-SzG" secondAttribute="bottom" constant="4" id="IJR-yJ-4xs"/>
<constraint firstItem="g0o-rT-qxm" firstAttribute="trailing" secondItem="oM4-Hy-Mkf" secondAttribute="trailingMargin" id="OFh-b3-2bf"/> <constraint firstItem="g0o-rT-qxm" firstAttribute="trailing" secondItem="oM4-Hy-Mkf" secondAttribute="trailingMargin" id="OFh-b3-2bf"/>

View File

@ -21,7 +21,7 @@
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1.7.1771</string> <string>1.7.1833</string>
<key>NSExtension</key> <key>NSExtension</key>
<dict> <dict>
<key>NSExtensionMainStoryboard</key> <key>NSExtensionMainStoryboard</key>

View File

@ -238,7 +238,6 @@
971A10471D022CBE007FC62C /* SearchResultTBVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SearchResultTBVC.swift; path = "Kiwix-iOS/Controller/SearchResultTBVC.swift"; sourceTree = SOURCE_ROOT; }; 971A10471D022CBE007FC62C /* SearchResultTBVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SearchResultTBVC.swift; path = "Kiwix-iOS/Controller/SearchResultTBVC.swift"; sourceTree = SOURCE_ROOT; };
971A10481D022CBE007FC62C /* SearchBooksVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SearchBooksVC.swift; path = "Kiwix-iOS/Controller/SearchBooksVC.swift"; sourceTree = SOURCE_ROOT; }; 971A10481D022CBE007FC62C /* SearchBooksVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SearchBooksVC.swift; path = "Kiwix-iOS/Controller/SearchBooksVC.swift"; sourceTree = SOURCE_ROOT; };
971A10511D022D9D007FC62C /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; 971A10511D022D9D007FC62C /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
971A106D1D022E62007FC62C /* DownloadProgress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DownloadProgress.swift; path = "Kiwix-iOS/Model/DownloadProgress.swift"; sourceTree = SOURCE_ROOT; };
971A106E1D022E62007FC62C /* Network_old.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Network_old.swift; path = "Kiwix-iOS/Model/Network_old.swift"; sourceTree = SOURCE_ROOT; }; 971A106E1D022E62007FC62C /* Network_old.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Network_old.swift; path = "Kiwix-iOS/Model/Network_old.swift"; sourceTree = SOURCE_ROOT; };
971A10781D022F05007FC62C /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = "<group>"; }; 971A10781D022F05007FC62C /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
971A107A1D022F74007FC62C /* DownloaderLearnMore.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; name = DownloaderLearnMore.html; path = Kiwix/HelpDocuments/DownloaderLearnMore.html; sourceTree = SOURCE_ROOT; }; 971A107A1D022F74007FC62C /* DownloaderLearnMore.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; name = DownloaderLearnMore.html; path = Kiwix/HelpDocuments/DownloaderLearnMore.html; sourceTree = SOURCE_ROOT; };
@ -571,7 +570,6 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
971A106E1D022E62007FC62C /* Network_old.swift */, 971A106E1D022E62007FC62C /* Network_old.swift */,
971A106D1D022E62007FC62C /* DownloadProgress.swift */,
); );
name = Network; name = Network;
path = Kiwix; path = Kiwix;

View File

@ -48,12 +48,10 @@ class DownloadBookOperation: URLSessionDownloadTaskOperation {
} }
class DownloadProgress: NSProgress { class DownloadProgress: NSProgress {
init(completedUnitCount: Int64, totalUnitCount: Int64) { typealias TimePoint = (completedUnitCount: Int64, timeStamp: NSTimeInterval)
super.init(parent: nil, userInfo: [NSProgressFileOperationKindKey: NSProgressFileOperationKindDownloading]) private var timePoints = [TimePoint]()
self.kind = NSProgressKindFile private let timePointMinCount: Int = 20
self.totalUnitCount = totalUnitCount private let timePointMaxCount: Int = 200
self.completedUnitCount = completedUnitCount
}
private lazy var percentFormatter: NSNumberFormatter = { private lazy var percentFormatter: NSNumberFormatter = {
let formatter = NSNumberFormatter() let formatter = NSNumberFormatter()
@ -65,9 +63,62 @@ class DownloadProgress: NSProgress {
return formatter return formatter
}() }()
init(completedUnitCount: Int64 = 0, totalUnitCount: Int64) {
super.init(parent: nil, userInfo: [NSProgressFileOperationKindKey: NSProgressFileOperationKindDownloading])
self.kind = NSProgressKindFile
self.totalUnitCount = totalUnitCount
self.completedUnitCount = completedUnitCount
}
override var completedUnitCount: Int64 {
didSet {
add(completedUnitCount)
}
}
// MARK: - Descriptions
var fractionCompletedDescription: String? { var fractionCompletedDescription: String? {
return percentFormatter.stringFromNumber(NSNumber(double: fractionCompleted)) return percentFormatter.stringFromNumber(NSNumber(double: fractionCompleted))
} }
var progressAndSpeedDescription: String! {
calculateSpeed()
return localizedAdditionalDescription
}
func calculateSpeed() {
guard self.timePoints.count >= timePointMinCount else {return}
let smoothingFactor = 1 / Double(self.timePoints.count)
var timePoints = self.timePoints
var oldPoint = timePoints.removeFirst()
let recentPoint = timePoints.removeFirst()
var averageSpeed: Double = Double(recentPoint.completedUnitCount - oldPoint.completedUnitCount) / (recentPoint.timeStamp - oldPoint.timeStamp)
oldPoint = recentPoint
for recentPoint in timePoints {
let lastSpeed = Double(recentPoint.completedUnitCount - oldPoint.completedUnitCount) / (recentPoint.timeStamp - oldPoint.timeStamp)
oldPoint = recentPoint
averageSpeed = smoothingFactor * lastSpeed + (1 - smoothingFactor) * averageSpeed
}
setUserInfoObject(NSNumber(double: averageSpeed), forKey: NSProgressThroughputKey)
let remainingSeconds = Double(totalUnitCount - completedUnitCount) / averageSpeed
setUserInfoObject(NSNumber(double: remainingSeconds), forKey: NSProgressEstimatedTimeRemainingKey)
}
private func add(completedUnitCount: Int64) {
let timeStamp = NSDate().timeIntervalSince1970
if let lastPoint = timePoints.last {
guard timeStamp - lastPoint.timeStamp > 0.2 else {return}
timePoints.append((completedUnitCount, timeStamp))
if timePoints.count > timePointMaxCount { timePoints.removeFirst() }
} else {
timePoints.append((completedUnitCount, timeStamp))
}
}
} }
class CancelBookDownloadOperation: Operation { class CancelBookDownloadOperation: Operation {

View File

@ -70,6 +70,11 @@ class LocalizedStrings {
class var Library: String {return NSLocalizedString("Library", comment: "OS X, Preference")} class var Library: String {return NSLocalizedString("Library", comment: "OS X, Preference")}
class var ZimFiles: String {return NSLocalizedString("Zim Files", comment: "OS X, Preference")} class var ZimFiles: String {return NSLocalizedString("Zim Files", comment: "OS X, Preference")}
class Common {
private static let comment = "Common"
static let cancel = "Cancel"
}
class LibraryTabTitle { class LibraryTabTitle {
private static let comment = "Library Tab Titles" private static let comment = "Library Tab Titles"
static let cloud = "Cloud" static let cloud = "Cloud"