This commit is contained in:
Chris Li 2017-01-25 10:59:22 -05:00
parent 4cdb46afb8
commit 11e4dfb7c2
8 changed files with 154 additions and 194 deletions

View File

@ -131,7 +131,7 @@ class LibraryBooksController: CoreDataCollectionBaseController, UICollectionView
book.articleCountDescription
].flatMap({$0}).joined(separator: " ")
cell.descriptionLabel.text = book.desc
cell.hasPicLabel.isHidden = book.hasPic
cell.hasPicLabel.isHidden = !book.hasPic
return cell
}

View File

@ -1,12 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11762" systemVersion="16C68" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="9Nq-QX-pIk">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12100" systemVersion="16D32" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="9Nq-QX-pIk">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11757"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12072"/>
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
<capability name="Navigation items with more than one left or right bar item" minToolsVersion="7.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
@ -52,7 +53,7 @@
</collectionViewFlowLayout>
<cells>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="Cell" id="Aea-Om-Uku" customClass="LibraryCollectionCell" customModule="Kiwix" customModuleProvider="target">
<rect key="frame" x="28" y="20" width="320" height="66"/>
<rect key="frame" x="27.5" y="20" width="320" height="66"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
<rect key="frame" x="0.0" y="0.0" width="320" height="66"/>
@ -174,13 +175,22 @@
<action selector="dismissButtonTapped:" destination="cDq-va-k4I" id="RgX-lM-cfN"/>
</connections>
</barButtonItem>
<barButtonItem key="rightBarButtonItem" image="LanguageFilter" id="0v8-uV-zCG">
<connections>
<segue destination="yzc-l3-ioG" kind="popoverPresentation" identifier="showLangFilter" popoverAnchorBarButtonItem="0v8-uV-zCG" id="hlL-SF-vTM">
<popoverArrowDirection key="popoverArrowDirection" up="YES" down="YES" left="YES" right="YES"/>
</segue>
</connections>
</barButtonItem>
<rightBarButtonItems>
<barButtonItem image="LanguageFilter" id="0v8-uV-zCG">
<connections>
<segue destination="yzc-l3-ioG" kind="popoverPresentation" identifier="showLangFilter" popoverAnchorBarButtonItem="0v8-uV-zCG" id="hlL-SF-vTM">
<popoverArrowDirection key="popoverArrowDirection" up="YES" down="YES" left="YES" right="YES"/>
</segue>
</connections>
</barButtonItem>
<barButtonItem title="Downloading" id="dtT-qi-KWI">
<connections>
<segue destination="slW-nF-UiF" kind="popoverPresentation" popoverAnchorBarButtonItem="dtT-qi-KWI" id="NDd-oj-RlO">
<popoverArrowDirection key="popoverArrowDirection" up="YES" down="YES" left="YES" right="YES"/>
</segue>
</connections>
</barButtonItem>
</rightBarButtonItems>
</navigationItem>
<connections>
<outlet property="collectionView" destination="UY0-46-c3y" id="lFH-jM-jh7"/>
@ -246,6 +256,35 @@
</objects>
<point key="canvasLocation" x="-1112.8" y="-740.7796101949026"/>
</scene>
<!--Table View Controller-->
<scene sceneID="6ID-KU-NtG">
<objects>
<tableViewController id="hlk-pR-5X0" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="28" sectionFooterHeight="28" id="4As-10-hJy">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" id="GKO-mU-RLA">
<rect key="frame" x="0.0" y="28" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="GKO-mU-RLA" id="DhQ-qe-TwG">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
</tableViewCellContentView>
</tableViewCell>
</prototypes>
<connections>
<outlet property="dataSource" destination="hlk-pR-5X0" id="t2d-pl-3Xc"/>
<outlet property="delegate" destination="hlk-pR-5X0" id="uOn-D5-gXu"/>
</connections>
</tableView>
<navigationItem key="navigationItem" id="Ear-9F-grv"/>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="nGp-ZZ-iRU" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1622" y="-2118"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="009-Nq-Jlu">
<objects>
@ -274,7 +313,7 @@
<color key="backgroundColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" textLabel="dyh-6J-ZGf" detailTextLabel="d0p-hJ-zVy" style="IBUITableViewCellStyleValue1" id="PuW-jR-mYi">
<rect key="frame" x="0.0" y="56" width="375" height="44"/>
<rect key="frame" x="0.0" y="55.5" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="PuW-jR-mYi" id="hQJ-5P-hy0">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
@ -311,7 +350,7 @@
</connections>
</barButtonItem>
<segmentedControl key="titleView" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" id="ODt-wT-Y6T">
<rect key="frame" x="126" y="8" width="123" height="29"/>
<rect key="frame" x="126" y="7.5" width="123" height="29"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<segments>
<segment title="English"/>
@ -330,6 +369,24 @@
</objects>
<point key="canvasLocation" x="1623" y="-1397"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="INT-af-HVz">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="slW-nF-UiF" sceneMemberID="viewController">
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" id="AEw-P0-Tdq">
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<nil name="viewControllers"/>
<connections>
<segue destination="hlk-pR-5X0" kind="relationship" relationship="rootViewController" id="v7D-WH-08D"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="z5c-N8-xas" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="721" y="-2118"/>
</scene>
</scenes>
<resources>
<image name="Cross" width="16" height="16"/>

