From a CLI task to a run (main) loop
In the previous post, we explored the process of creating a simple Swift Command Line Tool with a one-time execution. It starts up, processes a task, then it closes. This is a useful and very common pattern - but applications wouldn’t be very useful if they always closed after executing a task. Any application with a Graphical User Interface runs until it’s told to stop or until it crashes - and the same applies to some server-side applications. We can change the way this application behaves simply by adding a simple event loop (also known as a main loop or run loop). In essence, the mechanism remains the same, regardless of the underlying frameworks (SwiftUI, UIKit, or technologies outside of Apple’s infrastructure):
Your application performs some setup activities, either in a serial, synchronous manner - or asynchronously, concurrently
Once the setup process is complete, the final instruction in the application’s entry point creates an infinite loop, with some very clear (and mandatory) exit/return/continue conditions. To see this in action, simply replace the content of the cli-example main.swift file with the snippet below:
print("Enter some text and I will repeat it, or enter 'q' to quit\n")
while true {
print("what should I repeat?: ", terminator: "")
guard let input = readLine()?.trimmingCharacters(in: .whitespacesAndNewlines) else {
continue
}
// Check if user wants to quit
if input.lowercased() == "q" {
print("Goodbye!")
break
}
// Skip empty input
if input.isEmpty {
print("Please enter some text or 'q' to quit\n")
continue
}
print("Input was: \(input)")
}
print("Enter some text and I will repeat it, or enter 'q' to quit\n")
while true {
print("what should I repeat?: ", terminator: "")
guard let input = readLine()?.trimmingCharacters(in: .whitespacesAndNewlines) else {
continue
}
// Check if user wants to quit
if input.lowercased() == "q" {
print("Goodbye!")
break
}
// Skip empty input
if input.isEmpty {
print("Please enter some text or 'q' to quit\n")
continue
}
print("Input was: \(input)")
}
If you would run the new executable, you would essentially have a process with an event loop ( in an overly simplified manner, this is mimics very well how more complex applications work).
$\> swift run cli-example
Building for debugging...
[7/7] Applying cli-example
Build of product 'cli-example' complete! (0.81s)
Enter some text and I will repeat it, or enter 'q' to quit
what should I repeat?: Something
Input was: Something
what should I repeat?: Something else
Input was: Something else
what should I repeat?:
Please enter some text or 'q' to quit
what should I repeat?: q
Goodbye!
A Swift Program - from source code to executable file
When developing applications for Apple’s ecosystem, you would spend a good portion of your time writing Swift source code files. Sometimes, you may need to use Objective-C, C or C++, but even then, you would mostly write source code. While writing Swift source code, you use components (libraries) included in the Swift versions of Apple’s numerous frameworks - one of which being SwiftUI - and you ensure your code respects the requirements of Apple’s frameworks, into which your code would need to integrate.
Since Swift is a compiled language, when your program’s code is ready, you need to compile it. So far, we have seen how a simple Swift program is created by writing text into files - and then how Swift’s toolset transforms that text into another set of files, such as object files, dynamic library files and, importantly, the executable file. We have explored the main mechanisms you can use to describe an application, using concepts (symbols and abstractions) you find in the Swift programming language. We have then looked at the build process and how it translates those source files into binary object files, then bundles everything together into one, Mach-O executable file you can use to execute the program.
Between the source code files and the final executable files, the LLVM toolset (Compiler, Assembler, Linker) performs various other, intermediary tasks:
Perform Lexical and Syntactic Analysis (this is where compilation due to syntax errors would fail);
Perform Semantic Analysis (for type checking);
Generate intermediary LLVM IR files (LLVM Intermediate Representation files, which are target-independent files written in the LLVM Assembly Language)
Generate platform-specific assembly language files
Generate the Binary Object files, from the platform specific assembly language files
Bundle everything into an executable file, usable by the Operating System’s kernel ( in this particular case, theMach-O executable file for the XNU kernel )
More complex projects (such as an application with a GUI) would also include many other components, which are used while the application is running (from icons to pre-rendered sprites to interface builder files)
The diagram below represents a simplified overview of the numerous transformations your Swift Code will go through.
Once your program is bundled into a Mach-O executable file, it can be started by the Operating System.