I have an application that requires downloading large amount of data when the user logs in. I wanted to move the download portion of it to a background thread so the user can navigate the app without having to wait for the download to complete. I have tried the following methods but some of them still locks the app so user cant click on anything,
dispatch_async(dispatch_get_main_queue(), ^{
});
Have also tried
[self performSelectorInBackground:#selector(loadDataThatToBeFetchedInThread:)
withObject:objectArrayThatNeedToFetchData];
this one seems to just stop if I move between activity. Have tried moving it to the AppDelegate method but when I try to save to SQlite DB i get some error. Am i doing something wrong? Can some one please help.
Thanks in advance
Well, dispatch_get_main_queue() is going to give you the main thread, so that's probably not what you want.
Instead, you should obtain a background queue using:
dispatch_async (dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ ... });
And then, it's customary to either send out some notification, or even call back to the main thread directly to (in the UI) report success:
dispatch_async (dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
// Do the download...
// Download finishes...
dispatch_async(dispatch_get_main_queue(), ^{
// Call a UI-updating method, or similar
});
});
Look up NSURLSession and NSURLSessionDownloadTask. This is the latest and greatest from Apple.
Watch the Core Networking videos (What's New in Core Networking) from the 2015 WWDC videos and 2014 WWDC videos.
URL Session Programming Guide is also a good resource.
NSURLSession is asynchronous out of the box — which is what you're looking for.
As a bonus NSURLSessionDownloadTask makes it easy to continue the download when you app changes to background state (which is much different than a background thread). It also allows you to easily cancel and/or resume a download.
I'd recommend using NSOperation and NSOperationQueue to keep it nice and clean.
Read & Watch more:
NSOperation reference
NSOperationQueue reference
Advanced NSOperations by Dave DeLong, WWDC 2015
Here's a basic setup that you can customise to fit your needs
Disclaimer: although it seems like a lot, it makes up for a nicer API.
First, let's define an interface to handle our API endpoints:
// Endpoints.swift
let api_base = "https://myserver.com/"
let api_path = "api/"
protocol EndpointGenerator {
func URL() -> NSURL
}
extension EndpointGenerator {
func URL() -> NSURL {
return NSURL(string: api_base)!
}
}
// Represents a null endpoint. It will fail.
struct NullEndpoint: EndpointGenerator { }
enum Endpoint: String, EndpointGenerator {
case Login = "login"
case SignUp = "signup"
func URL() -> NSURL {
return NSURL(string: api_base + api_path + self.rawValue)!
}
}
Next, let's build our custom NSOperation:
// Operation.swift
public class Operation: NSOperation {
public typealias Completion = Operation -> ()
public typealias Error = NSError -> ()
var endpoint: EndpointGenerator {
return NullEndpoint()
}
var headerParams: [String:String]? {
return nil
}
var requestBody: [String:AnyObject]? {
return nil
}
var method: HTTPMethod {
return .GET
}
var networkTask: NSURLSessionTask?
var completion: Completion?
var error: Error?
public var parsedObject = [String:AnyObject]()
override public init() { }
public init(completion: Completion, error: Error) {
self.completion = completion
self.error = error
}
override public func start() {
NSURLSessionImplementaion.execute(self)
}
override public func cancel() {
networkTask?.cancel()
networkTask = nil
}
}
To be almost done, let's handle the actual queue:
// OperationQueue.swift
public class OperationQueue: NSOperationQueue {
public static let internalQueue = OperationQueue()
public static func addOperation(operation: NSOperation) {
internalQueue.addOperation(operation)
}
public static func addOperations(operations: NSOperation...) {
for operation in operations {
addOperation(operation)
}
}
public static func cancellAllOperations() {
internalQueue.cancelAllOperations()
}
}
Finally, the download part:
// NSURLSessionImplementation.swift
enum HTTPMethod: String {
case POST = "POST"
case GET = "GET"
case PATCH = "PATCH"
}
public let OSNetworkingErrorDomain = "com.swanros.errordomain"
class NSURLSessionImplementaion {
class func execute(operation: Operation) {
let session = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration())
let request = NSMutableURLRequest(URL: operation.endpoint.URL())
if let headerParams = operation.headerParams {
for element in headerParams {
request.setValue(element.1, forHTTPHeaderField: element.0)
}
}
if let body = operation.requestBody {
do {
request.HTTPBody = try NSJSONSerialization.dataWithJSONObject(body, options: .PrettyPrinted)
} catch {
return
}
}
request.HTTPMethod = operation.method.rawValue
let task = session.dataTaskWithRequest(request) { data, response, error in
if let e = error {
operation.error?(e)
return
}
guard let d = data else {
operation.error?(errorWithDescription("No data"))
return
}
do {
let json = try NSJSONSerialization.JSONObjectWithData(d, options: .MutableLeaves) as? [String:AnyObject]
guard let j = json else {
operation.error?(errorWithDescription("Error parsing JSON."))
return
}
if let errorMessage = string(j, key: "error") {
operation.error?(errorWithDescription(errorMessage))
return
}
operation.parsedObject = j
operation.completion?(operation)
} catch let jsonError as NSError {
operation.error?(jsonError)
}
}
operation.networkTask = task
task.resume()
}
}
func errorWithDescription(desc: String) -> NSError {
return NSError(domain: OSNetworkingErrorDomain, code: 0, userInfo: [NSLocalizedDescriptionKey:desc])
}
How do you implement this? Say you want to hit the /login endpoint. Subclass Operation as follows:
// LogInOperation.swift
public class LogInOperation: Operation {
override var endpoint: EndpointGenerator {
// A nice way to represent endpoints: use enums and protocols!
return Endpoint.Login
}
// The headers for this particular request. Maybe you need a token here!
override var headerParams: [String:String]? {
return [
"Content-Type": "application/json",
"Application-Id": "bAAvLosWNeSTHrlYilysdeEYoJHUXs88"
]
}
// The HTTP request body!
override var requestBody: [String:AnyObject]? {
return [
"mail": mail,
"password": password
]
}
// .GET is default
override var method: HTTPMethod {
return .POST
}
private var mail: String
private var password: String
public init(mail m: String, password p: String, completion: Completion, error: Error) {
mail = m
password = p
super.init(completion: completion, error: error)
}
}
And you use it like this:
// ViewController.swift
let loginOperation = LogInOperation(
mail: "mail#example.com",
password: "123123",
completion: { op in
// parsedObject would be the user's info
print(op.parsedObject?)
}, error: { error in
print(error.localizedDescription)
}
)
OperationQueue.addOperation(loginOperation)
Related
I am trying to save "author" data to global variable named "authors" from json(Link:"https://learnappmaking.com/ex/books.json") with these two libraries. But it only works at the trailing closure of func Alamofire.request(url).responseJSON. When I access the global variable named "authors" from somewhere except the trailing closure, what I get is an empty array of string.
Can someone explain the reason behind this werid situation?
Thanks a lot.
class ViewController: UIViewController {
var authors = [String]()
let url = "https://learnappmaking.com/ex/books.json"
func getAuthorsCount() {
print("the number of authors : \(authors.count)") // I hope that here, the number of authors should be 3 too! actually, it is 0. Why?
// this for loop doesn't get excuted
for author in authors {
print(author)
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
Alamofire.request(url).responseJSON { response in
if let data = response.data {
if let json = try? JSON(data: data) {
for item in json["books"].arrayValue {
var outputString: String
print(item["author"])
outputString = item["author"].stringValue
//urlOfProjectAsset.append(outputString)
self.authors.append(outputString)
print("authors.count: \(self.authors.count)")
}
}
}
}
getAuthorsCount()
print("-------------")
}
}
the actual output is:
Update:
I adjusted my code:
class ViewController: UIViewController {
var authors = [String]()
let url = "https://learnappmaking.com/ex/books.json"
func getAuthorsCount() {
print("the number of authors : \(authors.count)")
// this for loop doesn't get excuted
for author in authors {
print(author)
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
Alamofire.request(url).responseJSON { response in
if let data = response.data {
if let json = try? JSON(data: data) {
for item in json["books"].arrayValue {
var outputString: String
//print(item["author"])
outputString = item["author"].stringValue
//urlOfProjectAsset.append(outputString)
self.authors.append(outputString)
//print("authors.count: \(self.authors.count)")
}
self.getAuthorsCount() // I added this line of code.
}
}
}
getAuthorsCount()
print("-------------")
}
}
But why does the func getAuthorsCount() (not self. version) still print an empty array of strings ? I think the result should be the same as the result which
func self.getAuthorsCount() printed.
I am so confused now...
Again, I want to use the data kept in the variable named "authors", but what I only got is an empty array of strings.
I'll try to answer all your questions :
The data is persistant
You are doing the following : Alamo.request (Network call) -> getAuthors(print result - empty) ->
response (receive response) -> self.authors.append(save response) -> self.authors (print result)
You need to do : Alamo.request (Network call) -> response (receive response) -> self.authors.append(save response) -> self.getAuthors or getAuthors(same) (inside the response {})
You need to call getAuthors once you have your result, inside the response callback :
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
Alamofire.request(url).responseJSON { response in
if let data = response.data {
if let json = try? JSON(data: data) {
for item in json["books"].arrayValue {
var outputString: String
print(item["author"])
outputString = item["author"].stringValue
//urlOfProjectAsset.append(outputString)
self.authors.append(outputString)
print("authors.count: \(self.authors.count)")
}
self.getAuthorsCount()
print("-------------")
//Do whatever you want from here : present/push
}
}
}
Then you can use the saved data :
To send the data to another ViewController you can use various methods (present/push, closure/callback, ...)
Usually you will have a loading spinner to wait for the network to
answer then you will show your next controller
As requested via direct message: a Swift-only approach. Just paste this in a blank Playground:
import Foundation
final class NetworkService {
enum ServiceError: LocalizedError {
case invalidUrl
case networkingError(error: Error)
case parsingError
var localizedDescription: String? { return String(describing: self) }
}
func request(completion: #escaping (Result<[UserObject], Error>) -> Void ) {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/users") else {
completion(.failure(ServiceError.invalidUrl))
return
}
let dataTask = URLSession.shared.dataTask(with: url) { (jsonData, response, error) in
if let jsonData = jsonData {
let jsonDecoder = JSONDecoder()
do {
let users = try jsonDecoder.decode([UserObject].self, from: jsonData)
completion(.success(users))
} catch {
completion(.failure(ServiceError.parsingError))
}
} else if let error = error {
completion(.failure(ServiceError.networkingError(error: error)))
}
}
dataTask.resume()
}
}
struct UserObject: Codable {
let id: Int
let name: String
let username: String
let email: String?
let website: String?
}
let networkService = NetworkService()
networkService.request { result in
switch result {
case .success(let users):
debugPrint("Received \(users.count) users from REST API")
debugPrint(users)
case .failure(let error):
debugPrint(error.localizedDescription)
}
}
I'm trying to get some data from the server and use it globally in the app..
I mean for example, I'm using following code to get data from service:
struct Service : Decodable{
let id: Int
let name, description: String
let createdAt: String?
let updatedAt: String?
}
func makeGetCall() {
let todoEndpoint: String = "http://web.src01.view.beta.is.sa/public/api/services"
guard let url = URL(string: todoEndpoint) else {
print("Error: cannot create URL")
return
}
let urlRequest = URLRequest(url: url)
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
let task = session.dataTask(with: urlRequest) {
(data, response, error) in
guard error == nil else {
print("error calling GET on /public/api/services")
print(error!)
return
}
guard let responseData = data else {
print("Error: did not receive data")
return
}
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let todos = try decoder.decode([Service].self, from: responseData)
for todo in todos{
print(todo.name)
}
} catch {
print("error trying to convert data to JSON")
return
}
}
task.resume()
}
This code is located and called in HomeViewController and i'm getting data which i want.
But i want to access and use this data in another viewcontroller and in whole app...
How i can do it? How can i make the received data from the function is saved globally and how to use it in another viewcontroller?
Can someone tell me how i can do this?
For such cases we usually use static data. They may be served as singleton or just a static property. In your case a static property for cached data may be nice. We can put static properties in extension so adding following may be nice:
// MARK: - Fetching Data
extension Service {
private static var cachedServices: [Service]?
static func fetchServices(_ completion: (_ services: [Service]) -> Void?) {
if let cachedServices = cachedServices {
completion(cachedServices)
} else {
makeGetCall { services in
let newServices = services ?? []
self.cachedServices = newServices
completion(newServices)
}
}
}
}
Now the usage from everywhere is calling
Service.fetchServices { services in
}
and this call may be asynchronous or not, depending if data is already loaded.
If you need to access them synchronous and you are sure data is already loaded then simply add another method in extension:
static func getCachedData() -> [Service] {
return cachedServices ?? []
}
This method will return instantly but array will be empty if no data was received yet. But anywhere you can call Service.getCachedData()
This cache is now only preserved until your app terminates. If you want to preserve them longer then all you need to do is add the logic to save and load data into file or user defaults. The logic for that would be something like:
private static var cachedServices: [Service]? {
didSet {
self.saveServicesToFile(cachedServices)
}
}
static func fetchServices(_ completion: (_ services: [Service]) -> Void?)
{
if let cachedServices = cachedServices {
completion(cachedServices)
} else if let saved = self.loadFromFile() {
self.cachedServices = saved
completion(saved)
}else {
makeGetCall { services in
let newServices = services ?? []
self.cachedServices = newServices
completion(newServices)
}
}
}
I am using a Master Detail Application. Master Screen is a Dashboard and on selecting an item, moves to the detailed screen where I trigger an Alamofire request in the backend
Below is the snippet
class APIManager: NSObject {
class var sharedManager: APIManager {
return _sharedManager
}
private var requests = [Request]()
// Cancel any ongoing download
func cancelRequests() {
if requests.count > 0 {
for request in requests {
request.cancel()
}
}
}
func getData(completion: (dataSet: [Data]?, error: NSError?) -> Void) {
let request = Alamofire.request(.GET, "http://request")
.response { (request, response, data, error) in
dispatch_async(dispatch_get_main_queue(), {
if(error == nil) {
if let response = data, data = (try? NSJSONSerialization.JSONObjectWithData(response, options: [])) as? [NSDictionary] {
var dataSet = [Data]()
for (_, dictionary) in data.enumerate() {
let lat = dictionary["Latitude"]
let lng = dictionary["Longitude"]
let id = dictionary["ID"] as! Int
let data = Data(lat: lat!, long: lng!, id: shuttleID)
dataSet.append(data)
}
completion(dataSet: dataSet, error: nil)
}
} else { completion(dataSet: nil, error: error) }
})
}
requests.append(request)
}
}
I have a singleton API manager class and from the detail view controller I call getData() function. Everything works fine.
But, when I push and pop repeatedly, I see rapid increase in the memory and after 10-15 attempts, I get memory warning. However in the AppDelegate I am managing it to show an Alert message and adding a delay timer for 8 seconds. But however after 20-25 attempts app crashes due to memory warning.
In viewWillDisappear(), I cancel any ongoing requests also. But I couldn't able to stop memory warning issue. I commented the part where I call the request, I see no issues, even memory consumption is less.
I welcome ideas.
The problem is you are never removing the requests that you append to the member variable 'requests'.
You will need to ensure to remove the request when you either cancel it or when the request completes successfully.
Do the following modifications-
func cancelRequests() {
if requests.count > 0 {
for request in requests {
request.cancel()
}
}
requests.removeAll() //Delete all canseled requests
}
also
func getData(completion: (dataSet: [Data]?, error: NSError?) -> Void) {
let request = Alamofire.request(.GET, "http://request")
.response { (request, response, data, error) in
dispatch_async(dispatch_get_main_queue(), {
if(error == nil) {
if let response = data, data = (try? NSJSONSerialization.JSONObjectWithData(response, options: [])) as? [NSDictionary] {
var dataSet = [Data]()
for (_, dictionary) in data.enumerate() {
let lat = dictionary["Latitude"]
let lng = dictionary["Longitude"]
let id = dictionary["ID"] as! Int
let data = Data(lat: lat!, long: lng!, id: shuttleID)
dataSet.append(data)
}
requests.removeObject(request)
completion(dataSet: dataSet, error: nil)
}
} else {
requests.removeObject(request)
completion(dataSet: nil, error: error) }
})
}
requests.append(request)
}
Add this Handy extension on Array to remove item to your code:
// Swift 2 Array Extension
extension Array where Element: Equatable {
mutating func removeObject(object: Element) {
if let index = self.indexOf(object) {
self.removeAtIndex(index)
}
}
mutating func removeObjectsInArray(array: [Element]) {
for object in array {
self.removeObject(object)
}
}
}
On analysis, I found that the memory warning was not due to the Alamofire request. It was due to MKMapView. Loading a MKMapView, zooming in and zooming out consumes more memory. So, in viewWillDisappear I did the fix.
override func viewWillDisappear(animated:Bool){
super.viewWillDisappear(animated)
self.applyMapViewMemoryFix()
}
func applyMapViewMemoryFix(){
switch (self.mapView.mapType) {
case MKMapType.Hybrid:
self.mapView.mapType = MKMapType.Standard
break;
case MKMapType.Standard:
self.mapView.mapType = MKMapType.Hybrid
break;
default:
break;
}
self.mapView.showsUserLocation = false
self.mapView.delegate = nil
self.mapView.removeFromSuperview()
self.mapView = nil
}
Courtesy - Stop iOS 7 MKMapView from leaking memory
I'm having trouble retrieving data from my Alamofire request asynchronously.
class BookGetter {
static let instance = BookGetter()
func getBook(bookId: String) -> Book {
let rootUrl = "https://www.someusefulbookapi.com/bookid=?"
let url = rootUrl + bookId
var title = ""
Alamofire.request(.GET, url).response { response in
let jsonDict = JSON(data: response.2!)
title = String(jsonDict["items"][0]["volumeInfo"]["title"])
}
let book = Book(title: title)
print(book.title)
return book
}
}
The output of print(book.title) is "", and I understand this is because the print statement is running before the request returns.
How do I get the book instance to be returned only when it is instantiated with the data from the request?
The problem you have is that you are calling an asynchronous method and expecting to return the result synchronously. When your code is executed, the getBook function completes and returns before even the GET request has complete.
Basically, you have two options:
Update your getBook method to be asynchronous and return the result with a completion block/callback
Wait for the asynchronous call to complete, blocking the current thread (this is OK as long as it is not the main thread you are blocking), and return the result synchronously.
1. Update your method to be asynchronous
To do this, you must return the result on a block/callback function.
class BookGetter {
static let instance = BookGetter()
func getBook(bookId: String, complete: (book: Book?, error: NSError?) -> Void) {
let rootUrl = "https://www.someusefulbookapi.com/bookid=?"
let url = rootUrl + bookId
var title = ""
Alamofire.request(.GET, url).response { request, response, data, error in
// TODO: You should check for network errors here
// and notify the calling function and end-user properly.
if error != nil {
complete(book: nil, error: error as? NSError)
return
}
let jsonDict = JSON(data: response.2!)
title = String(jsonDict["items"][0]["volumeInfo"]["title"])
let book = Book(title: title)
print(book.title)
complete(book: book, error: nil)
}
}
}
As mentioned in the above code, ideally you should handle errors in the callback response (including exceptions while parsing the JSON). Once handled, you can update the callback parameters to (book: Book?, error: NSError?) -> Void or similar, and check for book or error to be set on the caller function.
To call the function, you need to pass a block to handle the response:
BookGetter.instance.getBook("bookID") { (book, error) in
if error != nil {
// Show UIAlertView with error message (localizedDescription)
return
}
// Update User Interface with the book details
}
2. Wait for the asynchronous call to complete
As mentioned above, this is a good idea only if you were running this code on a background thread. It is OK to block background threads, but it is never OK to block the main thread on a graphic application, as it will freeze the user interface. If you do not know what blocking means, please use the option #1.
class BookGetter {
static let instance = BookGetter()
func getBook(bookId: String) -> Book {
let rootUrl = "https://www.someusefulbookapi.com/bookid=?"
let url = rootUrl + bookId
var title = ""
let semaphore = dispatch_semaphore_create(0)
Alamofire.request(.GET, url).response { response in
let jsonDict = JSON(data: response.2!)
title = String(jsonDict["items"][0]["volumeInfo"]["title"])
dispatch_semaphore_signal(semaphore)
}
//Wait for the request to complete
while dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW) != 0 {
NSRunLoop.currentRunLoop().runMode(NSDefaultRunLoopMode, beforeDate: NSDate(timeIntervalSinceNow: 10))
}
let book = Book(title: title)
print(book.title)
return book
}
}
You can use closures and return a completionHandler with your book like in the following way:
func getBook(bookId: String, completionHandler: (book: Book?) -> ()) {
let rootUrl = "https://www.someusefulbookapi.com/bookid=?"
let url = rootUrl + bookId
Alamofire.request(.GET, url).response { response in completionHandler(
book:
{
// In this block you create the Book object or returns nil in case of error
if response == nil {
return nil
}
let jsonDict = JSON(data: response.2!)
let title = String(jsonDict["items"][0]["volumeInfo"]["title"])
let book = Book(title: title)
return book
}())
}
}
And then you can call it like in the following way:
getBook("idOfYourBook") { book in
if let book = book {
println(book.title)
}
}
I hope this help you.
I want to get the result of a webservice call as a block callback so I have added the method fetchOpportunities below to my Opportunity model class now I want to call this method from my UIViewController like that:
class HomeViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
Opportunity.fetchOpportunities(
success (data) {
}
)
}
}
I guess blocks is not present in Swift but how to replicate a similar behavior ? The call is asynchronous but I need to get the data when the call is completed in my TableViewController to update the TableViewand it can't work with my actual implementation in Objective-C I used to use blocks but what to do with Swift2
Here is the implementation of fetchOpportunities
class func fetchOpportunities() {
let urlPath = "http://www.MY_API_HERE.com"
guard let endpoint = NSURL(string: urlPath) else { print("Error creating endpoint");return }
let request = NSMutableURLRequest(URL:endpoint)
NSURLSession.sharedSession().dataTaskWithRequest(request) { (data, response, error) -> Void in
do {
guard let dat = data else { throw JSONError.NoData }
guard let json = try NSJSONSerialization.JSONObjectWithData(dat, options: []) as? NSDictionary else { throw JSONError.ConversionFailed }
var ops = [Opportunity]()
if let dataArray = json["data"] as? [[String:AnyObject]] {
for op in dataArray {
ops.append( Opportunity(op) )
}
}
print(ops)
} catch let error as JSONError {
print(error.rawValue)
} catch {
print(error)
}
}.resume()
}
Just use closure. The method declaration will be:
class func fetchOpportunities(callback: ([Opportunity]) -> ()) {
// Do all the work here and then send the data like :
callback(ops)
}
In UIViewController:
Opportunity.fetchOpportunities { (data) -> () in
print(data)
}
Efficient JSON in Swift with Functional Concepts and Generics
Learn NSURLSession using Swift Part 1