View File

@ -54,7 +54,6 @@
974C49681DA4266200E276E1 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 974C49671DA4266200E276E1 /* CloudKit.framework */; };
975227CD1D0227E8001D1DDE /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 975227CA1D0227E8001D1DDE /* Main.storyboard */; };
975227D01D022814001D1DDE /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 975227CF1D022814001D1DDE /* LaunchScreen.storyboard */; };
9757C74C1E106958008A9469 /* BackgroundDownload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9757C74B1E106958008A9469 /* BackgroundDownload.swift */; };
97599AA21E26D3B000BA15EF /* BookmarkBooksController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97599AA11E26D3B000BA15EF /* BookmarkBooksController.swift */; };
97599AE21E28193D00BA15EF /* BookmarkCollectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97599AE11E28193D00BA15EF /* BookmarkCollectionController.swift */; };
975B90FE1CEB909100D13906 /* iOSExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 975B90FD1CEB909100D13906 /* iOSExtensions.swift */; };
@ -213,7 +212,6 @@
974C49671DA4266200E276E1 /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; };
975227CA1D0227E8001D1DDE /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = Main.storyboard; path = "Kiwix-iOS/Storyboard/Main.storyboard"; sourceTree = SOURCE_ROOT; };
975227CF1D022814001D1DDE /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = "Kiwix-iOS/Storyboard/LaunchScreen.storyboard"; sourceTree = SOURCE_ROOT; };
9757C74B1E106958008A9469 /* BackgroundDownload.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundDownload.swift; sourceTree = "<group>"; };
97599AA11E26D3B000BA15EF /* BookmarkBooksController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarkBooksController.swift; sourceTree = "<group>"; };
97599AE11E28193D00BA15EF /* BookmarkCollectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarkCollectionController.swift; sourceTree = "<group>"; };
975B90FD1CEB909100D13906 /* iOSExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = iOSExtensions.swift; path = "Kiwix-iOS/iOSExtensions.swift"; sourceTree = SOURCE_ROOT; };
@ -271,7 +269,6 @@
97D6811C1D6F70AC00E5FA99 /* Queue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Queue.swift; sourceTree = "<group>"; };
97D6811E1D6F70AC00E5FA99 /* ScanLocalBook.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScanLocalBook.swift; sourceTree = "<group>"; };
97D681211D6F70AC00E5FA99 /* UpdateWidgetDataSourceOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdateWidgetDataSourceOperation.swift; sourceTree = "<group>"; };
97D681221D6F70AC00E5FA99 /* URLSessionDownloadTaskOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSessionDownloadTaskOperation.swift; sourceTree = "<group>"; };
97D6812B1D6F70DE00E5FA99 /* 1.5.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = 1.5.xcdatamodel; sourceTree = "<group>"; };
97D6812C1D6F70DE00E5FA99 /* 1.7.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = 1.7.xcdatamodel; sourceTree = "<group>"; };
97D6812D1D6F70DE00E5FA99 /* Kiwix.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Kiwix.xcdatamodel; sourceTree = "<group>"; };
@ -757,7 +754,6 @@
children = (
97D6811C1D6F70AC00E5FA99 /* Queue.swift */,
9764CBD21D8083AA00072D6A /* ArticleOperation.swift */,
9757C74B1E106958008A9469 /* BackgroundDownload.swift */,
970A2A211DD562CB0078BB7C /* BookOperations.swift */,
973A5C981DEBC54800C7804C /* CloudKit.swift */,
973208281DD223DB00EDD3DC /* RefreshLibrary.swift */,
@ -765,7 +761,6 @@
972F81561DDBFC79008D7289 /* Search.swift */,
97D4D64E1D874E6E00C1B065 /* SearchOperation.swift */,
97D681211D6F70AC00E5FA99 /* UpdateWidgetDataSourceOperation.swift */,
97D681221D6F70AC00E5FA99 /* URLSessionDownloadTaskOperation.swift */,
976C1DD31E300695005EDEC4 /* UIProcedure.swift */,
);
name = Operation;
@ -1059,7 +1054,6 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
9757C74C1E106958008A9469 /* BackgroundDownload.swift in Sources */,
973A5C951DEA6DD000C7804C /* URLResponseCache.swift in Sources */,
973207A51DD1984700EDD3DC /* SearchScopeAndHistoryController.swift in Sources */,
973208231DD19C7600EDD3DC /* DownloadProgress.swift in Sources */,

