Hi I'm using a share extension to post some data to my server using API request (Alamofire), the problem is that the request fails immediately and I don't know how to make it work, I read on some articles that I must use URLSession to send the request in the background but I couldn't find any example to how to make it work with alamofire, here is my code in share extension ViewController:
override func didSelectPost() {
MessageHTTPHelper.submitMessage(contains: contentText, completion: { (response) in
self.showAlert(title: "Result", message: response.result.isSuccess ? "SUCCESS" : "FAILURE")
})
}
The MessageHTTPHelper.submitMessage is a helper function that I defined and it works in the main app perfectly
I don't care about the response, I just want to send the request without any callbacks, can you please give me an example of sending a request in iOS share extension?
After lots of search and tests and fails, finally, this solution worked for me!
and here is my code in didSelectPost()
let body: Parameters = [
"version": Configs.currentReleaseVersion,
"content": cleanTextContent
]
let request = HTTPHelper.makeHTTPRequest(route: "message",
headers: HTTPHelper.defaultAuthHTTPHeaders,
verb: .post,
body: body,
apiV1Included: true)
let queue = DispatchQueue(label: "com.example.background", qos: .background, attributes: .concurrent)
request.responseJSON(queue: queue, options: .allowFragments) { (response) in
if response.result.isFailure {
guard let message = response.error?.localizedDescription else {
self.dismiss()
return
}
self.showAlert(title: "Error", message: message)
}
}
The HTTPHepler.makeHTTPRequest is just a helper method which creates an Alamofire DataRequest Instance with given parameters and returns it
Related
So migrating project from storyboard to swiftUI & figuring out swiftUI
We want business logic out of the swiftUI code
We have our login code technically working it works makes restful call but the flow is wrong in the button action
On login button action it calls authController().login(email,password)
In the login function I have it return bool
But in the button action it just keeps running to the next function without waiting for the return it needs
Have tried attaching a let to auth.login & working that way
If statements nothing it just runs all functions at once
Have not tired defer yet
Any suggestions or links to what I am missing
editing due to original was typed on my phone
this is the swiftUI
Button(action: {
LoginViewController().requestLogin(loginEmail: loginEmail,loginPassword: loginPassword)
self.goHome = true
}
that self.goHome moves the user to the next scene it don't matter what requestLogin is doing or what logic is in there it just proceeds
the requestLogin is using alamoFireManager for the API calls
// MARK: - Log In
func requestLogin(loginEmail: String, loginPassword: String){
//disable controls
enableControls(isEnabled: false)
//create request header and parameters
let loginParameters: [String: Any] = ["email": loginEmail, "password": loginPassword]
let header = ["Content-Type": "application/json", "Accept": "application/json"]
//setup configuration object
let configuration = URLSessionConfiguration.default
configuration.timeoutIntervalForRequest = TimeInterval(timeout)
configuration.timeoutIntervalForResource = TimeInterval(timeout)
//setup alamofire session manager
alamoFireManager = Alamofire.SessionManager(configuration: configuration)
//send request
alamoFireManager?.request(serverAddress + loginApi, method: .post, parameters: loginParameters, encoding: JSONEncoding.default, headers: header)
.responseJSON {response in
//enable controls
self.enableControls(isEnabled: true)
//SignUp response process
if let resultJson = processJsonData(responseData: response.data) {
isChecking = self.processLoginResponse(result: resultJson)
} else {
//Sign up failed
showAlert(parent: self, title: "Login Failed", message: "Login failed, please check the network connection and try it again.")
}
}
}
From my Watch, I send commands to my iOS app. It's not clear why but if the app is in the background I can see some errors:
Error Domain=NSURLErrorDomain Code=-997 "Lost connection to background transfer service"
Can't end BackgroundTask: no background task exists with identifier 383 (0x17f), or it may have already been ended. Break in UIApplicationEndBackgroundTaskError() to debug.
I've already tried to change my configuration to background, have a correct identifier for my config.
Static or Lazy implementation of my SessionManager.
Count for deinit on the process.
Network Session manager
static var sessionManager: SessionManager = {
let configuration = URLSessionConfiguration.background(withIdentifier: UUID().uuidString + ".WatchOS_Background")
configuration.httpShouldSetCookies = false
configuration.httpMaximumConnectionsPerHost = 4
configuration.timeoutIntervalForRequest = 50
configuration.networkServiceType = .background
configuration.isDiscretionary = false
configuration.shouldUseExtendedBackgroundIdleMode = true
if #available(iOS 13.0, *) {
configuration.allowsExpensiveNetworkAccess = true
configuration.allowsConstrainedNetworkAccess = true
}
let sessionManager = Alamofire.SessionManager(configuration: configuration)
sessionManager.delegate.sessionDidBecomeInvalidWithError = { _, error in
if let error = error {
print(error)
}
}
sessionManager.delegate.taskDidComplete = { _, task, error in
if let error = error {
print(error)
}
}
return sessionManager
}()
Request example
func getListFromServer(completion: #escaping (ServiceResponse<[Model1]>) -> Void) {
let header: HTTPHeaders = ["User-Agent": UserAgentHelper.fullUserAgentString]
request("/api/1/XXXX", method: .get, parameters: nil, encoding: nil, headers: header).responseData { [weak self] response in
guard let strongSelf = self else { return }
completion(strongSelf.completionResponse(response))
}
}
Request method
#discardableResult private func request(
_ path: String,
method: HTTPMethod,
parameters: Parameters? = nil,
encoding: ParameterEncoding? = nil,
headers: HTTPHeaders? = nil)
-> DataRequest {
let userEncoding = encoding ?? self.defaultEncoding
let task = beginBackgroundTask()
let dataRequest = NetworkService.sessionManager.request("\(API)\(path)",
method: method,
parameters: parameters,
encoding: userEncoding,
headers: headers)
dataRequest.validate()
self.endBackgroundTask(taskID: task)
return dataRequest
}
Begin and end background task
func beginBackgroundTask() -> UIBackgroundTaskIdentifier {
return UIApplication.shared.beginBackgroundTask(withName: "Background_API", expirationHandler: {})
}
func endBackgroundTask(taskID: UIBackgroundTaskIdentifier) {
UIApplication.shared.endBackgroundTask(taskID)
}
I hope to have a proper implementation from your and a stable request life cycle.
Many thanks for your help and sorry in advance for the lack of technical terms.
Your core problem is that you're not properly handling the expiration of your background tasks. You must end the tasks in their expiration handler explicitly:
let task = UIApplication.shared.beginBackgroundTask(withName: "Background_API") {
UIApplication.shared.endBackgroundTask(task)
}
I suggest you read more here, where an Apple DTS engineer has extensively outlined the requirements and edge cases of the background task handling.
Additionally, Alamofire doesn't really support background sessions. Using a foreground session with background task handling is probably your best bet. Once the Alamofire SessionManager is deinitialized, any requests it has started will be cancelled, even for background sessions.
Finally, calling validate() within an Alamofire response handler is invalid. You should be calling it on the request before the response handler is added, as it's validates the response before handlers are called. If you're calling it afterward it won't be able to pass the error it produces to your response handler.
I have 5 different services requests to load into same UItableView for each cells.
What is the best way to approach in order to do this.
https://example.com/?api/service1
https://example.com/?api/service2
https://example.com/?api/service3
https://example.com/?api/service4
https://example.com/?api/service5
let url = "https://example.com/?api/service1
Alamofire.request(url, method: .get, parameters:nil encoding: JSONEncoding.default, headers: nil)
.responseJSON { response in
print(response.result.value as Any) // result of response serialization
}
repeat the same Alamofire five times with different services name there is another way to implement it.
Look at using a DispatchGroup to perform multiple async requests and wait for them all to complete.
For each task you call group.enter() and in its completion handler when you know that request has finished you call group.leave(). Then there is a notify method which will wait for all requests to call leave to tell you that they have all finished.
I have created an example in a Playground (which will fail with errors because of the URL's used)
import UIKit
import PlaygroundSupport
let serviceLinks = [
"https://example.com/?api/service1",
"https://example.com/?api/service2",
"https://example.com/?api/service3",
"https://example.com/?api/service4",
"https://example.com/?api/service5"
]
// utility as I've not got alamofire setup
func get(to urlString: String, completion: #escaping (Data?, URLResponse?, Error?) -> Void) {
let url = URL(string: urlString)!
let session = URLSession.shared
let task = session.dataTask(with: url) { data, response, error in
completion(data, response, error)
}
task.resume()
}
let group = DispatchGroup()
for link in serviceLinks {
group.enter() // add an item to the list
get(to: link) { data, response, error in
// handle the response, process data, assign to property
print(data, response, error)
group.leave() // tell the group your finished with this one
}
}
group.notify(queue: .main) {
//all requests are done, data should be set
print("all done")
}
PlaygroundPage.current.needsIndefiniteExecution = true
You probably won't be able to just loop through the URL's like I have though because the handling of each service is probably different. You'll need to tweak it based on your needs.
There is alot more information about DispatchGroups available online such as this article
I'm having this weird issue in which a newly created URLSessionUploadTask gets cancelled instantly. I'm not sure if it's a bug with the current beta of Xcode 8.
I suspect it might be a bug because the code I'm about to post ran fine exactly once. No changes were made to it afterwards and then it simply stopped working. Yes, it literally ran once, and then it stopped working. I will post the error near the end.
I will post the code below, but first I will summarize how the logic here works.
My test, or user-exposed API (IE for use in Playgrounds or directly on apps), calls the authorize method. This authorize method will in turn call buildPOSTTask, which will construct a valid URL and return a URLSessionUploadTask to be used by the authorize method.
With that said, the code is below:
The session:
internal let urlSession = URLSession(configuration: .default)
Function to create an upload task:
internal func buildPOSTTask(onURLSession urlSession: URLSession, appendingPath path: String, withPostParameters postParams: [String : String]?, getParameters getParams: [String : String]?, httpHeaders: [String : String]?, completionHandler completion: URLSessionUploadTaskCompletionHandler) -> URLSessionUploadTask {
let fullURL: URL
if let gets = getParams {
fullURL = buildURL(appendingPath: path, withGetParameters: gets)
} else {
fullURL = URL(string: path, relativeTo: baseURL)!
}
var request = URLRequest(url: fullURL)
request.httpMethod = "POST"
var postParameters: Data? = nil
if let posts = postParams {
do {
postParameters = try JSONSerialization.data(withJSONObject: posts, options: [])
} catch let error as NSError {
fatalError("[\(#function) \(#line)]: Could not build POST task: \(error.localizedDescription)")
}
}
let postTask = urlSession.uploadTask(with: request, from: postParameters, completionHandler: completion)
return postTask
}
The authentication function, which uses a task created by the above function:
public func authorize(withCode code: String?, completion: AccessTokenExchangeCompletionHandler) {
// I have removed a lot of irrelevant code here, such as the dictionary building code, to make this snippet shorter.
let obtainTokenTask = buildPOSTTask(onURLSession: self.urlSession, appendingPath: "auth/access_token", withPostParameters: nil, getParameters: body, httpHeaders: nil) { (data, response, error) in
if let err = error {
completion(error: err)
} else {
print("Response is \(response)")
completion(error: nil)
}
}
obtainTokenTask.resume()
}
I caught this error in a test:
let testUser = Anilist(grantType: grant, name: "Test Session")
let exp = expectation(withDescription: "Waiting for authorization")
testUser.authorize(withCode: "a valid code") { (error) in
if let er = error {
XCTFail("Authentication error: \(er.localizedDescription)")
}
exp.fulfill()
}
self.waitForExpectations(withTimeout: 5) { (err) in
if let error = err {
XCTFail(error.localizedDescription)
}
}
It always fails instantly with this error:
Error Domain=NSURLErrorDomain Code=-999 "cancelled" UserInfo={NSErrorFailingURLKey=https://anilist.co/api/auth/access_token?client_secret=REMOVED&grant_type=authorization_code&redirect_uri=genericwebsitethatshouldntexist.bo&client_id=ibanez-hod6w&code=REMOVED,
NSLocalizedDescription=cancelled,
NSErrorFailingURLStringKey=https://anilist.co/api/auth/access_token?client_secret=REMOVED&grant_type=authorization_code&redirect_uri=genericwebsitethatshouldntexist.bo&client_id=ibanez-hod6w&code=REMOVED}
Here's a few things to keep in mind:
The URL used by the session is valid.
All credentials are valid.
It fails instantly with a "cancelled" error, that simply did not happen before. I am not cancelling the task anywhere, so it's being cancelled by the system.
It also fails on Playgrounds with indefinite execution enabled. This is not limited to my tests.
Here's a list of things I have tried:
Because I suspect this is a bug, I first tried to clean my project, delete derived data, and reset all simulators. None of them worked.
Even went as far restarting my Mac...
Under the small suspicion that the upload task was getting deallocated due to it not having any strong pointers, and in turn calling cancel, I also rewrote authorize to return the task created by buildPOSTTask and assigned it to a variable in my test. The task was still getting cancelled.
Things I have yet to try (but I will accept any other ideas as I work through these):
Run it on a physical device. Currently downloading iOS 10 on an iPad as this is an iOS 10 project. EDIT: I just tried and it's not possible to do this.
I'm out of ideas of what to try. The generated logs don't seem to have any useful info.
EDIT:
I have decided to just post the entire project here. The thing will be open source anyway when it is finished, and the API credentials I got are for a test app.
ALCKit
After struggling non-stop with this for 6 days, and after googling non-stop for a solution, I'm really happy to say I have finally figured it out.
Turns out that, for whatever mysterious reason, the from: parameter in uploadTask(with:from:completionHandler) cannot be nil. Despite the fact that the parameter is marked as an optional Data, it gets cancelled instantly when it is missing. This is probably a bug on Apple's side, and I opened a bug when I couldn't get this to work, so I will update my bug report with this new information.
With that said, everything I had to do was to update my buildPOSTTask method to account for the possibility of the passed dictionary to be nil. With that in place, it works fine now:
internal func buildPOSTTask(onURLSession urlSession: URLSession, appendingPath path: String, withPostParameters postParams: [String : String]?, getParameters getParams: [String : String]?, httpHeaders: [String : String]?, completionHandler completion: URLSessionUploadTaskCompletionHandler) -> URLSessionUploadTask {
let fullURL: URL
if let gets = getParams {
fullURL = buildURL(appendingPath: path, withGetParameters: gets)
} else {
fullURL = URL(string: path, relativeTo: baseURL)!
}
var request = URLRequest(url: fullURL)
request.httpMethod = "POST"
var postParameters: Data
if let posts = postParams {
do {
postParameters = try JSONSerialization.data(withJSONObject: posts, options: [])
} catch let error as NSError {
fatalError("[\(#function) \(#line)]: Could not build POST task: \(error.localizedDescription)")
}
} else {
postParameters = Data()
}
let postTask = urlSession.uploadTask(with: request, from: postParameters, completionHandler: completion)
return postTask
}
Are you by any chance using a third party library such as Ensighten? I had the exact same problem in XCode 8 beta (works fine in XCode 7) and all of my blocks with nil parameters were causing crashes. Turns out it was the library doing some encoding causing the issue.
For me, this was a weak reference causing the issue, so I changed
completion: { [weak self] (response: Result<ResponseType, Error>)
to
completion: { [self] (response: Result<ResponseType, Error>)
I'm using Alamofire in my application and wish to display an alert if the request has an error (e.g. wrong URL), etc.
I have this function in a separate class as it is shared among the pages of the application.
Alamofire.request(.GET, api_url)
.authenticate(user: str_api_username, password: str_api_password)
.validate(statusCode: 200..<300)
.response { (request, response, data, error) in
if (error != nil) {
let alertController = UIAlertController(title: "Server Alert", message: "Could not connect to API!", preferredStyle: UIAlertControllerStyle.Alert)
alertController.addAction(UIAlertAction(title: "Dismiss", style: UIAlertActionStyle.Default,handler: nil))
self.presentViewController(alertController, animated: true, completion: nil)
}
}
As Alamofire works asynchronously I need to do the error check then & there (unless you suggest otherwise) because then I want to manipulate the results and if the URL was wrong then it can get messy.
No surprise, the
self.presentViewController(alertController, animated: true, completion: nil)
does not work so how can I display this alert?
I'd say the conventional approach for this is to have whoever calls this network request be responsible for displaying the alert. If when the request is complete, you call back to the original calling object, they are responsible for displaying the alert. One of the reasons for this is that errors can mean different things in different contexts. You may not always want to display an alert - this provides you with more flexibility as you're building out your app. The same way that AlamoFire calls your response closure when it is done, I think it's best to pass that back to whoever initiated this call in your Downloader object.
Update:
You want to structure it the same way AlamoFire structures it. You pass the closure to AF which gets called when the AF request finishes.
You'll have to add a closure param to your download function (See downloadMyStuff). Then, once the AF request finishes, you can call the closure you previously defined ( completion). Here's a quick example
class Downloader {
func downloadMyStuff(completion: (AnyObject?, NSError?) -> Void) {
Alamofire.request(.GET, "http://myapi.com")
.authenticate(user: "johndoe", password: "password")
.validate(statusCode: 200..<300)
.response { (request, response, data, error) in
completion(data, error)
}
}
}
class ViewController: UIViewController {
let downloader = Downloader()
override func viewDidLoad() {
super.viewDidLoad()
self.downloader.downloadMyStuff { (maybeResult, maybeError) -> Void in
if let error = maybeError {
println("Show your alert here from error \(error)")
}
if let result: AnyObject = maybeResult {
println("Parse your result and do something cool")
}
}
}
}