Today, we'll be using the Swift Package Manager instead of Xcode Playgrounds. So make sure you've installed it via the links from yesterday's reading.

First, create a directory on your system where we can work freely. Within that directory, we'll create another directory called NetworkCore. This will be the "module" directory.

The Package file

Create a file in the NetworkCore directory named Package.swift. We'll talk more about what this file can do later, for now copy these lines of code in it:

  //Package.swift
  import PackageDescription
  let package = Package(
    name: "NetworkCore"
  )

Before we move on, notice the "package" file tells the Swift compiler what our product name will be, in this case, "NetworkCore". Notice that this file is written in valid Swift syntax itself.

Next, we'll create a directory inside NetworkCore, called Sources. This is a magic directory name. It tells the Swift compiler where to find source code files.

We'll add a Swift code file, which is any file with the extension .swift. I'll call it DataKey.swift.

Inside, we'll add some code from our lessons on associated types.

  import Foundation
  protocol DataKey : Hashable {
    associatedtype Value
      var request:String { get }
      func value(with data:Data)throws->Value
  }

We could add a lot more, but it's about time we built the project. In the Terminal app, let's cd into the NetworkCore directory, and run swift build.

If you're not familiar with the command line, here's the only thing I've ever needed to memorize: Use the command cd followed by a space, then drag and drop in the a Finder windows' folder icon to drop the correct path name. Then hit return, and the Terminal will execute subsequent commands relative to that directory.

Assuming all the tools are installed correctly, we'll see :

  Compile Swift Module 'NetworkCore' (1 sources)

appear in the Terminal.

If you've set Finder to show hidden files, you'll see a .build directory appears at the top level. Inside, we have a debug directory, which includes our build products.

Most of those aren't terribly useful by themselves at the moment. We'll get to a full app later.

Working with many source files

Since we're building full Swift modules now, and not just playgrounds, we can create as many .swift files as we like.

Let's look at our DataKey.swift file. There's more code in here than just DataKey. Suppose we had several hundred lines, we might want to break up the code a bit. Let's create a second file, named DataManager.swift, in our Sources directory and move our DataManager code into it.

  //DataManager.swift
  protocol DataManager {
      func value<K:DataKey>(for key:K)throws->K.Value?
  }
  protocol DataFetcher {
      func data(request:String)->Data?
  }
  class ConcreteDataManager : DataManager {
      var fetcher:DataFetcher
      init(fetcher:DataFetcher) {
          self.fetcher = fetcher
      }
      func value<K:DataKey>(for key:K)throws->K.Value? {
          guard let data = fetcher.data(request:key.request)
          else { return nil }
          return try key.value(with:data)
      }
  }

Now when we build, we'll have a failure:

  ///error: use of undeclared type 'Data'

The bug here is the Data type is defined in the Foundation module, but there is no import statement in the second file we created.

Unlike C #include and Obj-C #import statements, Swift import does not carry from one file to another.

  //DataManager.swift
  import Foundation

  protocol DataManager {
      func value<K:DataKey>(for key:K)throws->K.Value?
  }
  ...

We need to write specific import statements in each .swift file, as needed.

Let's swift build again, and we have success!

Unlike C-based languages, we had no need to import the DataKey.swift file into the DataManager.swift file. Swift automatically knows about types declared in other files. What it does not know about is types or methods declared as private or fileprivate in other files.

Order independent

As order independent source files, we can write code which refers to code which the compiler hasn't yet reached. So we're free to define our protocols after the types that adopt them. This means we can write types, and global constants in any order.

However, we can not write "top level statements". In other words, we can't directly call methods outside of method definitions.

Building executable modules

In addition to modules which work like frameworks, we can also build command line apps.

Let's move to a new directory where we can start fresh. I'll name this one SimpleApp.

  // /SimpleApp/Package.swift
  import PackageDescription
  let package = Package(
    name: "SimpleApp"
  )

We'll create a Package.swift file, and inside name the package, SimpleApp. Next we'll create a Sources directory, just like the framework module.

But we'll add a special file, a main.swift file.

Inside main.swift, we'll add code:

  print("Hello, Swift!")

Unlike other order-independent .swift files, main.swift is order-dependent, so we are allowed to add top-level statements, like this print statement.

From the command line, we swift build. In the .build/debug directory, we'll find the SimpleApp executable.

We can execute it using this command:

  ./.build/debug/SimpleApp

For command-line newbies, this is just the path to the executable from the current location.

  //Hello, Swift!

And now our print statements show up right in the console.

Command-line arguments

There are two ways to get the command line arguments.

  import Foundation
  let args:[String] = ProcessInfo.processInfo.arguments

First, we could use the shared ProcessInfo singleton to get the command line arguments one by one.

  import Foundation
  let defaults:UserDefaults = UserDefaults.standard

The second is to use the UserDefaults object.

  let shouldUseMetric:Bool = defaults.bool(forKey:"metric")
  print(shouldUseMetric ? "0C" : "32F")

One common convention for specifying options in a command line is to use two arguments, one an option name, preceded with a -, and the second argument is the value. The UserDefaults type facilitates this and other behavior.

  ./.build/debug/SimpleApp
  //32 F

So now if we execute the app, we'll get default behavior, in F.

  ./.build/debug/SimpleApp -metric 1
  //0C

But if we add the -metric option followed by a 1 or true or YES, Swift will interpret that as a Bool.

Notice that UserDefaults supports several other types, like Floats, and Strings.

  open class UserDefaults : NSObject {
    open func register(defaults registrationDictionary: [String : Any])

And it supports the explicit registration of default values when they aren't specified by the command line.

  open func synchronize() -> Bool

And it supports saving a user's preferences. This isn't suitable for saving security-sensitive information like passwords, nor is it suitable for storing entire documents, but for a small number of "preferences", UserDefaults will be useful.

Summary

Today we learned to build a module with the Swift Package Manager from the command line. We organized our source code files in magic directories, and learned when to include which import statements. We defined our module name in a Package.swift file. Then we learned to build executable command-line apps by adding a main.swift file.

Building command line apps from the command line, now that's Sw--- ok, that's pretty much what everybody else has been doing for decades.