When I first started working in Swift, I assumed it was like an Objective-C clone with cleaner syntax. I was wrong.
Swift has several idiosyncrasies and potential pitfalls you need to be aware of — whether your background is in Objective-C or some other language. Here are ten lessons I’ve learned about what sets Swift apart.
1. There are more to function names than you may think.
One of the things I always appreciated about Objective-C is that method invocations have their arguments interspersed into the name of the method. If you’re not familiar, consider this example:
[array indexOfObject:myObj inRange:NSMakeRange(1,5)]
This code is really nice to read. It’s very clear about what parameters it accepts, for what purpose, and in what order.
That’s a matter of personal preference, though. I’ve talked to many people who find this syntax odd or even infuriating. That may just be because it’s outside the norm; I’ve heard similar complaints about Lisp.
Regardless, an explicit goal of Swift is interoperability with Objective-C. Therefore, Swift makes a compromise to support this. In Swift, the name of a function or method includes the “public” name of the arguments that are passed to it. That’s to say, the following methods are discretely identified:
numbers(excluding:)
and
numbers(including:)
The labels of the parameters help identify the function, and you must include them when calling the function, e.g., numbers(excluding:45))
.
Note that the label names can be different from the identifier used in the definition of the method. The following examples are indistinguishable:
func numbers(excluding: Int) -> [Int] {
return self.filter { n in n != excluding }
}
function numbers(excluding number: Int) -> [Int] {
return self.filter {n in n != number }
}
Sometimes it doesn’t make sense to label your parameters, and Swift supports this. You can use a label of _
:
function numbersExcluding(_ number: Int) -> [Int] {
return self.filter {n in n != number }
}
Incidentally, you can also use this as the identifier of parameters that you don’t care about. There won’t even be a warning if you “reuse” the _
for multiple identifiers. The underscore is not quite a language feature but still more than mere convention.
Here’s an example:
myNumbers.map { _ in
7
}
2. Scoping is based on “modules.” But you can nest your types!
In Swift, all of your types, functions, structs, classes, etc. are scoped to the module that they are defined in.
In a lot of other languages, the file that your code is defined in provides some level of namespacing. Not so with Swift. Instead, it’s based on the “module,” which (roughly) is either the Application or Framework.
As a practical matter, this means that you do need to be concerned with reusing the same identifier in any of the source files that compose your application (or framework). For example, it’s not good practice to define a helper function within a file without recognizing that any other file in your application could refer to that function. No import needed.
What can you do about this? Well, Swift provides the ability to nest types. Within any class, struct, etc., you can nest the definition of another type. Its identity includes the identity of the type it’s defined within.
Here’s an example:
struct Thing {
enum Kind {
case Thing1
case Thing2
}
var kind: Kind
}
let aThing = Thing(kind: .Thing1)
let otherKind: Thing.Kind = .Thing2
It’s no accident that this feature exists. It becomes an important tool in organizing your code, especially as the size of your codebase grows.
3. Prefer structs over classes.
I almost didn’t write about this because it’s been covered so extensively elsewhere, but it’s important. In Swift, you should prefer using structs over classes.
Why? And what’s the difference? Structs are value types. If you pass them into another function or return them from a function, they are copied. Classes, on the other hand, are a reference type; they’re passed around by pointer.
Do note that there are legitimate reasons to use reference types. When you have such a reason, please don’t use a struct. Use a class. Classes are not inherently evil.
4. Mutability is a different paradigm than you may expect.
I have a strong background in both Objective-C and Clojure. As a result, I’m extremely conscious of the mutability of data. In particular, I’m biased toward leveraging immutable data whenever possible.
In my initial experiences in Swift, this led to me preferring the use of let
over var
. After all, I don’t want anyone to mutate my data. (It’s really frustrating to switch between Swift, which sensibly uses let
to declare to a constant, and “modern” JavaScript, which uses let
to declare to a variable. I wish I had a dollar for every time I typed const
in Swift.)
With structs, this is mostly unnecessary. As value types, structs are allocated on the stack. Unless you pass a pointer of your struct to another function (see inout parameters in the Swift language guide), it’s not possible to call another function and have it mutate your local var
struct. This is because they receive a copy of your struct.
So use structs and rely on the value type semantics to protect yourself from other code mutating your local data. By default, declare the fields of your structs to be var
s. Only if it doesn’t make sense for that field to ever change should you reach for let
.
For those familiar with Objective-C, here are some useful heuristics for thinking about mutability:
- When you’re passing a struct into a function, it’s always as if you had called
copy
on an object in Objective-C first. - If you’re receiving a struct from a function invocation, declaring your local as
var
is the same as having calledmutableCopy
on the corresponding Objective-C object. - If you’re declaring the local using
let
, it’s the same as callingcopy
on the object returned from the function you called.
5. Enums can have values associated with their cases, but you have to unwrap them.
Enums are fundamental and pervasive. They are roughly equivalent to Types in Haskell and Discriminated Unions in TypeScript. And like these, they may have associated values.
Let’s consider a definition of Haskell’s Either type in Swift:
enum Either<A,B> {
case Left(A)
case Right(B)
}
Additionally, consider a concrete usage:
let scanned: Either<ISBN, Stock> = scan(...)
Given this code, you know that scanned
is an ISBN or a Stock. In Swift, you have two tools to unwrap the value: switch
and if case
.
switch
Like C, Swift provides a switch statement. However, there are some key differences.
- The switch statement must be exhaustive by default (that is, you cannot leave a possible case unspecified).
- There is no fallthrough, so you don’t need to worry about using
break
.
The syntax also allows you to bind names for the associated values. So if you wanted to identify the book differently, you could do something like this:
var book: Book?
switch scanned {
case .Left(let isbn):
book = process(isbn: isbn)
case .Right(let stock):
book = process(stock: stock)
}
What if you have two cases that you’d like to handle in the same way? This is possible, but only if the cases you’d like to handle all have the same type of associated values.
switch x {
case .A(let n), .B(let n):
print("x was A or B, with value", n)
case .C, .D:
print("x was C or D")
}
if case
What if you’re only concerned with a particular case of an enum? Swift does support this. As long as you literally only care about one case (and not a subset of cases), you can use if case
. Similar to the switch statement, you can also choose to identify the associated value.
var book: Book?
if case .Left(let isbn) = scan() {
book = lookup(isbn: isbn)
}
For whatever reason, it’s hard to Google for this or otherwise find this bit of information within the Swift Language Guide. So there you go.
By the way, Swift’s optional type is defined in terms of an enum. It just has special syntax support.
6. Type Casting + if let
is a useful pattern for UIKit APIs.
The legacy of Objective-C API design means that type casting is a very common occasion.
Let’s say you’re writing a view controller and implementing the prepareForSegue:sender:
handler. In order to do anything useful, you’ll need to constrain the type of the segue’s destination controller. Swift provides two features whose confluence is perfectly suited to this scenario: if let
and as?
. For example:
if let bookScanner = segue.destination as? BookScannerController {
...
}
This is a helpful pattern, and you’ll likely use it a lot. It’s definitely much nicer than the things you had to do in Objective-C.
7. You do need to worry about strong references, but there’s a nice syntax for weak references.
Just as in Objective-C, there is no garbage collector in Swift. Actually, there was once a garbage collector for Objective-C, but it was deprecated because (give your author some artistic license here) the performance characteristics of garbage collection don’t mesh well with smooth animations.
As a practical concern, if you have object A that has a reference to B, and B has a reference to A… congratulations, you have leaked memory. I suspect that Swift retains enough semantic flexibility that this may someday change. For now, however, there is no escaping the fundamental understanding that Swift is reference counted.
In general, this is a design problem, but there is one specific syntactical feature that I wanted to touch upon. Closures in Swift have some very nice support for marking identifiers as weak.
object.doThing { [weak self, weak otherThing] in
if let otherThing = otherThing {
...
}
}
When defining a closure, before you define the parameters to the closure, you can use syntax like the above to mark specific closed-over references as weak. In return, a strong reference to those identifiers is not retained. When (if ever) the closure is invoked, the identifiers will be appropriately marked as Optional, and you must check for and handle their (lack of) existence.
8. There are @testable imports that allow you to access private symbols.
Testing private details of your implementations appears to be an evergreen challenge. Fortunately, Swift provides a decorator for importing “private” concerns: @testable import MyApp
.
When you import your application in tests using this decorator, you’ll have access to all the private types and methods and will be able to easily test them.
9. Methods can be lifted off structs and classes as lambdas.
Unlike Objective-C, Swift can treat methods as simple functions. In Objective-C, when you wish to pass a reference to a method for an object, you must pass both the instance of the receiver and an abstract representation of the message that will be passed to the receiver.
In Swift, this isn’t true because methods are simply (function) references. Given that all functions are references (think Blocks in Objective-C), all methods can be treated as functions. Consider the following code:
struct Detail {
...
var name: String
func describe() -> String {
...
}
}
let myDetail = Detail(...)
Aside from actually calling myDetail.describe()
, there are two ways that you could refer to the method as a value and pass it around.
Detail.describe
myDetail.describe
(or deref through any other instance of Detail)
What’s the difference? In the latter, you get a fairly simple function of () -> String
. But in the former, you get a function of Detail -> () -> String
.
You could conceivably call Detail.describe(myDetail)
as a function: Detail.describe(myDetail)()
. But you could also pass it around to other functions without any of the shenanigans of identifying both the receiver and an abstract concept of a message that could conceivably be sent to it.
Naturally, a strong reference will be retained to the receiver, and you’ll need to be cognizant of that.
10. Trailing closures feel like the power to declare new syntax.
Obviously, Swift is not Lisp, and you cannot possibly create your own syntax. Nevertheless, Swift does give special syntactic treatment to the last argument to a function when that last argument is a function/block/closure.
In the case where your function doesn’t even accept any arguments besides the closure, you don’t have to include any parentheses at all.
Let’s consider this function:
func isItTrue(pred: () -> Bool) -> Void {
if pred() {
print("yes!")
} else {
print("no.")
}
}
// the following invocation...
isItTrue { false }
// is equivalent to this invocation:
isItTrue(pred: {
false
})
This syntactic sugar is pretty cool. It allows you to blur the lines between simple function invocations and control structures.
This feature is rife for DSL building. When used tactfully, it can lead to more expressive code.
Conclusion
Swift is, in my opinion, a rather ugly language. There are a number of questionable design decisions, for example:
- Why does Swift even have statements? Why isn’t everything an expression? It’s a shame they missed that opportunity.
- Why is there so much syntax surrounding enums? They borrowed a good concept but made it arduous and painful to use.
Despite these, Swift is still an enjoyable language to use. I greatly prefer it to, say, TypeScript, which has some novel advantages of its own.
I hope that what I’ve outlined here is a useful resource. I’ve barely scratched the surface of what makes Swift unique.