I have a situation in my App which I need to upload several files ranging from 10-50+- (small size, about 5 mb) and also need to support continuing in the background if needed.
At this point I'm able to upload perfectly while the app is in the foreground but once the app goes into background the current running upload continue running and finish in the background, but for some reason the rest of the operations never gets called.
Here is my code
Background queue init:
private var _backgroundUploadQueue = OperationQueue()
Background queue setup:
self._backgroundUploadQueue.maxConcurrentOperationCount = 4
self._backgroundUploadQueue.name = "Background Queue"
self._backgroundUploadQueue.qualityOfService = .background
Creating the operetions:
private func _createUploadOperation(from action: S3UploadAction) -> AsyncBlockOperation {
let operation = AsyncBlockOperation({ [weak self] operation in
guard !operation.isCancelled else {
operation.finish()
return
}
self?._s3SinglePartUploader.upload(
action: action,
completion: { [weak self] result in
self?._handleUploadCompletion(operation: operation, action: action, result: result)
}
)
self?._singlePartUploadStarted(action: action)
})
operation.name = "\(action.recordingSetVersion)_\(action.partNumber)"
return operation
}
Adding operations:
private func _startBackgroundUploading() {
for (_, actions) in self._uploadActions.enumerated() {
for action in actions.value {
let uploadOperation = self._createUploadOperation(from: action)
self._backgroundUploadQueue.addOperation(uploadOperation)
}
}
}
In the AppDelegate I add this code:
func application(
_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: #escaping () -> Void
) {
S3Uploader.shared.setBackgourndSessionCompletion(backgroundTaskCompletion: completionHandler)
AWSS3TransferUtility.interceptApplication(
application,
handleEventsForBackgroundURLSession: identifier,
completionHandler: completionHandler
)
}
Related
My app uses CloudKit and I am trying to implement background fetch.
The method in App Delegate calls a method in my main view controller which checks for changes in the CloudKit database.
However, I realise that I am not calling the completion handler correctly, as the closures for the CloudKit will return asynchronously. I am really unsure how best to call the completion handler in the app delegate method once the operation is complete. Can I pass the completion handler through to the view controller method?
App Delegate
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
// Code to get a reference to main view controller
destinationViewController.getZoneChanges()
completionHandler(.newData)
}
}
Main view controller method to get CloudKit changes
// Fetch zone changes (a method in main table view controller)
func getZoneChanges() {
DispatchQueue.global(qos: .userInitiated).async {
let customZone = CKRecordZone(zoneName: "Drugs")
let zoneID = customZone.zoneID
let zoneIDs = [zoneID]
let changeToken = UserDefaults.standard.serverChangeToken // Custom way of accessing User Defaults using an extension
// Look up the previous change token for each zone
var optionsByRecordZoneID = [CKRecordZone.ID: CKFetchRecordZoneChangesOperation.ZoneOptions]()
// Some other functioning code to process options
// CK Zone Changes Operation
let operation = CKFetchRecordZoneChangesOperation(recordZoneIDs: zoneIDs, optionsByRecordZoneID: optionsByRecordZoneID)
// Closures for records changed, deleted etc.
// Closure details omitted for brevity as fully functional as expected.
// These closures change data model, Spotlight indexing, notifications and trigger UI refresh etc.
operation.recordChangedBlock = { (record) in
// Code...
}
operation.recordWithIDWasDeletedBlock = { (recordId, string) in
// Code...
}
operation.recordZoneChangeTokensUpdatedBlock = { (zoneId, token, data) in
UserDefaults.standard.serverChangeToken = changeToken
UserDefaults.standard.synchronize()
}
operation.recordZoneFetchCompletionBlock = { (zoneId, changeToken, _, _, error) in
if let error = error {
print("Error fetching zone changes: \(error.localizedDescription)")
}
UserDefaults.standard.serverChangeToken = changeToken
UserDefaults.standard.synchronize()
}
operation.fetchRecordZoneChangesCompletionBlock = { (error) in
if let error = error {
print("Error fetching zone changes: \(error.localizedDescription)")
} else {
print("Changes fetched successfully!")
// Save local items
self.saveData() // Uses NSCoding
}
}
CKContainer.default().privateCloudDatabase.add(operation)
}
}
Update your getZoneChanges to have a completion parameter.
func getZoneChanges(completion: #escaping (Bool) -> Void) {
// the rest of your code
operation.fetchRecordZoneChangesCompletionBlock = { (error) in
if let error = error {
print("Error fetching zone changes: \(error.localizedDescription)")
completion(false)
} else {
print("Changes fetched successfully!")
completion(true)
}
}
}
Then you can update the app delegate method to use it:
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
// Code to get a reference to main view controller
destinationViewController.getZoneChanges { (success) in
completionHandler(success ? .newData : .noData)
}
}
}
I'm using AWSAppSyncClient to upload files but I'm struggling to connect the upload progress hook with the view.
AWSAppSyncClient is a property of the the application delegate initialized with an S3ObjectManager. The object manager method upload has access to the upload progress via the AWSTransferUtilityUplaodExpression:
expression.progressBlock = {(task, progress) in
DispatchQueue.main.async(execute: {
// Can we update the controller's progress bar here?
print("Progress: \(Float(progress.fractionCompleted))")
})
}
My controller invokes the upload by calling perform:
var appSyncClient: AWSAppSyncClient? // retrieved from the app delegate singleton
appSyncClient?.perform(mutation: CreatePostMutation(input: input)) { (result, error) in ...
What I am struggling with: how do I provide the S3ObjectManager a reference to the controller? I thought of instantiating the AWSAppSyncClient in each controller, and maybe using some sort of delegate pattern?
It's probably overkill to instantiate a new client on each view controller. Setup & teardown take a bit of time & system resources to perform, and you'd probably prefer to keep those activities separate from the view controller in any case, just for separation of responsibilities.
There isn't really a good way of registering a per-object listener, since mutations are queued for eventual, asynchronous delivery. Your delegate idea seems like the best approach at this point.
NOTE: Code below is untested, and not thread-safe.
For example, you could declare a singleton delegate that manages watchers for individual views that need to report progress:
class AppSyncS3ObjectManagerProgressWatcher {
typealias ProgressSubscription = UUID
static let shared = AppSyncS3ObjectManagerProgressWatcher()
private var watchers = [UUID: AppSyncS3ObjectManagerProgressDelegate?]()
func add(_ watcher: AppSyncS3ObjectManagerProgressDelegate) -> ProgressSubscription {
let subscription = UUID()
weak var weakWatcher = watcher
watchers[subscription] = weakWatcher
return subscription
}
func remove(_ subscription: ProgressSubscription?) {
guard let subscription = subscription else {
return
}
watchers[subscription] = nil
}
}
extension AppSyncS3ObjectManagerProgressWatcher: AppSyncS3ObjectManagerProgressDelegate {
func progressReportingExpression(forDownloadingObject object: AWSS3ObjectProtocol) -> AWSS3TransferUtilityDownloadExpression {
let expression = AWSS3TransferUtilityDownloadExpression()
expression.progressBlock = { _, progress in
self.didReportProgress(forDownloadingObject: object, progress: progress)
}
return expression
}
func progressReportingExpression(forUploadingObject object: AWSS3ObjectProtocol & AWSS3InputObjectProtocol) -> AWSS3TransferUtilityUploadExpression {
let expression = AWSS3TransferUtilityUploadExpression()
expression.progressBlock = { _, progress in
self.didReportProgress(forUploadingObject: object, progress: progress)
}
return expression
}
func didReportProgress(forDownloadingObject object: AWSS3ObjectProtocol, progress: Progress) {
for watcher in watchers.values {
watcher?.didReportProgress(forDownloadingObject: object, progress: progress)
}
}
func didReportProgress(forUploadingObject object: AWSS3ObjectProtocol & AWSS3InputObjectProtocol, progress: Progress) {
for watcher in watchers.values {
watcher?.didReportProgress(forUploadingObject: object, progress: progress)
}
}
}
Wherever you conform S3TransferUtility to S3ObjectManager, you would do something like:
extension AWSS3TransferUtility: AWSS3ObjectManager {
public func download(s3Object: AWSS3ObjectProtocol, toURL: URL, completion: #escaping ((Bool, Error?) -> Void)) {
let completionBlock: AWSS3TransferUtilityDownloadCompletionHandlerBlock = { task, url, data, error -> Void in
if let _ = error {
completion(false, error)
} else {
completion(true, nil)
}
}
let progressReportingExpression = AppSyncS3ObjectManagerProgressWatcher
.shared
.progressReportingExpression(forDownloadingObject: s3Object)
let _ = self.download(
to: toURL,
bucket: s3Object.getBucketName(),
key: s3Object.getKeyName(),
expression: progressReportingExpression,
completionHandler: completionBlock)
}
public func upload(s3Object: AWSS3ObjectProtocol & AWSS3InputObjectProtocol, completion: #escaping ((_ success: Bool, _ error: Error?) -> Void)) {
let completionBlock : AWSS3TransferUtilityUploadCompletionHandlerBlock = { task, error -> Void in
if let _ = error {
completion(false, error)
} else {
completion(true, nil)
}
}
let progressReportingExpression = AppSyncS3ObjectManagerProgressWatcher
.shared
.progressReportingExpression(forUploadingObject: s3Object)
let _ = self.uploadFile(
s3Object.getLocalSourceFileURL()!,
bucket: s3Object.getBucketName(),
key: s3Object.getKeyName(),
contentType: s3Object.getMimeType(),
expression: progressReportingExpression,
completionHandler: completionBlock
).continueWith { (task) -> Any? in
if let err = task.error {
completion(false, err)
}
return nil
}
}
}
And then in the progress reporting view:
override func awakeFromNib() {
super.awakeFromNib()
progressSubscription = AppSyncS3ObjectManagerProgressWatcher.shared.add(self)
}
func didReportProgress(forUploadingObject object: AWSS3InputObjectProtocol & AWSS3ObjectProtocol, progress: Progress) {
// TODO: Filter by object local URI/key/etc to ensure we're updating the correct progress
print("Progress received for \(object.getKeyName()): \(progress.fractionCompleted)")
self.progress = progress
}
As I noted, this code is untested, but it should outline a general approach for you to start from. I'd welcome your feedback and would like to hear what approach you eventually settle on.
Finally, please feel free to open a feature request on our issues page: https://github.com/awslabs/aws-mobile-appsync-sdk-ios/issues
I am taking user input to download files from the server. The downloading task may include requesting web services.
I am expecting something like this:
1) Whenever the user selects a file to download or requesting for web
service, then it should be treated as one block of operation or task
and should go in the queue which will be managed globally at the app
level.
2) At the same time if the queue is empty then it should
automatically start executing the current task.
3) If queue contains
any operation then it should execute all old operation in
synchronously then execute the last one.
Can any one suggest how this can be done by the optimized way?
Take a look what I tried:
class func downloadChaptersFromDownloadQueue() {
let gbm = GlobalMethods()
for chapterDetail in gbm.downloadOpertationQueue.array.enumerated() {
if chapterDetail.element.chapterdata.state == .non || chapterDetail.element.chapterdata.state == .paused || chapterDetail.element.chapterdata.state == .downloading {
gbm.downloadOpertationQueue[chapterDetail.offset].chapterdata.state = .downloading
let s = DispatchSemaphore(value: 0)
self.downloadActivty(courseId: chapterDetail.element.courseId, mod: chapterDetail.element.chapterdata, selectedIndexpath: chapterDetail.element.cellForIndexpath, success: { (result) in
if (result) {
if (WC_SQLite.shared.updateChapterState(courseId: chapterDetail.element.courseId, chapterId: chapterDetail.element.chapterdata.id, state: .downloaded)) {
s.signal()
gbm.downloadOpertationQueue[chapterDetail.offset].chapterdata.state = .downloaded
NotificationCenter.default.post(name: NSNotification.Name(("DownloadChapter")), object: self, userInfo: ["progress": 1.0, "notifIdentifier":(chapterDetail.element.cellForIndexpath)])
}
else {
s.signal()
gbm.downloadOpertationQueue[chapterDetail.offset].chapterdata.state = .non
NotificationCenter.default.post(name: NSNotification.Name(("DownloadChapter")), object: self, userInfo: ["progress": -1.0, "notifIdentifier":(chapterDetail.element.cellForIndexpath)])
}
}
else {
_ = WC_SQLite.shared.updateChapterState(courseId: chapterDetail.element.courseId, chapterId: chapterDetail.element.chapterdata.id, state: .non)
s.signal()
gbm.downloadOpertationQueue[chapterDetail.offset].chapterdata.state = .non
NotificationCenter.default.post(name: NSNotification.Name(("DownloadChapter")), object: self, userInfo: ["progress": -1.0, "notifIdentifier":(chapterDetail.element.cellForIndexpath)])
}
})
s.wait()
}
}
}
Create an async queue. First use a dispatch group to track how many requests are completed and get notified when all are finished (completely async).
Next, enqueue ALL your requests. Each request should have a unique identifier so you know which request completed or failed (the chapterId & pageNumber in your case should be enough).
Execute all the requests at once (again it is async) and you will be notified when each one completes (on the main queue through your completion block). The completion block should be called with all the requests responses and their unique identifiers.
Example:
class NetworkResponse {
let data: Data?
let response: URLResponse?
let error: Error?
init(data: Data?, response: URLResponse?, error: Error?) {
self.data = data
self.response = response
self.error = error
}
}
class NetworkQueue {
static let instance = NetworkQueue()
private let group = DispatchGroup()
private let lock = DispatchSemaphore(value: 0)
private var tasks = Array<URLSessionDataTask>()
private var responses = Dictionary<String, NetworkResponse>()
private init() {
}
public func enqueue(request: URLRequest, requestID: String) {
//Create a task for each request and add it to the queue (we do not execute it yet). Every request that is created, we enter our group.
self.group.enter();
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
//Only one thread can modify the array at any given time.
objc_sync_enter(self)
self.responses.updateValue(NetworkResponse(data: data, response: response, error: error), forKey: requestID)
objc_sync_exit(self)
//Once the request is complete, it needs to leave the group.
self.group.leave()
}
//Add each task to the queue.
self.tasks.append(task)
}
public func execute(completion: #escaping (_ responses: Dictionary<String, NetworkResponse>) -> Void) {
//Get notified on the main queue when every single request is completed (they all happen asynchronously, but we get one notification)
self.group.notify(queue: DispatchQueue.main) {
//Call our completion block with all the responses. Might be better to use a sorted dictionary or something here so that the responses are in order.. but for now, a Dictionary with unique identifiers will be fine.
completion(self.responses)
}
//Execute every task in the queue.
for task in self.tasks {
task.resume()
}
//Clear all executed tasks from the queue.
self.tasks.removeAll()
}
}
EDIT (Using your own code):
class func downloadChaptersFromDownloadQueue() {
let gbm = GlobalMethods()
let group = DispatchGroup()
let lock = NSLock()
//Get notified when ALL tasks have completed.
group.notify(queue: DispatchQueue.main) {
print("FINISHED ALL TASKS -- DO SOMETHING HERE")
}
//Initially enter to stall the completion
group.enter()
defer {
group.leave() //Exit the group to complete the enqueueing.
}
for chapterDetail in gbm.downloadOpertationQueue.array.enumerated() {
if chapterDetail.element.chapterdata.state == .non || chapterDetail.element.chapterdata.state == .paused || chapterDetail.element.chapterdata.state == .downloading {
gbm.downloadOpertationQueue[chapterDetail.offset].chapterdata.state = .downloading
//Enter the group for each downloadOperation
group.enter()
self.downloadActivty(courseId: chapterDetail.element.courseId, mod: chapterDetail.element.chapterdata, selectedIndexpath: chapterDetail.element.cellForIndexpath, success: { (result) in
lock.lock()
defer {
group.leave() //Leave the group when each downloadOperation is completed.
}
if (result) {
if (WC_SQLite.shared.updateChapterState(courseId: chapterDetail.element.courseId, chapterId: chapterDetail.element.chapterdata.id, state: .downloaded)) {
gbm.downloadOpertationQueue[chapterDetail.offset].chapterdata.state = .downloaded
lock.unlock()
NotificationCenter.default.post(name: NSNotification.Name(("DownloadChapter")), object: self, userInfo: ["progress": 1.0, "notifIdentifier":(chapterDetail.element.cellForIndexpath)])
}
else {
gbm.downloadOpertationQueue[chapterDetail.offset].chapterdata.state = .non
lock.unlock()
NotificationCenter.default.post(name: NSNotification.Name(("DownloadChapter")), object: self, userInfo: ["progress": -1.0, "notifIdentifier":(chapterDetail.element.cellForIndexpath)])
}
}
else {
_ = WC_SQLite.shared.updateChapterState(courseId: chapterDetail.element.courseId, chapterId: chapterDetail.element.chapterdata.id, state: .non)
gbm.downloadOpertationQueue[chapterDetail.offset].chapterdata.state = .non
lock.unlock()
NotificationCenter.default.post(name: NSNotification.Name(("DownloadChapter")), object: self, userInfo: ["progress": -1.0, "notifIdentifier":(chapterDetail.element.cellForIndexpath)])
}
})
}
}
}
Again, this is asynchronous because you don't want the user waiting forever to download 100 pages..
For tasks like these the first thing you need to do is to do them asynchronously using dispatch_async, so that they are on a different thread and don't impact the performance of the application (or freeze it.)
Whenever your downloads succeeds/fails, you can always control what happens next in it's completion block. (I suggest you use recursion for what you're trying to achieve as it suits your needs).
Hope this helps!
I'm developing an iOS download app, that uses Alamofire 4.4 and Swift 3.0.
The app gets a url and downloads the file using Alamofire.
I'm using the background session manager so that the app can download whilst in the background and send a notification when complete.
For some reason, after adding this, the .failure response is never triggered even when putting airplane mode on.
If it turn airplane mode back off, the download weirdly continues going, but leave it long enough and it doesn't continue downloading, so you'd imagine it would have triggered the failure...
I have this code for the download request:
if let resumeData = resumeData {
request = BackendAPIManager.sharedInstance.alamoFireManager.download(resumingWith: resumeData, to: destination)
}
else {
request = BackendAPIManager.sharedInstance.alamoFireManager.download(extUrl, to: destination)
}
alamoRequest = request
.responseData { response in
switch response.result {
case .success:
//get file path of downloaded file
let filePath = response.destinationURL?.path
completeDownload(filePath: filePath!)
case .failure:
//this is what never calls on network drop out
print("Failed")
}
.downloadProgress { progress in
let progressPercent = Float(progress.fractionCompleted)
//handle other progress stuff
}
}
.success case triggers fine, and progress returns fine.
If I cancel a download with alamoRequest?.cancel() then it does trigger the .failure case.
Here's my completion handler:
class BackendAPIManager: NSObject {
static let sharedInstance = BackendAPIManager()
var alamoFireManager : Alamofire.SessionManager!
var backgroundCompletionHandler: (() -> Void)? {
get {
return alamoFireManager?.backgroundCompletionHandler
}
set {
alamoFireManager?.backgroundCompletionHandler = newValue
}
}
fileprivate override init()
{
let configuration = URLSessionConfiguration.background(withIdentifier: "com.uniqudeidexample.background")
self.alamoFireManager = Alamofire.SessionManager(configuration: configuration)
}
}
And in my AppDelegate:
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: #escaping () -> Void) {
BackendAPIManager.sharedInstance.backgroundCompletionHandler = completionHandler
//do other notification stuff...
}
Before I added the background session stuff it worked ok, and when I activated airplane mode it failed. So am I missing something?
Hope that all makes sense, it's my first app, so not clued up on all the technicalities,
Thanks
I am not sure how to handle this situation as I am very new to iOS development and Swift. I am performing data fetching like so:
func application(application: UIApplication!, performFetchWithCompletionHandler completionHandler: ((UIBackgroundFetchResult) -> Void)!)
{
loadShows()
completionHandler(UIBackgroundFetchResult.NewData)
println("Background Fetch Complete")
}
My loadShows() function parses a bunch of data it gets from a website loaded into a UIWebView. The problem is that I have a timer that waits for 10 seconds or so in the loadShows function. This allows for the javascript in the page to fully load before I start parsing the data. My problem is that the completion handler completes before my loadShows() does.
What I would like to do is add a bool for "isCompletedParsingShows" and make the completionHandler line wait to complete until that bool is true. What is the best way to handle this?
you have to pass your async function the handler to call later on:
func application(application: UIApplication!, performFetchWithCompletionHandler completionHandler: ((UIBackgroundFetchResult) -> Void)!) {
loadShows(completionHandler)
}
func loadShows(completionHandler: ((UIBackgroundFetchResult) -> Void)!) {
//....
//DO IT
//....
completionHandler(UIBackgroundFetchResult.NewData)
println("Background Fetch Complete")
}
OR (cleaner way IMHO)
add an intermediate completionHandler
func application(application: UIApplication!, performFetchWithCompletionHandler completionHandler: ((UIBackgroundFetchResult) -> Void)!) {
loadShows() {
completionHandler(UIBackgroundFetchResult.NewData)
println("Background Fetch Complete")
}
}
func loadShows(completionHandler: (() -> Void)!) {
//....
//DO IT
//....
completionHandler()
}
two ways to solve this, both use Grand Central Dispatch (which is similar in Swift and Objective C):
change loadShows method to make it synchronous and use the same dispatch queue as completionHandler, then wrap the entire body of the method in a dispatch_async ; this way the method call ends right away, but the completionHandler will be called after loadShows if finished, just like in a synchronous program
use a GCD semaphore - just like the BOOL you mention, but created with dispatch_semaphore_create ; you call dispatch_semaphore_wait before completionHandler to make it wait for the semaphore to be unlocked (unlock it with dispatch_semaphore_signal ) ; remember to place your method body inside a dispatch_async call in order not to have it block the rest of the app while waiting for loadShows to complete.
Details
xCode 9.2, Swift 4
Solution
class AsyncOperation {
private let semaphore: DispatchSemaphore
private let dispatchQueue: DispatchQueue
typealias CompleteClosure = ()->()
init(numberOfSimultaneousActions: Int, dispatchQueueLabel: String) {
semaphore = DispatchSemaphore(value: numberOfSimultaneousActions)
dispatchQueue = DispatchQueue(label: dispatchQueueLabel)
}
func run(closure: #escaping (#escaping CompleteClosure)->()) {
dispatchQueue.async {
self.semaphore.wait()
closure {
self.semaphore.signal()
}
}
}
}
Usage
let asyncOperation = AsyncOperation(numberOfSimultaneousActions: 1, dispatchQueueLabel: "AnyString")
asyncOperation.run { completeClosure in
// sync/async action
// ...
// action complete
completeClosure()
}
Full sample
import UIKit
class ViewController: UIViewController {
let asyncOperation = AsyncOperation(numberOfSimultaneousActions: 1, dispatchQueueLabel: "AnyString")
var counter = 1
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton(frame: CGRect(x: 50, y: 50, width: 100, height: 40))
button.setTitle("Button", for: .normal)
button.setTitleColor(.blue, for: .normal)
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
view.addSubview(button)
}
#objc func buttonTapped() {
print("Button tapped at: \(Date())")
asyncOperation.run { completeClosure in
let counter = self.counter
print(" - Action \(counter) strat at \(Date())")
self.counter += 1
DispatchQueue.global(qos: .background).async {
sleep(1)
print(" - Action \(counter) end at \(Date())")
completeClosure()
}
}
}
}
Results