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
Related
I need to update orders from my app while app is in background.
Ok, I am using OneSignal, I can get message on didReceiveRemoteNotification and inside it, I call Alamofire to check on my api what I need to update.
The problem is when the code get to the point: Alamofire.request(url).responseJSON {(response) in it doesnt go inside, just when I open the app I can get the result.
I would like it to get the new data on background and notify users after updating, so they can click on the notification to see whats is new.
I read that Alamofire runs on a background thread by default, but the network request goes on Main thread.
So, I tried: this and this, both don't work.
I tried URLSessionConfiguration but I got Error code -999 cancelled.
So, I added sessionManager.session.finishTasksAndInvalidate() in the end of my response. The error stops, but the code still don't go inside Alamofire request.
Some of my code - didReceiveRemoteNotification on my AppDelegate:
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
if let custom = userInfo["custom"] as? NSDictionary {
if let a = custom["a"] as? NSDictionary {
if let update = a["update"] as? NSString {
if update.isEqual(to: "Pedido") {
let strdataPedido = PedidoDAO().getMostRecentDtPedido()
if self.defaults.getDownloadInicialPedido(){
if strdataPedido != "" {
//let task = self.beginBackgroundTask()
self.loadOrdersFromLastSyncByApi(strdataPedido)
//self.endBackgroundTask(task)
}
}
}
}
}
}
loadOrdersFromLastSyncByApi function on my AppDelegate:
func loadOrdersFromLastSyncByApi(_ lastSync: String) {
let parceiroId = defaults.getParceiroId()
PedidoAPI().loadOrdersForLastSync(parceiroId, lastSync){ (dados) in
if let dadosPedidoModel = dados as? [PedidoModel] {
//do what needs to do to save new data
}
PedidoAPI().loadOrdersForLastSync function:
func loadOrdersForLastSync(_ parceiroId: String, _ lastSync: String, _ onComplete: #escaping(Any?) -> Void) {
let url = Test.basePath + "/api/orders/parceiro/\(parceiroId)/\(lastSync)"
//let queue = DispatchQueue(label: "com.test.br", qos: .background, attributes: .concurrent)
let task = self.beginBackgroundTask()
let queue = DispatchQueue.global(qos: .background)
queue.async {
Alamofire.request(url).responseJSON {(response) in
//This result its fired just when I open my app, I would like it to make everything on background
switch (response.result) {
//do what needs to send new data
}
self.endBackgroundTask(task)
Any help please?
Thanks for your question. So to clarify, I understood your question as the following:
You want to update users when there has been a change in their orders while the app is in background.
Follow-up Question:
Is there a reason you want to make the request from the client? Also, what data is coming in from OneSignal that you couldn't just handle on your server?
Answer:
You should handle any requests to a third party (Alamofire) on the server and then use our API to send a notification to the user with new info from the request response. I think that would be the best approach.
I was wanting to know if anyone knows how to detect if there is data transfer or not within an iOS app. I am using a Swift Reachability library to detect if there is wifi or cellular connection, but this does not let me know if there is actual data being transferred.
My app starts out with a WKWebView, and in the case there is no data being transferred to load the webview that takes up the entire screen, I would like to present an alertController to the user.
Reachability lets me know if there is a connection to wifi or a cellular network, but they don't help with letting me know if there is any data being transferred. I'm testing with my wifi on, but with no network connection, and I'm unable to present any alertController as connection is always passing true for isReachable().
Does anyone know how to go about this? Thank you.
Alamofire Provide the functionaly to check the network status. So here it is in detail with some other functionality.
I have created an APIClient Manager class in my project.
import UIKit
import Alamofire
import AlamofireNetworkActivityIndicator
// Completion handeler
typealias TSAPIClientCompletionBlock = (response: NSHTTPURLResponse?, result: AnyObject?, error: NSError?) -> Void
// MARK: -
class TSAPIClient: Manager {
// MARK: - Properties methods
private let manager = NetworkReachabilityManager(host: "www.apple.com") // could be any website, Just to check connectivity
private var serviceURL: NSURL?
// MARK: - init & deinit methods
init(baseURL: NSURL,
configuration: NSURLSessionConfiguration = NSURLSessionConfiguration.defaultSessionConfiguration(),
delegate: SessionDelegate = SessionDelegate(),
serverTrustPolicyManager: ServerTrustPolicyManager? = nil) {
super.init(configuration: configuration, delegate: delegate, serverTrustPolicyManager: serverTrustPolicyManager)
var aURL = baseURL
// Ensure terminal slash for baseURL path, so that NSURL relativeToURL works as expected
if aURL.path?.characters.count > 0 && !aURL.absoluteString.hasSuffix("/") {
aURL = baseURL.URLByAppendingPathComponent("")
}
serviceURL = baseURL
NetworkActivityIndicatorManager.sharedManager.isEnabled = true
observeNetworkStatus()
}
// MARK: - Public methods
func serverRequest(methodName: String, parameters: [String : AnyObject]? , isPostRequest: Bool, headers: [String : String]?, completionBlock: TSAPIClientCompletionBlock) -> Request {
let url = NSURL(string: methodName, relativeToURL: serviceURL)
print("\(url)")
let request = self.request(isPostRequest ? .POST : .GET, url!, parameters: parameters, encoding: isPostRequest ? .JSON : .URL, headers: headers)
.validate(statusCode: 200..<600)
.responseJSON { response in
switch response.result {
case .Success:
completionBlock(response: response.response, result: response.result.value, error: nil
)
break
case .Failure(let error):
completionBlock(response: response.response, result: nil, error: error)
break
}
}
return request
}
func cancelAllRequests() {
session.getAllTasksWithCompletionHandler { tasks in
for task in tasks {
task.cancel()
}
}
}
// this will contiously observe network changes
func observeNetworkStatus() {
manager?.listener = { status in
print("Network Status Changed: \(status)")
if status == .NotReachable {
} else if status == .Unknown {
} else {
// status is reachable
// posting a notification for network connectivity NSNotificationCenter.defaultCenter().postNotificationName(kNetworkStatusConnectedNotification, object: nil)
}
}
manager?.startListening()
}
// If you want to check network manually for Example before pushing a viewController.
func isNetworkReachable() -> Bool {
return manager?.isReachable ?? false
}
}
kNetworkStatusConnectedNotification is a constant declared as following
let kNetworkStatusConnectedNotification = "kNetworkStatusConnectedNotification"
Now, Where you want to observe the network changes in your application, register the notification in that viewController as follows
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(HomeViewController.applicationIsConnectedToNetwok), name: kNetworkStatusConnectedNotification, object: nil)
and in the selector do whatever you want to do when you get network connectivity
func applicationIsConnectedToNetwok() {
// e.g LoadData
}
I have written this method in my Session class and I am using it to check connectivity mannualy
func isNetworkReachable() -> Bool {
return YourAPIClient.sharedClient.isNetworkReachable()
}
I have added some comments in the code for your understanding. I hope it helps.
I'm having some trouble with an HTTP request performed from our iOS app to our API. The problem with this request is that it usually takes 30-40s to complete. I don't need to handle the response for now, so I just need to fire it and forget about it. I don't know if the problem is in my code or in the server, that's why I'm asking here.
I'm using Alamofire and Swift 2.2 and all the other requests are working just fine. This is an screenshot from Charles proxy while I was trying to debug it: Charles screenshot
As you can see, the request that blocks the others is the refreshchannels. When that request fires (#6 and #25), the others are blocked and don't finish until the refreshchannels finishes.
Here is the code that triggers that request and also the APIManager that i've built on top of Alamofire:
// This is the method that gets called when the user enables the notifications in the AppDelegate class
func application(application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: NSData) {
// Recieve the APNSToken and handle it. I've removed it to make it shorter
// This sends a POST to our API to store some data
APIManager().registerForPushNotifications(parametersPush) { (result) in
switch result {
case .Success (let JSON):
// This is the slow call that blocks the other HTTP requests
APIManager().refreshChannels { _ in } // I don't need to handle the response for now
case .Failure: break
}
}
}
And the manager:
//This is my custom manager to handle all the networking inside my app
class APIManager {
typealias CompletionHandlerType = (Result) -> Void
enum Result {
case Success(AnyObject?)
case Failure(NSError)
}
let API_HEADERS = Helper.sharedInstance.getApiHeaders()
let API_DOMAIN = Helper.sharedInstance.getAPIDomain()
//MARK: Default response to a request
func defaultBehaviourForRequestResponse(response: Response<AnyObject, NSError>, completion: CompletionHandlerType) {
print("Time for the request \(response.request!.URL!): \(response.timeline.totalDuration) seconds.")
switch response.result {
case .Success (let JSON):
if let _ = JSON["error"]! {
let error = NSError(domain: "APIError", code: response.response!.statusCode, userInfo: JSON as? [NSObject : AnyObject])
completion(Result.Failure(error))
} else {
completion(Result.Success(JSON))
}
case .Failure (let error):
completion(Result.Failure(error))
}
}
func refreshChannels(completion: CompletionHandlerType) {
Alamofire.request(.PUT, "\(API_DOMAIN)v1/user/refreshchannels", headers: API_HEADERS).responseJSON { response in
self.defaultBehaviourForRequestResponse(response, completion: completion)
}
}
}
Any help will be appreciated. Have a nice day!
In my iOS app users complete transactions which I need to post back to the server. I've created a function to do this:
static let configurationParam = NSURLSessionConfiguration.defaultSessionConfiguration()
static var manager = Alamofire.Manager(configuration: configurationParam)
func postItemToServer(itemToPost:DemoItem) {
let webServiceCallUrl = "..."
var itemApiModel:[String: AnyObject] = [
"ItemId": 123,
"ItemName": itemToPost.Name!,
//...
]
ApiManager.manager.request(.POST, webServiceCallUrl, parameters: itemApiModel, encoding: .JSON)
.validate()
.responseJSON { response in
switch response.result {
case .Success:
print("post success")
case .Failure:
print("SERVER RESPONSE: \(response.response?.statusCode)")
}
}
}
Currently I call this once a transaction is complete:
//...
if(transactionCompleted!) {
let apiManager = ApiManager()
apiManager.postItemToServer(self.item)
self.senderViewController!.performSegueWithIdentifier("TransactionCompletedSegue", sender: self)
}
//...
Where DemoItem is a CoreData object.
This all works as expected. However I need the ability to retry the POST request if it fails. For example if the network connection is down at the point of trying post to the server I need to automatically post the data once it becomes active again - at which point there may be several DemoItem's which need to be synced.
I'm new to Swift. In a similar Xamarin app I had a status column in my SQLite database which I set to 'AwaitingSync'. I then had an async timer that ran every 30 seconds, queried the DB for any items which had status='AwaitingSync' and then tried to post them if they existed. If it succeed it updated the status in the DB. I could implement something along the same lines here - but I was never really happy with that implementation as I had a DB query every 30 seconds even if nothing had changed.
Finally, it needs to be still work if the app is terminated. For example any items which weren't synced before the app is killed should sync once the app is resumed. What's the best way to approach this?
Edit
Based on Tom's answer I've created the following:
class SyncHelper {
let serialQueue = dispatch_queue_create("com.mycompany.syncqueue", DISPATCH_QUEUE_SERIAL)
let managedContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
func StartSync() {
//Run on serial queue so it can't be called twice at once
dispatch_async(serialQueue, {
//See if there are any items pending to sync
if let itemsToSync = self.GetItemsToSync() {
//Sync all pending items
for itemToSync in itemsToSync {
self.SyncItemToServer(itemToSync)
}
}
})
}
private func GetItemsToSync() -> [DemoItem]? {
var result:[DemoItem]?
do {
let fetchRequest = NSFetchRequest(entityName: "DemoItem")
fetchRequest.predicate = NSPredicate(format: "awaitingSync = true", argumentArray: nil)
result = try managedContext.executeFetchRequest(fetchRequest) as? [DemoItem]
} catch {
//Handle error...
}
return result
}
private func SyncItemToServer(itemToSync:DemoItem) {
let apiManager = ApiManager()
//Try to post to the server
apiManager.postItemToServer(itemToSync:DemoItem, completionHandler: { (error) -> Void in
if let _ = error {
//An error has occurred - nothing need to happen as it will be picked up when the network is restored
print("Sync failed")
} else {
print("Sync success")
itemToSync.awaitingSync = false
do {
try self.managedContext.save()
} catch {
//Handle error...
}
}
})
}
}
I then call this when ever a transaction is completed:
//...
if(transactionCompleted!) {
let syncHelper = SyncHelper()
syncHelper.StartSync()
}
//...
And then finally I've used Reachability.swift to start the sync every time the network connection resumes:
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var reachability:Reachability?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
//...
//Setup the sync for when the network connection resumes
do {
reachability = try Reachability.reachabilityForInternetConnection()
NSNotificationCenter.defaultCenter().addObserver(self,
selector: "reachabilityChanged:",
name: ReachabilityChangedNotification,
object: reachability)
try reachability!.startNotifier()
} catch {
print("Unable to create Reachability")
}
return true
}
func reachabilityChanged(note: NSNotification) {
let reachability = note.object as! Reachability
if reachability.isReachable() {
print("Network reachable")
let syncHelper = SyncHelper()
syncHelper.StartSync()
} else {
print("Not reachable")
}
}
}
This all seems to be working. Is this approach ok and have I missed anything which would improve it? The only gap I can see is if the network connectivity is active however the server throws an error for some reason - I guess I could then add a button for the user to retry any pending items.
Firstly, if your concern is whether the network connection is working, you shouldn't be polling at intervals. You should be using iOS's network reachability API to get notified when the network status changes. Apple provides a simple implementation of this and there are numerous alternative implementations online.
Since a sync status value should be a boolean flag, it's not as if a fetch request is a heavy-duty operation, especially if you use reachability. Not only should the fetch request be fast, you can update the flag after the fact in a single step-- use NSBatchUpdateRequest to set the flag to false on every instance you just sent to the server.
If you want to get the sync status out of the persistent store (not a bad idea since it's metadata), you'll need to maintain your own list of unsynced objects. The best way to do this is by tracking the objectID of the managed objects awaiting sync. That would be something like:
Get the objectID of a newly changed managed object
Convert that to an NSURL using NSManagedObjectID's URIRepresentation() method.
Put the NSURL on a list that you save somewhere, so it'll persist.
You can save the list in a file, in user defaults, or in the persistent store's own metadata.
When it's time to sync, you'd do something like:
Get an NSURL from your list
Convert that into an NSManagedObjectID using managedObjectIDForURIRepresentation(url:NSURL) (which is on NSPersistentStoreCoordinator)
Get the managed object for that ID objectWithID: on NSManagedObjectContext.
Sync that object's data.
Then on a successful sync, remove entries from the list.
I am using Alamofire to download data
How to make alamofire run download in background with swift?
Thanks
The basic idea is as follows:
The key problem is that with background downloads, your app may actually be terminated while downloads are in progress (e.g. jettisoned due to memory pressure). Fortunately, your app is fired up again when background downloads are done, but any task-level closures you originally supplied are long gone. To get around this, when using background sessions, one should rely upon session-level closures used by the delegate methods.
import UIKit
import Alamofire
import UserNotifications
fileprivate let backgroundIdentifier = ...
fileprivate let notificationIdentifier = ...
final class BackgroundSession {
/// Shared singleton instance of BackgroundSession
static let shared = BackgroundSession()
/// AlamoFire `SessionManager`
///
/// This is `private` to keep this app loosely coupled with Alamofire.
private let manager: SessionManager
/// Save background completion handler, supplied by app delegate
func saveBackgroundCompletionHandler(_ backgroundCompletionHandler: #escaping () -> Void) {
manager.backgroundCompletionHandler = backgroundCompletionHandler
}
/// Initialize background session
///
/// This is `private` to avoid accidentally instantiating separate instance of this singleton object.
private init() {
let configuration = URLSessionConfiguration.background(withIdentifier: backgroundIdentifier)
manager = SessionManager(configuration: configuration)
// specify what to do when download is done
manager.delegate.downloadTaskDidFinishDownloadingToURL = { _, task, location in
do {
let destination = try FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
.appendingPathComponent(task.originalRequest!.url!.lastPathComponent)
try FileManager.default.moveItem(at: location, to: destination)
} catch {
print("\(error)")
}
}
// specify what to do when background session finishes; i.e. make sure to call saved completion handler
// if you don't implement this, it will call the saved `backgroundCompletionHandler` for you
manager.delegate.sessionDidFinishEventsForBackgroundURLSession = { [weak self] _ in
self?.manager.backgroundCompletionHandler?()
self?.manager.backgroundCompletionHandler = nil
// if you want, tell the user that the downloads are done
let content = UNMutableNotificationContent()
content.title = "All downloads done"
content.body = "Whoo, hoo!"
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
let notification = UNNotificationRequest(identifier: notificationIdentifier, content: content, trigger: trigger)
UNUserNotificationCenter.current().add(notification)
}
// specify what to do upon error
manager.delegate.taskDidComplete = { _, task, error in
let filename = task.originalRequest!.url!.lastPathComponent
if let error = error {
print("\(filename) error: \(error)")
} else {
print("\(filename) done!")
}
// I might want to post some event to `NotificationCenter`
// so app UI can be updated, if it's in foreground
}
}
func download(_ url: URL) {
manager.download(url)
}
}
Then I can just initiate those downloads. Note, I do not specify any task-specific closure when I initiate the download, but rather merely use the above session-level closures that use the details of the URLSessionTask to identify what to do:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// request permission to post notification if download finishes while this is running in background
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { granted, error in
if let error = error, !granted {
print("\(error)")
}
}
}
#IBAction func didTapButton(_ sender: Any) {
let urlStrings = [
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/s72-55482.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo10/hires/as10-34-5162.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo-soyuz/apollo-soyuz/hires/s75-33375.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/as17-134-20380.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/as17-140-21497.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/as17-148-22727.jpg"
]
let urls = urlStrings.flatMap { URL(string: $0) }
for url in urls {
BackgroundSession.shared.download(url)
}
}
}
If your app isn't running when the downloads finish, iOS needs to know that, after it restarted your app, when you're all done and that it can safely suspend your app. So, in handleEventsForBackgroundURLSession you capture that closure:
class AppDelegate: UIResponder, UIApplicationDelegate {
...
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: #escaping () -> Void) {
BackgroundSession.shared.saveBackgroundCompletionHandler(completionHandler)
}
}
That is used by sessionDidFinishEventsForBackgroundURLSession, in step 1.
Two observations:
This is only called if your app was not running when the downloads finish.
If doing background sessions, though, you must capture this closure and call it when you're all done processing the background session delegate methods.
So, to recap, the basic limitations of background sessions are:
You can only use download and upload tasks while the app is in background;
You can only rely upon session-level delegates because the app may have been terminated since the requests were initiated; and
In iOS, you must implement handleEventsForBackgroundURLSession, capture that completion handler, and call it when your background process is done.
I must also point out that while Alamofire is a wonderful library, it's not actually adding a lot value (above and beyond what is provided by URLSession to this background download process). If you're doing simple uploads/downloads only, then you might consider just using URLSession directly. But if you are using Alamofire in your project already or if your requests consist of more complicated application/x-www-form-urlencoded requests (or whatever) which merit the advantages of Alamofire, then the above outlines the key moving parts involved in the process.