7

So, I have a Swift command-line program:

import Foundation

print("start")

startAsyncNetworkingStuff()

RunLoop.current.run()

print("end")

The code compiles without error. The async networking code runs just fine, fetches all its data, prints the result, and eventually calls its completion function.

How do I get that completion function to break out of above current runloop so that the last "end" gets printed?

Added:

Replacing RunLoop.current.run() with the following:

print("start")

var shouldKeepRunning = true

startAsyncNetworkingStuff()

let runLoop = RunLoop.current
while (   shouldKeepRunning
       && runLoop.run(mode:   .defaultRunLoopMode,
                      before: .distantFuture ) ) {
}

print("end")

Setting

shouldKeepRunning = false

in the async network completion function still does not result in "end" getting printed. (This was checked by bracketing the shouldKeepRunning = false statement with print statements which actually do print to console). What is missing?

hotpaw2
  • 70,107
  • 14
  • 90
  • 153
  • In the [`run` documentation](https://developer.apple.com/reference/foundation/nsrunloop/1412430-run?language=objc), they say "If you want the run loop to terminate, you shouldn't use this [`run`] method. Instead, use one of the other `run` methods and also check other arbitrary conditions of your own, in a loop." The subsequent example there is in Objective-C, but it illustrates the idea. – Rob May 06 '17 at 20:54
  • See update to question, as per the other suggested run method. No change in "end" result. – hotpaw2 May 06 '17 at 21:12

3 Answers3

16

For a command line interface use this pattern and add a completion handler to your AsyncNetworkingStuff (thanks to Rob for code improvement):

print("start")

let runLoop = CFRunLoopGetCurrent()
startAsyncNetworkingStuff() { result in 
   CFRunLoopStop(runLoop)
}

CFRunLoopRun()
print("end")
exit(EXIT_SUCCESS)

Please don't use ugly while loops.


Update:

In Swift 5.5+ with async/await it has become much more comfortable. There's no need anymore to maintain the run loop.

Rename the file main.swift as something else and use the @main attribute like in a normal application.

@main
struct CLI {

    static func main() async throws {
        let result = await startAsyncNetworkingStuff()
        // do something with result 
    }
}

The name of the struct is arbitrary, the static function main is mandatory and is the entry point.

vadian
  • 274,689
  • 30
  • 353
  • 361
  • 2
    Ugly? But it's recommended in Apple's official documentation: https://developer.apple.com/reference/foundation/runloop/1412430-run – hotpaw2 May 06 '17 at 21:18
  • Ugly. I guess the (Objective-C) documentation has been written (and never been revised) before the introduction of blocks ;-) – vadian May 06 '17 at 21:21
  • Halfway works. Calling CFRunLoopStop(CFRunLoopGetCurrent()) in a Timer callback function works ("end" prints). But calling CFRunLoopStop(CFRunLoopGetCurrent()) in an NSURLSession dataTask completion block does not seem to stop the CFRunLoop ("end" never prints). – hotpaw2 May 06 '17 at 21:39
  • I'm using exactly this code with URLSession in a CLI. @Rob The redundancy of stopping the run loop and calling `exit` could be, probably it's just the practice to leave the CLI with a certain state. – vadian May 06 '17 at 21:45
  • @vadian. I agree. That while loop was hella ugly. There's no need to to use parentheses. – Peter Schorn Aug 14 '20 at 16:16
4

Here's how to use URLSession in a macOS Command Line Tool using Swift 4.2

// Mac Command Line Tool with Async Wait and Exit
import Cocoa

// Store a reference to the current run loop
let runLoop = CFRunLoopGetCurrent()

// Create a background task to load a webpage
let url = URL(string: "http://SuperEasyApps.com")!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
    if let data = data {
        print("Loaded: \(data) bytes")
    }
    // Stop the saved run loop
    CFRunLoopStop(runLoop)
}
task.resume()

// Start run loop after work has been started
print("start")
CFRunLoopRun()
print("end") // End will print after the run loop is stopped

// If your command line tool finished return success,
// otherwise return EXIT_FAILURE
exit(EXIT_SUCCESS)

You'll have to call the stop function using a reference to the run loop before you started (as shown above), or using GCD in order to exit as you'd expect.

func stopRunLoop() {
    DispatchQueue.main.async {
        CFRunLoopStop(CFRunLoopGetCurrent())
    }
}

References

https://developer.apple.com/documentation/corefoundation/1542011-cfrunlooprun

Run loops can be run recursively. You can call CFRunLoopRun() from within any run loop callout and create nested run loop activations on the current thread’s call stack.

https://developer.apple.com/documentation/corefoundation/1541796-cfrunloopstop

If the run loop is nested with a callout from one activation starting another activation running, only the innermost activation is exited.

Paul Solt
  • 8,375
  • 5
  • 41
  • 46
1

(Answering my own question)

Adding the following snippet to my async network completion code allows "end" to be printed :

DispatchQueue.main.async {
    shouldKeepRunning = false
}
hotpaw2
  • 70,107
  • 14
  • 90
  • 153
  • It looks like my original code had a threading bug in the inter-thread communication between the async networking thread and the main runloop thread. – hotpaw2 May 06 '17 at 21:57
  • Here's my final working code using solution #2: https://gist.github.com/hotpaw2/827a6e710d24036bdcb6a37aab921c6a#file-nsurl-dtask-test1-swift – hotpaw2 May 06 '17 at 22:09
  • It's much easier to use the URLSession API with completion handler rather than protocol/delegate. – vadian May 06 '17 at 22:16
  • @vadian : Example for a long running dataTask? (it's for data from a very slow server where the data dribbles down over many seconds.) – hotpaw2 May 06 '17 at 22:21
  • That doesn't matter. The delegate methods are only useful if you need to handle credentials or display a progress bar. – vadian May 06 '17 at 22:23
  • A progress bar is important if the data download takes many seconds, minutes, hours; and/or partial data is useful (which is true in my case). – hotpaw2 Oct 10 '18 at 05:39