I am trying to login with macbook using code but I keep on getting this error:
Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value
// API to log an user in
func login(userType: String, completionHandler: #escaping (NSError?) -> Void) {
let path = "api/social/convert-token/"
let url = baseURL!.appendingPathComponent(path)
let params: [String: Any] = [
"grant_type": "convert_token",
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"backend": "facebook",
"token": FBSDKAccessToken.current().tokenString,
"user_type": userType
]
Alamofire.request(url!, method: .post, parameters: params, encoding: URLEncoding(), headers: nil).responseJSON { (response) in
switch response.result {
case .success(let value):
let jsonData = JSON(value)
self.accessToken = jsonData["access_token"].string!
self.refreshToken = jsonData["refresh_token"].string!
self.expired = Date().addingTimeInterval(TimeInterval(jsonData["expire_in"].int!))
completionHandler(nil)
break
case .failure(let error):
completionHandler(error as NSError?)
break
}
}
}
The error is referring to this line:
self.accessToken = jsonData["access_token"].string!
and this is LoginViewController code:
import UIKit
import FBSDKLoginKit
class LoginViewController: UIViewController {
#IBOutlet weak var bLogin: UIButton!
#IBOutlet weak var bLogout: UIButton!
var fbLoginSuccess = false
var userType: String = USERTYPE_CUSTOMER
override func viewDidLoad() {
super.viewDidLoad()
if (FBSDKAccessToken.current() != nil) {
bLogout.isHidden = false
FBManager.getFBUserData(completionHandler: {
self.bLogin.setTitle("Continue as \(User.currentUser.email!)", for: .normal)
// self.bLogin.sizeToFit()
})
}
}
override func viewDidAppear(_ animated: Bool) {
if (FBSDKAccessToken.current() != nil && fbLoginSuccess == true) {
performSegue(withIdentifier: "CustomerView", sender: self)
}
}
#IBAction func facebookLogout(_ sender: AnyObject) {
APIManager.shared.logout { (error) in
if error == nil {
FBManager.shared.logOut()
User.currentUser.resetInfo()
self.bLogout.isHidden = true
self.bLogin.setTitle("Login with Facebook", for: .normal)
}
}
}
#IBAction func facebookLogin(_ sender: AnyObject) {
if (FBSDKAccessToken.current() != nil) {
APIManager.shared.login(userType: userType, completionHandler: { (error) in
if error == nil {
self.fbLoginSuccess = true
self.viewDidAppear(true)
}
})
} else {
FBManager.shared.logIn(
withReadPermissions: ["public_profile", "email"],
from: self,
handler: { (result, error) in
if (error == nil) {
FBManager.getFBUserData(completionHandler: {
APIManager.shared.login(userType: self.userType, completionHandler: { (error) in
if error == nil {
self.fbLoginSuccess = true
self.viewDidAppear(true)
}
})
})
}
})
}
}
}
Swift 3
Xcode 9
iOS 10.2
I read several texts to find out the causes of this type of error but not succeed.
First of all, it is a bad practice to use force unwrap (!) everywhere. Try to avoid it, until you really need it and/or know what are you doing. Here you use SwiftyJSON. It helps you to extract data from JSON in a convenient way. But you shouldn't rely, that you will always get proper JSON from the backend. There are many reasons why it can return wrong JSON and there would not be needed value. There are two options:
You can use .stringValue instead of .string - it will return an empty string instead of nil
You can do in this way: if let token = jsonData["access_token"].string {...} or even use guard statement
Here is a good article to understand force unwrapping: https://blog.timac.org/2017/0628-swift-banning-force-unwrapping-optionals/
This error happens when you have a ! (force unwrap symbol), meaning that you're certain the data will be there; but in fact, the data isn't there - it's nil.
Try using a guard statement. For example:
guard let self.accessToken = jsonData["access_token"].string else {
// log error message, if desired; then exit the function
return
}
Alternatively, you could use an if let statement:
if let self.accessToken = jsonData["access_token"].string {
// do stuff if 'jsonData["access_token"].string' is valid
}
else {
// do other stuff if 'jsonData["access_token"].string' is nil
}
Now, why the data is not there (nil) - that's another question. Perhaps check your JSON function to ensure it's properly processing the JSON response. Also, check to make sure you're getting a valid response:
guard let statusCode = (response as? HTTPURLResponse)?.statusCode, statusCode == 200 else {
// handle a bad response code
return
}
// handle the good response code stuff after the guard statement
Learn about using if let to handle possible nil values in the Swift Programming Language (Swift 4.2) Guide under Optional Binding in the Basics section.
Learn about guard statements under Early Exit in the Control Flow section of the Guide, and also in the Statements section of the Reference.
Related
I am new iOS Developer
I want to change the websiteLogo API with a textfield to change the URL.
how can I change the line with the ***
with a var and a textfield in my viewcontroller?
With screenshoot it's will be easier to understand what I want? Thank you !!! Guys. OneDriveLink. 1drv.ms/u/s!AsBvdkER6lq7klAqQMW9jOWQkzfl?e=fyqOeN
private init() {}
**private static var pictureUrl = URL(string: "https://logo.clearbit.com/:http://www.rds.ca")!**
private var task: URLSessionDataTask?
func getQuote(callback: #escaping (Bool, imageLogo?) -> Void) {
let session = URLSession(configuration: .default)
task?.cancel()
task = session.dataTask(with: QuoteService.pictureUrl) { (data, response, error) in
DispatchQueue.main.async {
guard let data = data, error == nil else {
callback(false, nil)
return
}
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
callback(false, nil)
return
}
let quote = imageLogo(image: data)
callback(true, quote)
print(data)
}
}
task?.resume()
}
First, please don't use screenshots do show your code. If you want help, others typically copy/paste your code to check whats wrong with it.
There are some minor issues with your code. Some hints from me:
Start your types with a big letter, like ImageLogo not imageLogo:
Avoid statics
Avoid singletons (they are almost statics)
Hand in the pictureUrl into getQuote
struct ImageLogo {
var image:Data
}
class QuoteService {
private var task: URLSessionDataTask?
func getQuote(from pictureUrl:URL, callback: #escaping (Bool, ImageLogo?) -> Void) {
let session = URLSession(configuration: .default)
task?.cancel()
task = session.dataTask(with: pictureUrl) {
(data, response, error) in
DispatchQueue.main.async {
guard let data = data, error == nil else {
callback(false, nil)
return
}
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
callback(false, nil)
return
}
let quote = ImageLogo(image: data)
callback(true, quote)
print(data)
}
}
task?.resume()
}
}
Store an instance of QuoteService in your view controller
Call getQuote on that instance, handing in the pictureUrl
class ViewController : UIViewController {
var quoteService:QuoteService!
override func viewDidLoad() {
self.quoteService = QuoteService()
}
func toggleActivityIndicator(shown:Bool) { /* ... */ }
func update(quote:ImageLogo) { /* ... */ }
func presentAlert() { /* ... */ }
func updateconcept() {
guard let url = URL(string:textField.text!) else {
print ("invalid url")
return
}
toggleActivityIndicator(shown:true)
quoteService.getQuote(from:url) {
(success, quote) in
self.toggleActivityIndicator(shown:false)
if success, let quote = quote {
self.update(quote:quote)
} else {
self.presentAlert()
}
}
}
/* ... */
}
Hope it helps.
I think you want to pass textfield Text(URL Enter By user) in Web Services
Add a parameter url_str in getQuote function definition first and pass textfield value on that parameters
fun getQuote(url_str : String, callback : #escaping(Bool, ImgaeLogo/)->void){
}
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 creating an application that should call some web services in the begging of the app.
I've wrote a WebServiceManager that contains methods for calling web services and return result in completionHandler, here is one method of WebServiceManager:
extension WebServiceManager {
func checkVersion(completionHandler: #escaping (CheckVersionResponse?, Error?) -> ()) {
sessionManager?.adapter = BaseClientInterceptor()
let appVersion = Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String
let buildNumber = Bundle.main.infoDictionary!["CFBundleVersion"] as! String
sessionManager?.request(Config.VERSION_URL + "/\(appVersion)/\(buildNumber)" + Config.STATUS, method: .get).responseData { response in
print(response.response?.statusCode)
switch response.result {
case .success(let value):
print("----WS: checkVersion Success-----")
let value = JSON(value)
print(value)
let response = CheckVersionResponse(responseCode: response.response?.statusCode ?? -1)
if (response.getResponseCode() == 200) {
response.setUrl(value["url"].string ?? "")
response.setNow(value["now"].intValue)
response.setStatus(value["status"].stringValue)
response.setMessage(value["message"].stringValue)
}
completionHandler(response, nil)
case .failure(let error):
print(error)
print("----WS: checkVersion Failed-----")
completionHandler(nil, error)
}
}
}
}
and I use some methods of this manager in SplashScreenViewController:
class SplashViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
WebServiceManager.shared.checkVersion() { response, error in
//...
}
WebServiceManager.shared.getCurrentDate() { response, error in
//...
}
WebServiceManager.shared.getUpdatedKeyValues() { response, error in
//...
}
if !UserDefaults.standard.bool(forKey: "hasRegistered") {
self.performSegue(withIdentifier: "ToRegistration", sender: self)
} else {
self.performSegue(withIdentifier: "ToMain", sender: self)
}
}
}
I expect that this method runs line by line but it goes through to if at first.
How can I call these web services in order then make a decision based on UserDefaults values ?
sessionManager.request methods are asynchronous, so they will not execute synchronously (line by line) on viewDidAppear (by the way you are missing super.viewDidAppear())
If you want some code to execute after those 3 services you can do something like this, not sure if its the best solution tho:
class SplashViewController: UIViewController {
private var allDone = 0
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
WebServiceManager.shared.checkVersion() { response, error in
//...
checkUserDefault()
}
WebServiceManager.shared.getCurrentDate() { response, error in
//...
checkUserDefault()
}
WebServiceManager.shared.getUpdatedKeyValues() { response, error in
//...
checkUserDefault()
}
}
private func checkUserDefault() {
allDone += 1
/// When all 3 services are finish
if allDone == 3 {
if !UserDefaults.standard.bool(forKey: "hasRegistered") {
self.performSegue(withIdentifier: "ToRegistration", sender: self)
} else {
self.performSegue(withIdentifier: "ToMain", sender: self)
}
}
}
}
Hope it helps you!
EDIT: This is the simplest solution based on your existing code, if you want to do it 'the right way' you may want to check Promises: http://khanlou.com/2016/08/promises-in-swift/ https://www.raywenderlich.com/9208-getting-started-with-promisekit
You need to use PromiseKit and do something like this:
Replace your API methods so they use Promise as a response:
func checkVersion() -> Promise<CheckVersionResponse> {
sessionManager?.adapter = BaseClientInterceptor()
let appVersion = Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String
let buildNumber = Bundle.main.infoDictionary!["CFBundleVersion"] as! String
// Return a Promise for the caller of this function to use.
return Promise { fulfill, reject in
// Inside the Promise, make an HTTP request
sessionManager?.request(Config.VERSION_URL + "/\(appVersion)/\(buildNumber)" + Config.STATUS, method: .get).responseData { response in
print(response.response?.statusCode)
switch response.result {
case .success(let value):
print("----WS: checkVersion Success-----")
let value = JSON(value)
print(value)
let response = CheckVersionResponse(responseCode: response.response?.statusCode ?? -1)
if (response.getResponseCode() == 200) {
response.setUrl(value["url"].string ?? "")
response.setNow(value["now"].intValue)
response.setStatus(value["status"].stringValue)
response.setMessage(value["message"].stringValue)
}
fulfill(response)
case .failure(let error):
print(error)
print("----WS: checkVersion Failed-----")
reject(error)
}
}
}
}
Then on the UIViewController call the WebServiceManager methods like this:
WebServiceManager.shared.checkVersion().then { response in
// ....
WebServiceManager.shared.getCurrentDate()
}.then { date in
// ...
WebServiceManager.shared.getUpdatedKeyValues()
}.catch { error in
print(error)
}
We retrieve any saved passwords through the function:
SecRequestSharedWebCredential(NULL, NULL, ^(CFArrayRef credentials, CFErrorRef error) {
if (!error && CFArrayGetCount(credentials)) {
CFDictionaryRef credential = CFArrayGetValueAtIndex(credentials, 0);
if (credential > 0) {
CFDictionaryRef credential = CFArrayGetValueAtIndex(credentials, 0);
NSString *username = CFDictionaryGetValue(credential, kSecAttrAccount);
NSString *password = CFDictionaryGetValue(credential, kSecSharedPassword);
dispatch_async(dispatch_get_main_queue(), ^{
//Updates the UI here.
});
}
}
});
The issue is that on IOS 9.3.3 iPhone 6 A1524, we get the prompt with an entry called 'Passwords not saved'. There is no error message to suggest the no passwords have been found. Because the array > 0, it completes the form with the entry.
Why is this the case? We thought the prompt does not appear if no passwords are stored under your entitled domains.
Any suggestions?
Thank you.
I'm checking for this in viewDidLoad() for my Auth view controller. The code is a bit different than above, gleaned from several other SO answers.
Swift 3:
SecRequestSharedWebCredential(Configuration.webBaseFQDN as CFString, nil, { (credentials, error) in
if let error = error {
print("ERROR: credentials")
print(error)
}
guard let credentials = credentials, CFArrayGetCount(credentials) > 0 else {
// Did not find a shared web credential.
return
}
guard CFArrayGetCount(credentials) == 1 else {
// There should be exactly one credential.
return
}
let unsafeCredential = CFArrayGetValueAtIndex(credentials, 0)
let credential = unsafeBitCast(unsafeCredential, to: CFDictionary.self)
let unsafeEmail = CFDictionaryGetValue(credential, Unmanaged.passUnretained(kSecAttrAccount).toOpaque())
let email = unsafeBitCast(unsafeEmail, to: CFString.self) as String
let unsafePassword = CFDictionaryGetValue(credential, Unmanaged.passUnretained(kSecSharedPassword).toOpaque())
let password = unsafeBitCast(unsafePassword, to: CFString.self) as String
if self.isValidEmail(email) && self.isValidPassword(password) {
self.usedSharedWebCredentials = true
self.doSignIn(email: email, password: password)
}
})
The extra check at the end for isValidEmail(_:) and isValidPassword(_:) handles the case where SecRequeestSharedWebCredential() returns "Passwords not saved" in the first credential (email).
Hopefully someone can explain why this is happening, but if not, at least there's a way to trap this scenario.
I'd also like to add that I've seen this up to iOS 10.2.1
I bumped into the same issue and wanted to add that the spaces in "Passwords not saved" are not real spaces. Not sure why, maybe something odd when converting from CFString.
Either way, since Apple documentation is still in ObjC and their Security framework is still very much CoreFoundation-heavy, I thought it'd be nice to post the whole Swift 5 code I've written for the Shared Web Credentials wrapper.
It has nice error management logic (to adjust since you might not have the same ErrorBuilder API). About the weird spaces, when copied from Xcode to StackOverflow, they turn into real spaces, hence the extra logic in the String extension.
There is nothing better online from what I've seen.
//
// CredentialsRepository.swift
// Created by Alberto De Bortoli on 26/07/2019.
//
import Foundation
public typealias Username = String
public typealias Password = String
public struct Credentials {
public let username: Username
public let password: Password
}
public enum GetCredentialsResult {
case success(Credentials)
case cancelled
case failure(Error)
}
public enum SaveCredentialsResult {
case success
case failure(Error)
}
protocol CredentialsRepository {
func getCredentials(completion: #escaping (GetCredentialsResult) -> Void)
func saveCredentials(_ credentials: Credentials, completion: #escaping (SaveCredentialsResult) -> Void)
}
//
// SharedWebCredentialsController.swift
// Created by Alberto De Bortoli on 26/07/2019.
//
class SharedWebCredentialsController {
let domain: String
init(domain: String) {
self.domain = domain
}
}
extension SharedWebCredentialsController: CredentialsRepository {
func getCredentials(completion: #escaping (GetCredentialsResult) -> Void) {
SecRequestSharedWebCredential(domain as CFString, .none) { cfArrayCredentials, cfError in
switch (cfArrayCredentials, cfError) {
case (_, .some(let cfError)):
let underlyingError = NSError(domain: CFErrorGetDomain(cfError) as String,
code: CFErrorGetCode(cfError),
userInfo: (CFErrorCopyUserInfo(cfError) as? Dictionary))
let error = ErrorBuilder.error(forCode: .sharedWebCredentialsFetchFailure, underlyingError: underlyingError)
DispatchQueue.main.async {
completion(.failure(error))
}
case (.some(let cfArrayCredentials), _):
if let credentials = cfArrayCredentials as? [[String: String]], credentials.count > 0,
let entry = credentials.first,
// let domain = entry[kSecAttrServer as String]
let username = entry[kSecAttrAccount as String],
let password = entry[kSecSharedPassword as String] {
DispatchQueue.main.async {
if username.isValidUsername() {
completion(.success(Credentials(username: username, password: password)))
}
else {
let error = ErrorBuilder.error(forCode: .sharedWebCredentialsFetchFailure, underlyingError: nil)
completion(.failure(error))
}
}
}
else {
DispatchQueue.main.async {
completion(.cancelled)
}
}
case (.none, .none):
DispatchQueue.main.async {
completion(.cancelled)
}
}
}
}
func saveCredentials(_ credentials: Credentials, completion: #escaping (SaveCredentialsResult) -> Void) {
SecAddSharedWebCredential(domain as CFString, credentials.username as CFString, credentials.password as CFString) { cfError in
switch cfError {
case .some(let cfError):
let underlyingError = NSError(domain: CFErrorGetDomain(cfError) as String,
code: CFErrorGetCode(cfError),
userInfo: (CFErrorCopyUserInfo(cfError) as? Dictionary))
let error = ErrorBuilder.error(forCode: .sharedWebCredentialsSaveFailure, underlyingError: underlyingError)
DispatchQueue.main.async {
completion(.failure(error))
}
case .none:
DispatchQueue.main.async {
completion(.success)
}
}
}
}
}
extension String {
fileprivate func isValidUsername() -> Bool {
// https://stackoverflow.com/questions/38698565/secrequestsharedwebcredential-credentials-contains-passwords-not-saved
// don't touch the 'Passwords not saved', the spaces are not what they seem (value copied from debugger)
guard self != "Passwords not saved" else { return false }
let containsAllInvalidWords = contains("Passwords") && contains("not") && contains("saved")
return !containsAllInvalidWords
}
}
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