55

In a swift 2 command line tool (main.swift), I have the following:

import Foundation
print("yay")

var request = HTTPTask()
request.GET("http://www.stackoverflow.com", parameters: nil, completionHandler: {(response: HTTPResponse) in
    if let err = response.error {
        print("error: \(err.localizedDescription)")
        return //also notify app of failure as needed
    }
    if let data = response.responseObject as? NSData {
        let str = NSString(data: data, encoding: NSUTF8StringEncoding)
        print("response: \(str)") //prints the HTML of the page
    }
})

The console shows 'yay' and then exits (Program ended with exit code: 0), seemingly without ever waiting for the request to complete. How would I prevent this from happening?

The code is using swiftHTTP

I think I might need an NSRunLoop but there is no swift example

codecowboy
  • 9,835
  • 18
  • 79
  • 134
  • Does it have to be async? – trojanfoe Aug 11 '15 at 14:05
  • @trojanfoe not necessarily but am interested in either case, async or not. I decided to try and use https://github.com/daltoniam/SwiftHTTP as an experiment – codecowboy Aug 11 '15 at 14:07
  • Possible duplicate of [CFRunLoop in Swift Command Line Program](http://stackoverflow.com/questions/25126471/cfrunloop-in-swift-command-line-program) (which *has* a Swift example for NSRunLoop). – Martin R Aug 11 '15 at 15:10
  • You can use readline() if you are just debugging, it will keep the runloop running waiting for an input. Or you can use it to actually make user quit on their choice checking input against "y" or something – Sajad Khan Aug 01 '18 at 09:19

9 Answers9

44

Adding RunLoop.main.run() to the end of the file is one option. More info on another approach using a semaphore here

duan
  • 8,515
  • 3
  • 48
  • 70
codecowboy
  • 9,835
  • 18
  • 79
  • 134
35

I realize this is an old question, but here is the solution I ended on. Using DispatchGroup.

let dispatchGroup = DispatchGroup()

for someItem in items {
    dispatchGroup.enter()
    doSomeAsyncWork(item: someItem) {
        dispatchGroup.leave()
    }
}

dispatchGroup.notify(queue: DispatchQueue.main) {
    exit(EXIT_SUCCESS)
}
dispatchMain()
skwashua
  • 1,627
  • 17
  • 14
17

You can call dispatchMain() at the end of main. That runs the GCD main queue dispatcher and never returns so it will prevent the main thread from exiting. Then you just need to explicitly call exit() to exit the application when you are ready (otherwise the command line app will hang).

import Foundation

let url = URL(string:"http://www.stackoverflow.com")!
let dataTask = URLSession.shared.dataTask(with:url) { (data, response, error) in
    // handle the network response
    print("data=\(data)")
    print("response=\(response)")
    print("error=\(error)")

    // explicitly exit the program after response is handled
    exit(EXIT_SUCCESS)
}
dataTask.resume()

// Run GCD main dispatcher, this function never returns, call exit() elsewhere to quit the program or it will hang
dispatchMain()
Mike Vosseller
  • 4,097
  • 5
  • 26
  • 28
  • 1
    This is an elegant solution imo. It has the least cognitive load. – gprasant Jul 07 '17 at 21:27
  • 1
    Maybe it got down voted because the down voter could not find `dispatchMain()`. I think that Mike meant `dispatch_main()`. – Jerry Krinock Oct 27 '17 at 22:29
  • 2
    `dispatch_main()` is for obj-c. In Swift it is `dispatchMain()` https://developer.apple.com/documentation/dispatch/1452860-dispatchmain – Mike Vosseller Oct 28 '17 at 02:04
  • Xcode 10.1 seems to be bugged when using this and doesn't let you stop the process unless you quit Xcode (`kill -9` stops it but Xcode doesn't figure that out). `RunLoop.main.run()` works however – Allison Feb 23 '19 at 22:08
  • @gprasant please explain why displatchMain() is a better solution then DispatchSemaphore( value: 0) – Dblock247 Aug 21 '19 at 00:54
13

Don't depend on timing.. You should 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")

  // signals the process to continue
  sema.signal()
}

task.resume()

// sets the process to wait
sema.wait()
Sam Soffes
  • 14,831
  • 9
  • 76
  • 80
thiagoh
  • 7,098
  • 8
  • 51
  • 77
  • 2
    [Article here](https://priteshrnandgaonkar.github.io/concurrency-with-swift-3/) was also useful to me – Kdawgwilk Feb 21 '17 at 21:14
7

Swift 4: RunLoop.main.run()

At the end of your file

Chris Karani
  • 414
  • 5
  • 14
6

If your need isn't something that requires "production level" code but some quick experiment or a tryout of a piece of code, you can do it 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

More info : https://stackoverflow.com/a/40870157/469614


Please note that you shouldn't rely on fixed execution time in your architecture.

Community
  • 1
  • 1
Vexy
  • 1,274
  • 1
  • 12
  • 17
  • Hard-coded time intervals for asynchronous tasks are never a good idea. The command line interface should run the loop (without *timeout*) and stop it explicitly after returning from the completion block. Any timeout is supposed to be controlled by the `HTTPTask` or `URLSession` class. – vadian Nov 29 '16 at 16:08
  • As described in NOTES section of my original answer ;) – Vexy Nov 29 '16 at 19:39
  • can `sleep(15)` do the same trick? I am doing this way in CodeRunner – Kent Liau Dec 13 '16 at 10:32
  • 1
    Just to add onto this: To keep it running indefinitely you can use `Date.distantFuture`. – Kilian Dec 30 '16 at 13:06
1

Nowadays, we would use Swift concurrency and simply await the async task. E.g.,

do {
    guard let url = URL(string: "https://stackoverflow.com") else {
        throw URLError(.badURL)
    }

    let (data, _) = try await URLSession.shared.data(from: url)
    if let string = String(data: data, encoding: .utf8) {
        print(string)
    } else {
        print("Unable to generate string representation")
    }
} catch {
    print(error)
}

Unlike traditional completion handler patterns, if you await an async task, the command line app will not terminate before the asynchronous task is done.

For more information, see WWDC 2021 video Meet async/await or any of the “Related videos” listed on that page.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
0
// Step 1: Add isDone global flag

var isDone = false
// Step 2: Set isDone to true in callback

request.GET(...) {
    ...
    isDone = true
}

// Step 3: Add waiting block at the end of code

while(!isDone) {
    // run your code for 0.1 second
    RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.1))
}
joshmori
  • 438
  • 6
  • 11
0

In addition to Rob's answer nowadays (Swift 5.5+) there is a convenient way to create an asynchronous command line interface

@main
struct AsyncCLI { // the name is arbitrary 
    static func main() async throws {
        // your code goes here
    }
}

Important:

  • The enclosing file must not be named main.swift
vadian
  • 274,689
  • 30
  • 353
  • 361