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
Float
s, and
String
s.
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.
Comments for Module Layouts