In no particular order, here are a few intriguing language features and possibilities I’ve come across over the last few years. Some might seem obvious, some you’ll never need to use… hopefully there’s one or two things here that you find useful or interesting.
1. Custom Subscript Notation
Dictionaries and arrays use square brackets to simplify access. It is quite possible to declare your own subscript accessors too:
class MovieCinemaSeatModel {
subscript (row row: Character, column column: Int) -> MovieTicket? {
// Return logic goes here. Note the doubled parameter names -
// this forces callers to label their arguments.
}
subscript (ticketId: String) -> MovieTicket? {
// This subscript will not require a parameter name when called.
}
}
let ticketBySeat = movieSeating[row: "C", column: 3]
let ticketById = movieSeating["1234"]
By the way, the Swift standard library contains more subscript patterns that you might realise, like the default value variant of the usual Dictionary
accessor.
2. Variadic Parameters
Although arrays can be used instead, a variable number of function arguments can be expressed via ellipses. This results in slightly nicer call site notation, with the caveat that ordinary arrays cannot be used to fulfill a variadic parameter.
func purchaseMovieTickets(for showings: Movie...) {
/// showings can be treated as an array internally.
}
func refundMovieTickets(_ ticketIds: [String]) {
/// Unlike the former, this function will accept arrays
/// at the call site.
}
purchaseMovieTickets(for: showingOne, showingTwo)
refundMovieTickets([ticketIdOne, ticketIdTwo])
3. Single Line Guard Statements
Often guard
statements are chained, or styled so that the else
scope spans three lines. However, single line guard
statements can be a very clear, sequential way of structuring code.
func findNextFilmShowtime() -> Date? {
guard let films = getFilmList(orderedBy: .showtime) else { return nil }
guard let nextMovie = films.first else { return nil }
return findShowtimes(for: nextMovie).first
}
4. Functions as Variables
As in languages like Javascript, Swift functions can be treated like variables - and this doesn’t just apply to any declared closures.
func displayDate(fromNow: Bool) {
let dateProducer = fromNow ? Date.init : findNextFilmShowtime
prepareAndThenDisplay(dateProducer)
}
5. if case let
Statements
It is possible to match against a single Swift enum (as opposed to switching through each case as is usual). This can be done using an if case let
statement.
let movieRating = MovieRating.restricted(age: 16)
if case let .restricted(_) = movieRating {
// This will run. You can enter associated values
// to match at a more fine grained level too.
}
6. Indirect enum Cases
Swift enums can associate types with each case (this is an incredible useful feature itself). However, sometimes you might wish to associate enums in a recursive way - one common use case might be in a node like data structure. This presents an impasse to the compiler: it needs to know how much memory to allocate to each enum case, and a recursive relationship could result in an infinite amount of memory being allocated.
This problem would be quite solvable by wrapping recursively associated values with a thin wrapper type, effectively providing a layer of indirection to the compiler. However, Swift allows us to do this without any wrapper type trickery needed.
enum MovieGenre {
case action, romance, drama
indirect case comedy(subtype: MovieGenre?)
}
7. Direct Usage of Optional Types
Swift optional types are implemented in the language itself. An optional is just an enum with two cases: .none
, and .some(Any)
. You can directly reference these types, although it isn’t very often that this is actually useful.
if Optional(MovieGenre.action) is Optional<MovieGenre> { print("It certainly is") }
switch Optional<Bool>.init(nilLiteral: ()) {
case .some(_): break // This won't run
case .none: break // This will
}
8. Overriding via Extensions
Extensions can not only extend a class - they can override methods too. I’d suggest thinking twice, or perhaps many more times before doing this… modifying classes in this way can result in behaviour that is very tricky to track down.
extension UITableViewController {
open override func viewDidLoad() {
// Is this really such a good idea...?
}
}
9. Lazily Instantiated Variables
Declaring a non optional property, with a dependancy on self
(perhaps as a delegate) can prove to be tough. The lazy keyword can help here:
class MovieRatingView: NetworkResponseDelegate {
lazy var networkProvider = { [unowned self] in
// Unowned prevents retain cycles. Weak is not
// necessary since networkProvider cannot be created
// after the owning self is deinitialized
return NetworkProvider(responseDelegate: self)
}()
}
10. Exact Equivalence Operator
Some things can be more equal than other things… under the hood, this checks that objects not only match (via their Equatable
conformance), but that the underlying memory address is identical.
let this = Movie(id: 123456)
let that = Movie(id: 123456)
print(this == that) // true
print(this === that) // false
print(this === this) // true
11. Loops can be Labelled
It’s not often you need to do this, but in confusing situations, labels can make the intent of your code very clear.
outerLoop: for tvShowId in 0...10000 {
innerLoop: for seasonNumber in 0...100 {
guard let show = TVShow(id: tvShowId) else { continue outerLoop }
guard let season = TVShow.Season(number: seasonNumber) else {
break innerLoop
}
}
}
12. Unsafe Operators
Unlike some other languages, Swift isn’t so naïve about manipulating numeric types outside the range they support. In fact, bounds errors result in a runtime crash. However, sometimes you might wish to keep the unsafe, bound wrapping behavior - you can do this with unsafe operators. Most numeric operators have unsafe equivalents, found by prefixing the safe operator with an &
symbol.
Int.max + 1 // Int overflow runtime error
Int.max &+ 1 // Int.min
13. Private Setters
Sometimes, you may wish for a property to be publicly readable, but only privately writable. You can do this!
class Actor {
let id: Int
private(set) var stageName: String
init(id: Int) { stageName = getStageName(forActorId: id); self.id = id }
func updateProperties() { stageName = getStageName(forActorId: id) }
}
let andySamberg = Actor(id: 99)
print(andySamberg.stageName)
andySamberg.stageName = "Andrew" // Compile error
14. Functions that Don’t Return
This is a special return type, signalling to the compiler that the method will never return.
func forceCrash(forReason reason: String) -> Never {
print(reason)
abort()
// Note the lack of a return statement
}
15. Closure Capture Blocks
Variables used inside a closure are evaluated at the time that the closure is run. If you’d like to evaluate them at the time the closure is declared, this is quite possible using a closure capture block.
class MovieCollectionViewCell: UICollectionViewCell {
private class ReuseIdentifier {}
private var coverImageReuseIdentifier: ReuseIdentifier?
func displayCoverImage(with url: URL) {
coverImageReuseIdentifier = .init()
URLSession.shared.dataTask(with: url) { [weak self,
coverImageReuseIdentifier] data, response, error in
// Prevent cell reuse image issues. This is one
// way of doing it... (there are plenty of other ways too).
// The reuse identifier is captured when the closure is
// defined, allowing us to compare it with the non captured
// variant - to check that the cover image hasn't been updated
// in the meantime.
guard self?.coverImageReuseIdentifier
=== coverImageReuseIdentifier else { return }
}
}
}