Using NSURLSession from a Swift command line progr

2019-01-17 03:58发布

问题:

I'm trying to test a little proof-of-concept command line app prior to integrating it into a larger app. What I'm trying to do is download some data using NSURLSession using this example. However it appears that if I use the examples given in a simple OS X command line app then the app exits prior to the data being retrieved.

How can I download data from a stand-alone command line app using NSURLSession? What I've read about is using NSRunLoop however I've not yet found a clear example in Swift so if NSRunLoop is actually the way to go then any examples would be appreciated.

Any other strategies for downloading data from a URL for a Swift command line app is also welcome (infinite while loop?).

回答1:

You can use a semaphore to block the current thread and wait for your URL session to finish.

Create the semaphore, kick off your URL session, then wait on the semaphore. From your URL session completion callback, signal the semaphore.

You could use a global flag (declare a volatile boolean variable) and poll that from a while loop, but that is less optimal. For one thing, you're burning CPU cycles unnecessarily.

Here's a quick example I did using a playground:

import Foundation

var sema = DispatchSemaphore( value: 0 )

class Delegate : NSObject, URLSessionDataDelegate
{
    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data)
    {
        print("got data \(String(data: data, encoding: .utf8 ) ?? "<empty>")");
        sema.signal()
    }
}

let config = URLSessionConfiguration.default
let session = URLSession(configuration: config, delegate: Delegate(), delegateQueue: nil )

guard let url = URL( string:"http://apple.com" ) else { fatalError("Could not create URL object") }

session.dataTask( with: url ).resume()    

sema.wait()


回答2:

Try this

let sema = DispatchSemaphore( value: 0)

let url = URL(string: "https://upload.wikimedia.org/wikipedia/commons/4/4d/Cat_November_2010-1a.jpg")!;

let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
  print("after image is downloaded");
  sema.signal(); // signals the process to continue
};

task.resume();
sema.wait(); // sets the process to wait


回答3:

If it's just for testing purposes, you can avoid semaphore usage if you hard code "execution time" of your command line app like this :

SWIFT 3

//put at the end of your main file
RunLoop.main.run(until: Date(timeIntervalSinceNow: 15))  //will run your app for 15 seconds only

This is very quick and dirty way to "enable" commandline apps to wait for other threads to finish. Also, your app will exit normally after timeout expires, without the need to explicitly kill or cancel app process.

NOTE :

  1. You can alter 'timeout' period if your networking tasks require more time to complete.
  2. This 'solution' is definitely poor decision if you want more serious waiting mechanism (aka. DON'T USE THIS IN PRODUCTION)


回答4:

I got around this with a while loop and bool flag which is set to true when url session returns

Xcode > MacOS > console app

main.swift

import Foundation

//calls api to send push message but call is async and console app may have died before ws call returns
func sendRequest() {
    print("sendRequest called")
    let sessionConfig = URLSessionConfiguration.default
    let session = URLSession(configuration: sessionConfig, delegate: nil, delegateQueue: nil)
    guard var URL = URL(string: "https://api.pushover.net/1/messages.json") else {return}
    let URLParams = [
        "user": "USER",
        "token": "TOKEN",
        "message": "New+PUSH",
        ]
    //helper method to append params    
    URL = URL.appendingQueryParameters(URLParams)
    var request = URLRequest(url: URL)
    request.httpMethod = "POST"

    /* Start a new Task */

    print("sendRequest called: POST ")


    //FLAG to use to keep app around till WS returns
    var keepRunning = true
    let task = session.dataTask(with: request,
                                completionHandler: { (data: Data?, response: URLResponse?, error: Error?) -> Void in

        if (error == nil) {
            // Success
            print("sendRequest called: POST RETURNED Success")
            let statusCode = (response as! HTTPURLResponse).statusCode
            print("URL Session Task Succeeded: HTTP \(statusCode)")
        }
        else {
            // Failure
            print("URL Session Task Failed: %@", error!.localizedDescription);
        }

        //WS - returns async and reponse handled - flip bool to kill while loop below and also kills app
              print("WS reponse handled set keepRunning to false")
    keepRunning = false
    })
    task.resume()
    //session.finishTasksAndInvalidate()


    //Issue: console app will die when doSearch() ends, but ws call may not have returned so session may die and ws call may fail so add while loop to prevent that
    while keepRunning {
        print("keepRunning: true: loop")
    }
    print("keepRunning:\(keepRunning) - sendRequest() ends")
}

//main.swift - starts here
print("call sendRequest")
sendRequest()





call sendRequest
sendRequest called
sendRequest called: POST 
keepRunning: true: loop
keepRunning: true: loop
....
keepRunning: true: loop
keepRunning: true: loop
keepRunning: true: loop
.....
keepRunning: true: loop
keepRunning: true: loop
sendRequest called: POST RETURNED Success
keepRunning: true: loop
keepRunning: true: loop
.....
keepRunning: true: loop
URL Session Task Succeeded: HTTP 200
keepRunning: true: loop
WS reponse handled set keepRunning to false
keepRunning: true: loop
keepRunning:false - sendRequest() ends
Program ended with exit code: 0