Cocoa-, Xcode-, and Swift-Specific Features
Selecting a kind of type
Prefer using Structs, Protocols, and Extensions more than classes and subclasses; Swift will behave more predictably and optimize better this way. It also allows for some patterns which are impossible with subclassing.
Documentation Comments
GOLDEN RULE If you are having trouble describing your API’s functionality in simple terms, you may have designed the wrong API.
Documentation comments are highly encouraged. We do not have any preferences of its style, so long as the doc comment is provided. You can view them in the Quick Help sidebar on the right of Xcode when the typing cursor is on the name, or by ⌥-clicking the name.
For reference, here are a couple examples of documentation comments:
Sparse Doc CommentAcceptable | GoodDetailed Doc Comment |
---|---|
|
|
Note that documentation comments are not to explain what something does in a way its name cannot, but instead to elaborate on what its name already says, including intended uses, parameter limits, possible return values, related code, external documentation, etc. Do not use documentation comments as an excuse to poorly name anything.
Also, be sure you know what something actually does before documenting it.
RULE-OF-THUMB Less documentation is better than incorrect documentation.
Good | Bad |
---|---|
|
ACTUAL EXAMPLE FROM CODEBASE 😱 |
See Also
Here’s a couple starting guides on how to write awesome documentation:
Numbers
By default, use Int
for integers and CGFloat
for fractions (or Length
if you have access to SwiftUI), as they automatically scale to the current processor architecture. If a specific task requires a specific number type like UInt8
or NSDecimalNumber
, then that’s OK, but for common operations, use those aforementioned types.
Optionals
In Swift, “optional” means “able to be nil
”. The great thing is that, in Swift, anything can be optional, even primitives like Int
s! This is a very powerful feature, and such power comes with a lot of responsibility.
Do Not Use Exclamation Points*†
”!
” means “I know this might be nil
, but I’m smart enough to know it isn’t nil
right now. It’s okay to crash if I’m wrong.” The problem here is that there is still a chance for crashing*, whereas there’s almost always another approach which does not provide a chance of crashing.
The following are preferred ways to unwrap an optional:
- The
?.
,?(
, and?[
operators. These somewhat mirror Objective-C’s behavior, meaning “If it’snil
, stop evaluating and returnnil
if anything expects a value. Either way, continue as normal.” - If the question-mark syntax does not accomplish the goal, or would be overly verbose for this context,
if let
(orif var
) is the next best option. This creates a new sub-context in which the existence of the value is guaranteed, but not before nor after. - If one or more values are required for a context (usually a function), or if the if let nesting is getting too deep,
guard let
(orguard var
) is preferred. This is much like an invertedif let
, in that it creates a sub-context in which the nonexistence of the value is guaranteed, the difference being that after that context, its existence is guaranteed. * These should go at the top of a context which requires the value, rather than farther down. - If you have a backup value handy, use
??
to use it as an alternative in case the ideal value doesn’t exist.
Good | Bad |
---|---|
|
|
* There is one exception where exclamation points are acceptable: On nib-initialized fields which are guaranteed to be available after load. If you are using SwiftUI, this exception does not apply.
† Obviously, using them to mean “Boolean invert” / “not” is okay (
!condition
).
Throwing functions
Another way to write a function that may or may not return a value is to declare it with the throws
keyword. This is perfectly acceptable, and in fact encouraged when it would lead to a more clear API. However, never return an optional value from a throwing function. Instead, where you would return nil
, throw a new, custom, descriptive error that tells why you couldn’t return a value. This is because, once the error has been handled, the API caller shouldn’t have to then deal with a nil
value, as from their perspective all errors that would cause that have been handled. Additionally, that approach wouldn’t play well with the try?
keyword.
Good | Bad |
---|---|
|
|
if
and guard
if
should never be used in place of guard
, and vice-versa. As its name implies, guard
should be used to guard against a bad state; if something is required, it acts as a guard that everything is OK before proceeding.
When entering a guard
’s else
block constitutes a bad state, use assertionFailure
(or at least log a message) when a guard
’s else
block was entered.
Do not use guard
or if
in place of each other! guard
is meant to be used as a guard against a bad state, not as an unless
-style statement.
extension
Extensions are the bread and butter of Swift and Cocoa. Always prefer creating an extension function over a utility function. Always prefer creating an extension over subclassing. It’s OK to make private extension
s if something is only needed in the current context and nowhere else.
GOLDEN RULE Always prefer extensions over subclassing.
Good | Bad |
---|---|
|
|
|
|
|
|
|
|
This is more natural. “I’m looking for a common URL; I will look in the URL class.” |
This is less natural. “I’m looking for a common URL; I will look in one of possibly several utility classes.” Equally bad is placing the constant in global scope, since it’s still not obvious how to find it, and it overpopulates autocomplete. |
// MARK:
Separate logical sections of code with // MARK:
and // MARK: -
messages. Use // MARK: -
more sparingly than // MARK:
.
When implementing a protocol (or subclassing) outside an extension
, make sure you place a // MARK:
message before the variables and functions you’ve implemented, making sure the mark’s message is the name of the protocol.
See also: Blank Lines
Good | Acceptable |
---|---|
|
|
Fancy var
s: get
, set
, willSet
, and didSet
In case you don’t know or need a refresher, here are the basics:
Swift
var
s are very powerful, much moreso than allowing a value to be re-set. There are three ways to use avar
:
- As a normal (unobserved*) instance, whose value can be set and gotten. The scope of these operations can be set individually, like
public private(set)
.- As an observed* instance whose changes are monitored just before andor just after the value is changed
- As a dynamic† value, whose setter and getter are specified by the developer. In this mode, it can also be read-only.
These are mutually exclusive; an unobserved instance cannot* report when it changes, an observed instance cannot have a dynamic† getter and setter, and a dynamic† value does not have a backing stored value.
It’s also important to know that a
var
in astruct
inherits the mutability and observation status of the instance in which it’s held. That is to say,let parentStruct
’s fields won’t be changeable, regardless of whether they’relet
s orvar
s. However,var parentStruct
’s fields will be changeable if (and only if) they arevar
s, but not if they’relet
s. Even better, ifparentStruct
has awillSet
or adidSet
, it will be notified if any of its fields changes.Do note that this behavior does not exist in
class
instances, even if those instances are inside astruct
(the best immutability those will get is not being able to change the reference; its own fields’ mutability won’t change). This is one reason why you should avoid usingclass
es as much as possible.
With the basics over, here’s the guidelines:
- Normal, unobserved*
var
s should be used only for values that need to change rapidly, but whose changes aren’t important. - Observed*
var
s should be used for important values, which should propagate other changes or send notifications when they change. - Dynamic†
var
s should be used in place of functions which only get or set a single value
Good |
---|
|
* Here, “unobserved” refers to lacking the built-in
willSet
andordidSet
blocks; these can still be observed using key-value approaches. Conversely, “observed” refers to the presence of these blocks and does not imply KVO.
† Here, “dynamic” refers to returning a generated, abstracted, translated, or conditional value (like a function). It does not refer to using the
dynamic
keyword for KVO.
Function Overloading and Default Parameters
In Objective-C, it was necessary to create multiple methods when you wanted one parameter to be optional. With Swift’s default parameters, this is no longer necessary.
Of course, overloading is okay if it is necessary for the current task.
Good | Bad |
---|---|
|
|
|
|
Function parameter labels
Sometimes it’s a good idea to label function parameters, and sometimes it’s not.
- Parameter labels may be omitted when the parameter reflects the main purpose of the function. For instance, the string message of a log function, the new value of a setter, or the two items in a two-item comparison function. This overrides the following guidelines.
- Boolean parameters must always have labels
- Unlike Objective-C, the first parameter can have a label separate from the method name. When possible, prefer this.
- When the value being passed in has a specific purpose other than just providing required data, try to give it a separate, more-English label than its variable name (not simple ones like
for
orwith
) - Do not provide a label for parameters whose name is in the function name
- When calling an external API which does not label its parameters, but leaves their purpose confusing, add “label” block-comments
RULE-OF-THUMB When in doubt, use a label.
Good | Bad |
---|---|
|
|
Mutability
Swift was designed around less-mutating, more-functional approaches first. In general, use approaches that create changed immutable copies, rather than ones that mutate an existing value. This may sound inefficient, but remember that the Swift compiler and runtime expect this to be the default approach, and will optimize for it.
If you run tests and find that using a non-mutating approach is too slow, then it’s okay to delicately choose which parts of your approach to convert to mutating. Use a scalpel, not a mallet when deciding what needs to be mutated. If you’re using Swift’s functional collection functions like map
and filter
, first try preceding these with .lazy
to turn it into a LazySequence
, whose performance is improved by waiting until the last moment to perform the operations. This can be even faster than using mutation.
This is much more expressive, safer, and often even faster than mutating.
Good | Bad |
---|---|
In my tests, this takes an average of ~82% as long as the mutating approach. |
In my tests, this takes an average of ~122% as long as the functional approach. |
Control Flow
One of the beautiful things Swift does is decrease the amount of control flow that’s needed by turning common patterns into production-ready functions in the standard library, called higher-order functions. When at all possible, prefer these functions and paradigms over traditional control flow.
Good | Bad |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
Enum Case Associated Values
See Also:
Associated values are a very powerful Swift paradigm, but there are many ways to use them. In our codebase, we prefer this approach:
- Always label the associated values
- When reading the values in a switch case, always give the reader variable the same name as the label
- When pattern-matching in a switch case, always use the label
- When pattern-matching in a switch case and ignoring the associated values, always write the label followed by an underscore, rather than just an underscore or omitting the associated values entirely
Good | Bad |
---|---|
|
|
NSCache
NSCache
is very useful when applied correctly. It cannot be used for all caching needs.
This is because NSCache
can and will evict things even if everything is under the limits.
In case you’re unfamiliar with NSCache
, it is a Cocoa class which allows you to give limits on both total number of items in that cache, as well as total cost (with each item optionally given a cost when it is cached). As previously mentioned, NSCache
only loosely respects these guidelines. For performance reasons, it might evict items even if no limits have been reached. Similarly, it might not yet have evicted items well after its limits have been passed.
Never use NSCache
to cache values which cannot be recreated (for instance, items with UUIDs)!
If your requirements are okay with this looseness, then NSCache
might be the right tool for the job. Otherwise, you should probably write your own cache which is specialized to fit those requirements.