Swift Regex Deep Dive
iOS MacOur introductory guide to Swift Regex. Learn regular expressions in Swift including RegexBuilder examples and strongly-typed captures.
In It Looks Like You Are Trying to Use a Framework we learned about object files, static and dynamic libraries, frameworks, and their structure. It was a detailed, albeit theoretical view of frameworks.
We’re going to get our hands dirty to understand the inner-workings of frameworks. This will give you guidance on fixing esoteric build errors related to frameworks, and knowledge of how Swift and Objective-C interoperability works at the interface level. We’re going to build a framework without Xcode. Our good friend Libby is not impressed by this undertaking. Why? There is no reason to ever do this in practice. Xcode’s build process for frameworks abstracts most of the work of building a framework into Cmd-B. We do this here so that when you come up against compile time and link time errors, obscured by absolute paths, muttering about dyld, “Module not found”, “Framework not at path”, or my personal least favorite “Here’s your exit code and use -v to see invocation”, you won’t go to StackOverflow (which many times doesn’t help because so many scenarios can yield the same error), but instead tap into your knowledge of frameworks to deduce what is wrong, fix the problem, and get on with coding.
I recommend following along in your favorite shell and Finder to feel that rush when the handmade .framework appears as a functional framework bundle. To prove that the framework works, we’ll use an executable iOS project. Create an empty iOS project in Xcode called “The Shell”. We’re building a rowing app. We will develop a framework called “Coxswain”. The Coxswain is the person who sits in the rear of the boat who steers and coordinates the rowers rowing together. We need a coxswain to steer and make calls in the boat (also called a shell) before our shell can get moving on the water.
In Xcode’s project navigator, create a folder-backed group called Frameworks. On the command line cd
into that folder and type mkdir Coxswain.framework
. In Xcode, use the File -> Add Files menu to add your framework to the project. Your filesystem should have this structure
The Shell
├── Frameworks
│ └── Coxswain.framework
├── The Shell
│ ├── AppDelegate.swift
│ ├── Assets.xcassets
│ ├── Base.lproj
│ │ ├── LaunchScreen.storyboard
│ │ └── Main.storyboard
│ ├── Info.plist
│ └── ViewController.swift
└── The Shell.xcodeproj
and be mirrored in Xcode.
When you added the framework to the project, Xcode should have automatically added it to the Linked Frameworks and Libraries under the project settings. If we import Coxswain
in ViewController.swift and try to build, we get the error
❗️ No such module 'Coxswain'
Which makes sense. Our linked empty framework doesn’t include a single line of code, much less a module. Let’s make one. Create a directory called Scratch Space, but don’t add it to Xcode. Use your favorite text editor to create a swift file.
CoxCalls.swift
import Foundation
@objc public class Coxswain: NSObject {
public func steer(left: Bool) {
print("Steering (left ? "left": "right")")
}
@objc public func stroke(count: Int) {
print("Gimme (count) strong strokes!")
}
func talk(to seat: Int) {
print("Adjust your technique seat (seat)")
}
}
The name of this file doesn’t affect the module name, which is specified later.
We create a variety of basic functions and signatures to see how each is exposed in Swift and Objective-C interfaces. In Scratch Space, generate the interface files.
$ swiftc -module-name Coxswain -c CoxCalls.swift -target x86_64-apple-ios12.1-simulator
-sdk /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/
Developer/SDKs/iPhoneSimulator12.1.sdk
-emit-module -emit-objc-header -emit-objc-header-path Coxswain-Swift.h
You might not have the SDK version and simulator listed above. You should safely be able to substitute the version of iOS with which you are working into those path arguments.
This command creates the Swift module and the Objective-C header for our single source file. Let’s break down the arguments:
-module-name Coxswain
: The name of the module, what comes after import
(or @import
). Typically this will match the name of the framework.-c CoxCalls.swift
The source files to compile-target x86_64-apple-ios12.1-simulator -sdk /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator12.1.sdk
These are the target architecture and the SDK to link against. This will compile specifically for the iOS 12 simulator and won’t run on any earlier simulator. This will use the iOS 12 SDK, including the Foundation framework we linked to.-emit-module
will emit a .swiftdoc and .swiftmodule for the code we just wrote.-emit-objc-header
will emit an Objective-C header, including only those symbols marked @objc.-emit-objc-header-path
Notice that we choose to follow Xcode’s naming convention here for exposing Swift symbols to Objective-C by appending our framework name with -Swift.h.We know from Part 1 of this post that each generated file has a proper place in a
framework. We need to rename our .swiftmodule and .swiftdoc files created
by swiftc
to the architecture they represent and keep them in a bundle called Coxswain.swiftmodule.
There is little documentation on this structure. I reverse engineered it by looking
at frameworks created by Xcode. Change the prefix of both of the Swift module files
from Coxswain to x86_64 keeping each extension, then put them both in a folder
called Coxswain.swiftmodule. Copy this to a new Modules/ directory at the
root of the framework directory. Then copy the Objective-C header into a Headers/
directory in the framework. The new framework structure should be:
Coxswain.framework
├── Headers
│ └── Coxswain-Swift.h
└── Modules
└── Coxswain.swiftmodule
├── x86_64.swiftdoc
└── x86_64.swiftmodule
Xcode now knows about our symbols. Add let cox = Coxswain()
to ViewController.viewDidLoad()
in the project; notice that Xcode can autocomplete that class name. Try to build again in Xcode. No more module error! Instead we have a new one.
❗️ld: framework not found Coxswain
clang: error: linker command failed with exit code 1 (use -v to see invocation)
This error looks more intimidating. ld
and clang
have chimed in. When you see errors like this, take a breath and remember, you know what is going on! The module definition is there, and the compiler knows what symbols it should be finding (the Coxswain class and its public methods). The problem is, at link time, they aren’t there. Thinking back again to a framework’s structure, what is missing? The library! Let’s make that. In Scratch Space, compile the code (run this command with the -v flag to see interesting verbose output and additional flags that will be familiar from Xcode build logs).
$ swiftc -module-name Coxswain -parse-as-library -c CoxCalls.swift -target
x86_64-apple-ios12.1-simulator -sdk /Applications/Xcode.app/Contents/
Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator12.1.sdk -emit-object
We kept most of the arguments from the previous command, only now we pass -emit-object
to output an object file, and -parse-as-library
so the compiler interprets our file as a library, not an executable. An ls
reveals the new object file, CoxCalls.o. Drawing on knowledge from Part 1, we will archive this into the library format that Xcode expects. We’ll use libtool
which is intended to replace the classic Unix archive tool, ar.
$ libtool -static CoxCalls.o -arch_only x86_64 -o Coxswain
Normally, the extension for this file would be .a, but the default name Xcode expects in frameworks is the name of the module only, no extension. Without creating an Info.plist for our framework, we’re constrained to these defaults.
I’ve chosen to create a static framework, rather than the more common dynamically linked framework because it simplifies this step. For dynamic libraries, the location of the libraries used by the object files must be specified when the library is created; these include the Swift core libraries and the wrappers around the Objective-C runtime and Foundation. With a static library, Xcode will handle this linking when it builds the app.
Copy the Coxswain library into the framework bundle. The structure of our operational, artisanal, organic, free-range, grass-fed framework is:
Coxswain.framework/
├── Coxswain
├── Headers
│ └── Coxswain-Swift.h
└── Modules
└── Coxswain.swiftmodule
├── x86_64.swiftdoc
└── x86_64.swiftmodule
In Xcode, build the application. The compilation errors should be gone and you will be left with an unused variable warning in ViewController.swift where we added the cox
variable earlier. Add a method call to one of the framework’s methods to convince yourself that we were successful.
(1..<10).reversed().forEach { cox.stroke(count: $0) }
Now run the app and look in the console for the coxswain’s calls.
Gimme 9 strong strokes!
Gimme 8 strong strokes!
Gimme 7 strong strokes!
...
Congratulations! Look back at the files and folders you’ve created with some pride, this is not easy.
Throughout this process, we took care to make sure the framework would work with Swift and Objective-C code. Making frameworks work with both can be a delicate process. You must adopt modules if you want a hybrid project. When bringing old Objective-C code to a Swift project, this means setting the “Defines Module” (DEFINES_MODULE
) build setting to YES
in the framework target. This instructs Xcode to install a module.modulemap file (and possibly a module.private.modulemap) alongside the headers in the framework. Objective-C frameworks need to define a module to be used by Swift. Next, we put our framework to the test.
Add an Objective-C class to your app (allowing Xcode to create the bridging header for you). If you use the older #import <Coxswain/Coxswain-Swift.h>
syntax for including the framework, the project will build and compile and Objective-C will have access to the functions you declared public
and @objc
. The newer module import syntax @import Coxswain
however, will yield:
❗️Module 'Coxswain' not found
The .modulemap file is missing.
For our Objective-C code to import Coxswain as a module, we need to define a module map, which “describes how a collection of existing headers maps on to the logical structure of a module” (Clang Documentation: Modules). For the benefits of Clang modules and how they fit into Xcode, there is a useful WWDC 2013 session
According to the clang modules documentation, the module map language isn’t guaranteed to be stable yet, so it’s best to let Xcode generate these as well. Our framework structure is simple, so creating a module map isn’t bad (still, I did look to a pre-existing Xcode project’s build products for the structure and syntax of the file):
module.modulemap
framework module Coxswain { // Define a module with framework semantics
umbrella header "Coxswain.h" // Use the imported headers in the umbrella header to define the module
export * // Re-export all symbols from submodules into the main module
module * { export * } // Create a submodule for every header in the umbrella header
}
module Coxswain.Swift { // Define a submodule called Coxswain.Swift
header "Coxswain-Swift.h" // Use the imported headers to define the module
requires objc
}
Move this into the Modules directory within the framework, at the same level as Coxswain.swiftmodule.
An umbrella header is also required by the typical module map structure. It is possible to omit this header, but it complicates the module map. Here, we declare constants that clients of frameworks expect to be defined in the framework.
Coxswain.h
#import <UIKit/UIKit.h>
//! Project version number for Coxswain.
FOUNDATION_EXPORT double CoxswainVersionNumber;
//! Project version string for Coxswain.
FOUNDATION_EXPORT const unsigned char CoxswainVersionString[];
Move this into the Headers directory within the framework, at the same level as Coxswain-Swift.h.
We don’t strictly have to import the header Coxswain-Swift.h which declares the symbols
for the Coxswain framework. This is because we defined the Coxswain
module
to include the Coxswain.Swift
submodule which includes Coxswain-Swift.h in the modulemap. If
not for modules, we would have to import Coxswain-Swift.h.
The final structure of the framework on the filesystem is:
Coxswain.framework/
├── Coxswain
├── Headers
│ ├── Coxswain-Swift.h
│ └── Coxswain.h
└── Modules
├── Coxswain.swiftmodule
│ ├── x86_64.swiftdoc
│ └── x86_64.swiftmodule
└── module.modulemap
Objective-C classes in the project should now be able to access Coxswain.framework using modules. If you are tracking this demo project with version control, you might notice that Xcode helpfully set the build setting “Enable Modules (C and Objective-C)” (CLANG_ENABLE_MODULES
) to YES
when you introduced Objective-C into the Swift project.
You’re done! You have successfully created a framework. You have little reason to tremble at load, link, and compile time errors when working with frameworks and hybrid applications. The source code for the sample project can be found on GitHub with tags corresponding to each section above.
If you have read any of our Big Nerd Ranch Guides you have seen the For the More Curious sections. This is where you’re encouraged to dig deeper after completing a chapter. This post is getting a bit long, but I couldn’t resist including these questions that will solidify the concepts covered in this post and the previous one.
If you navigate to the built products directory (In the Xcode Project Navigator, right-click on your app and select Show in Finder) and look for signs of the Coxswain framework, you won’t find it. Why?
We declared the symbols CoxswainVersionNumber
and CoxswainVersionString
in
the framework’s umbrella header, but never defined them. Luckily, no code ever referenced
these symbols, so our app didn’t crash. Create a new framework project in Xcode.
Xcode creates a similar umbrella header. Find the definition (where the actual value
assignment is done) of the version number and string. A clue to where the
missing symbols are can be found in the build logs. Look for a file ending in _vers.c.
The DERIVED_FILE_DIR Build Settings Reference and our Understanding the Swift/Objective-C Build Pipeline posts may help.
If you feel comfortable with this process. Dive even deeper. We built our framework for the x86_64 architecture. This allowed us to build and run on the iOS Simulator in Xcode. If we switch the deployment target in Xcode to a real device and try to deploy our app, you’ll receive the error:
❗️file was built for archive which is not the architecture being linked (arm64): {absolute/path}/Coxswain.framework/Coxswain ld: warning: ignoring file {absolute/path}/Coxswain.framework/Coxswain
or perhaps
❗️'Coxswain' is unavailable: cannot find Swift declaration for this class
As we built the entire Framework, we know that we don’t have an object file or Swift module for a real device’s arm64 architecture, thus the compiler can’t find the Swift declaration for it. Compile CoxCalls.swift for the arm64 architecture. Use macOS’s libtool
and lipo
to build a fat binary that includes both the simulator and device architectures. If you make something you’re proud of, share your solution in the comments!
Our introductory guide to Swift Regex. Learn regular expressions in Swift including RegexBuilder examples and strongly-typed captures.
The Combine framework in Swift is a powerful declarative API for the asynchronous processing of values over time. It takes full advantage of Swift...
SwiftUI has changed a great many things about how developers create applications for iOS, and not just in the way we lay out our...