View File

@ -11,14 +11,15 @@ import CoreData
class DownloadTask: NSManagedObject {
class func fetch(book: Book, context: NSManagedObjectContext) -> DownloadTask? {
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "DownloadTask")
class func fetch(bookID: String, context: NSManagedObjectContext) -> DownloadTask? {
let fetchRequest = DownloadTask.fetchRequest() as! NSFetchRequest<DownloadTask>
guard let book = Book.fetch(bookID, context: context) else {return nil}
fetchRequest.predicate = NSPredicate(format: "book = %@", book)
let downloadTask = DownloadTask.fetch(fetchRequest, type: DownloadTask.self, context: context)?.first ?? insert(DownloadTask.self, context: context)
downloadTask?.creationTime = Date()
downloadTask?.book = book
guard let downloadTask = try? context.fetch(fetchRequest).first ?? DownloadTask(context: context) else {return nil}
downloadTask.creationTime = Date()
downloadTask.book = book
return downloadTask
}

View File

@ -8,7 +8,82 @@
import UIKit
class Network: NSObject {
class Network: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionDownloadDelegate {
static let shared = Network()
private override init() {}
var progresses = [String: Int64]()
let managedObjectContext = AppDelegate.persistentContainer.viewContext
var timer: Timer?
lazy var wifiSession: URLSession = {
let configuration = URLSessionConfiguration.background(withIdentifier: "org.kiwix.wifi")
configuration.allowsCellularAccess = false
configuration.isDiscretionary = false
return URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
}()
lazy var cellularSession: URLSession = {
let configuration = URLSessionConfiguration.background(withIdentifier: "org.kiwix.cellular")
configuration.allowsCellularAccess = true
configuration.isDiscretionary = false
return URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
}()
// MARK: - actions
func start(book: Book) {
guard let url = book.url else {return}
let task = (book.fileSize > 100000000 ? wifiSession: cellularSession).downloadTask(with: url)
task.taskDescription = book.id
task.resume()
let downloadTask = DownloadTask.fetch(bookID: book.id, context: managedObjectContext)
downloadTask?.state = .queued
progresses[book.id] = 0
if progresses.count == 1 { startTimer() }
}
func startTimer() {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { (timer) in
for (bookID, bytesWritten) in self.progresses {
guard let book = Book.fetch(bookID, context: self.managedObjectContext) else {continue}
book.downloadTask?.totalBytesWritten = bytesWritten
}
})
}
// MARK: - URLSessionTaskDelegate
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
}
// MARK: - URLSessionDownloadDelegate
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) {
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
guard let bookID = downloadTask.taskDescription else {return}
progresses[bookID] = bytesWritten
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
managedObjectContext.perform {
guard let bookID = downloadTask.taskDescription,
let book = Book.fetch(bookID, context: self.managedObjectContext),
let downloadTask = DownloadTask.fetch(bookID: bookID, context: self.managedObjectContext) else {return}
//book.state = .local
self.managedObjectContext.delete(downloadTask)
print("finish downloading")
self.progresses[bookID] = nil
if self.progresses.count == 0 { self.timer?.invalidate() }
}
}
}

View File

@ -1,80 +0,0 @@
//
// BookDownload.swift
// Kiwix
//
// Created by Chris Li on 12/25/16.
// Copyright © 2016 Chris Li. All rights reserved.
//
import ProcedureKit
class BackgroundDownloadProcedure: Procedure {
let task: URLSessionDownloadTask
let resumeDataProcessing: (Data?) -> Void
private let stateLock = NSLock()
private var produceResumeData = false
init(task: URLSessionDownloadTask, resumeData: @escaping (Data?) -> Void) {
self.task = task
self.resumeDataProcessing = resumeData
super.init()
add(observer: NetworkObserver())
addDidCancelBlockObserver { procedure, errors in
procedure.stateLock.withCriticalScope {
if procedure.produceResumeData {
procedure.task.cancel(byProducingResumeData: self.resumeDataProcessing)
} else {
procedure.task.cancel()
}
}
}
}
override func execute() {
stateLock.withCriticalScope {
guard !isCancelled, task.state == .suspended else { return }
task.resume()
}
}
func pause() {
produceResumeData = true
cancel()
}
}
class BookDownloadProcedure: BackgroundDownloadProcedure {
init(task: URLSessionDownloadTask) {
super.init(task: task) { data in
print("cancelled, resume data length = \(data?.count)")
}
addDidFinishBlockObserver { (procedure, errors) in
print("Download has finished")
}
}
convenience init(session: URLSession, bookID: String, url: URL) {
let task = session.downloadTask(with: url)
task.taskDescription = bookID
self.init(task: task)
}
convenience init(session: URLSession, bookID: String, resumeData: Data) {
let task = session.downloadTask(withResumeData: resumeData)
task.taskDescription = bookID
self.init(task: task)
}
override func execute() {
let context = AppDelegate.persistentContainer.viewContext
context.performAndWait {
guard let bookID = self.task.taskDescription,
let book = Book.fetch(bookID, context: context),
let downloadTask = DownloadTask.fetch(book: book, context: context) else {return}
downloadTask.state = .queued
}
super.execute()
}
}

View File

@ -91,6 +91,7 @@ extension AlertProcedure {
let alert = AlertProcedure(presentAlertFrom: context, withPreferredStyle: .actionSheet, waitForDismissal: true)
alert.title = book.title
alert.add(actionWithTitle: Localized.Library.download, style: .default) { _ in
Network.shared.start(book: book)
alert.finish()
}
alert.add(actionWithTitle: Localized.Library.copyURL, style: .default) { _ in

View File

@ -1,88 +0,0 @@
//
// URLSessionDownloadTaskOperation.swift
// Kiwix
//
// Created by Chris Li on 7/11/16.
// Copyright © 2016 Chris Li. All rights reserved.
//
import ProcedureKit
class URLSessionDownloadTaskOperation: Procedure {
enum KeyPath: String {
case State = "state"
}
let task: URLSessionTask
fileprivate(set) var produceResumeData = false
fileprivate var removedObserved = false
fileprivate let lock = NSLock()
init(downloadTask: URLSessionDownloadTask) {
self.task = downloadTask
super.init()
add(observer: NetworkObserver())
addObserver(DidCancelObserver { _ in
if self.produceResumeData {
downloadTask.cancelByProducingResumeData({ _ in })
} else {
downloadTask.cancel()
}
})
}
func cancel(produceResumeData: Bool) {
self.produceResumeData = produceResumeData
cancel()
}
override func execute() {
guard task.state == .suspended || task.state == .running else {
finish()
return
}
task.addObserver(self, forKeyPath: "state", options: NSKeyValueObservingOptions(), context: &URLSessionTaskOperationKVOContext)
if task.state == .suspended {
task.resume()
}
}
override func
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [String : AnyObject]?, context: UnsafeMutableRawPointer) {
guard context == &URLSessionTaskOperationKVOContext else { return }
lock.withCriticalScope {
if object === task && keyPath == KeyPath.State.rawValue && !removedObserved {
if case .completed = task.state {
finish(task.error)
}
switch task.state {
case .completed, .canceling:
task.removeObserver(self, forKeyPath: KeyPath.State.rawValue)
removedObserved = true
default:
break
}
}
}
}
}
// swiftlint:disable variable_name
private var URLSessionTaskOperationKVOContext = 0
// swiftlint:enable variable_name
extension NSLock {
func withCriticalScope<T>(_ block: () -> T) -> T {
lock()
let value = block()
unlock()
return value
}
}