15 Tips to Become a Better Swift Developer

Nathan Hillyer

Last updated Jul 23, 2019

Since its 1.0 release in September 2014, Savvy Apps' development team has been experimenting with and using Swift in iOS projects. Though the majority of our current projects remain in Objective-C because of the relative youth of Swift, we've started coding new projects in Swift since its 2.0 release in September 2015. Keep in mind that most of our work with Swift is for iOS apps as it stands in January 2016.

Swift adds language features that make developers' code safer, faster, more reliable, and more predictable compared to Objective-C. Below we’ve outlined some of the Swift tips we've learned throughout our experience using this new language. They've helped us code more cleanly while aiding developers who are more familiar with Objective-C acclimate to Swift better. We’ve provided tips for various backgrounds and experience with Swift. Head to the relevant section and enjoy.

The sections below are organized by how familiar you are with Swift. The first section is about tips you may not know, the second is for those getting started, and the last is for people already using it.

Swift Tips You May Not Know But Should

Improve the Readability of Constants

A neat way to utilize structs in Swift is to make a file for all the constants in our application. This is useful because Swift allows us to nest structures like so:

import Foundation

struct Constants {

    struct FoursquareApi {
        static let BaseUrl = "https://api.foursquare.com/v2/"
    struct TwitterApi {
        static let BaseUrl = "https://api.twitter.com/1.1/"

    struct Configuration {
        static let UseWorkaround = true

This nesting gives us a namespace for our constants. For example, we could use Constants.FoursquareApi.BaseUrl to access Foursquare's BaseUrl constant. This makes things more readable and provides a layer of encapsulation around related constants.

Avoid NSObject and @objc to Improve Performance

Swift allows us to extend classes from NSObject to get Objective-C runtime features for an object. It also allows us to annotate Swift methods with @objc to allow the methods to be used by the Objective-C runtime.

Supporting the Objective-C runtime means that method calls are going to be using dynamic dispatch instead of static or vtable dispatch. This end result is that methods that support the Objective-C runtime have a four times performance penalty when called. In practical usage, the performance hit may be negligible but the cool thing is that armed with this knowledge, we now know that method execution in Swift is four times faster than Objective-C.

Use Method Swizzling in Swift

Method swizzling is a technique that substitutes one method implementation for another. If you're not familiar with swizzling, check out this blog post. Swift optimizes code to call direct memory addresses instead of looking up the method location at runtime as in Objective-C. So by default swizzling doesn’t work in Swift classes unless we:

  • Disable this optimization with the dynamic keyword. This is the preferred choice, and the choice that makes the most sense if the codebase is entirely in Swift.
  • Extend NSObject. Never do this only for method swizzling (use dynamic instead). It’s useful to know that method swizzling will work in already existing classes that have NSObject as their base class, but we're better off selectively choosing methods with dynamic .
  • Use the @objc annotation on the method being swizzled. This is appropriate if the method we would like to swizzle also needs to be exposed to Objective-C code at the same time.

Update: As requested, we've added an example of swizzling purely in Swift. It still requires the Objective-C runtime, but our class doesn’t inherit from NSObject and our methods are not marked as @objc. Click here to see it in action in a playground.

import UIKit
class AwesomeClass {
    dynamic func originalFunction() -> String {
        return "originalFunction"
    dynamic func swizzledFunction() -> String {
        return "swizzledFunction"
let awesomeObject = AwesomeClass()
print(awesomeObject.originalFunction()) // prints: "originalFunction"
let aClass = AwesomeClass.self
let originalMethod = class_getInstanceMethod(aClass, "originalFunction")
let swizzledMethod = class_getInstanceMethod(aClass, "swizzledFunction")
method_exchangeImplementations(originalMethod, swizzledMethod)
print(awesomeObject.originalFunction())  // prints: "swizzledFunction"

Swift Tips for Those Getting Started

Clean Up Asynchronous Code

Swift has a neat syntax for writing completion functions. We had completion blocks in Objective-C, but they came late in the language’s development and had some gnarly syntax, as we see here:

[self loginViaHttpWithRequest:request completionBlockWithSuccess:^(LoginOperation *operation, id responseObject) {
  [self showMainScreen];
} failure:^(LoginOperation *operation, NSError *error) {
  [self showFailedLogin];

Swift makes this a lot simpler with the new closure syntax. Any method with a closure as its last parameter can use Swift’s new syntax to make callbacks look much cleaner, as seen here:

loginViaHttp(request) { response in
  if response.success {
  } else {
Control Access to Our Code

We should always be using the appropriate access control modifier to encapsulate code. Good encapsulation helps us understand how pieces of code interact without having to remember our thought process or ask the developer who wrote it.

Swift has common access control mechanisms of private, internal, and public, but Swift doesn’t have the protected access control modifier that is common in other Object-oriented programming languages. Why is this? Well, protected doesn’t gain us any protection because a subclass could expose a protected method or property with a new public method or property. protected also doesn’t open up any optimization opportunities for the Swift compiler as new overrides can come from anywhere. Lastly, protected encourages poor encapsulation because it prevents the subclass’s helpers from accessing information to which the subclass has access. You can read more about the Swift team’s thoughts on protected here.

Experiment and Validate with Playgrounds

A playground is an interactive coding environment for Swift. We can create playgrounds to test and validate ideas, learn Swift, and share concepts with each other. This can all be done without needing to create a new project. To create a playground simply choose it as an option on Xcode launch.

You can also create a new playground from inside Xcode:

Once we have a playground, all we have to do is start coding and we’ll see the results on the right:

Playgrounds are a great way to prototype and demonstrate ideas in code without the overhead of starting a project.

Use Optionals Safely

An optional property is a property that can have a valid value or have no value (nil). We can implicitly unwrap an optional by following the optional's name with an exclamation point, as in optionalProperty!. This is generally something you want to avoid, as hinted at by the exclamation point meaning “danger!”

There are some cases where using an implicitly unwrapped optional is acceptable. IBOutlets, for example, default as implicitly unwrapped optionals (when we click and drag them from Interface Builder) because UIKit assumes we have hooked up our outlets to Interface Builder. The outlets are optional because IBOutlets are set at some point after initialization, and all non-optional properties must have values after initialization according to Swift rules. Another case is getting a UIImage by name that we know exists in our asset catalog:

let imageViewSavvyNewYearsParty = UIImageView(image: UIImage(named: "Savvy2016.png")!)

This is setting the default value of a constant property and wouldn't be possible without the implicitly unwrapped optional. That said, ! still means “danger!” and, in this case, is telling us to be careful about typos and verify that the name matches before running. In general, if we have to use nil values, we risk causing our app to crash. Implicitly unwrapping values with ! tells the compiler that we know the optional will not be nil at runtime. This is a risky gamble in almost all scenarios, so it’s better to use the if let pattern to determine if an optional has a valid value or is nil:

if let name = user.name {
} else {
    print("404 Name Not Found")
Leave NSNumber Behind

Objective-C used C primitives for numbers, and the Foundation Objective-C API provided the NSNumber type to box and unbox primitives. We had code that looked like [array addObject:@(intPrimitive)] and [array[0] intValue] when we needed to go back and forth between primitives and object types. Swift doesn’t have this awkward mechanism. Instead, we can actually add Int / Float / AnyObject values to Swift dictionaries and arrays.

Here are the most common Swift types used in place of NSNumber:

  • Swift: Objective-C
  • Int: [NSNumber integerValue]
  • UInt: [NSNumber unsignedIntegerValue]
  • Float: [NSNumber floatValue]
  • Bool: [NSNumber boolValue]
  • Double: [NSNumber doubleValue]

We also used NSNumber for conversions between different types in Objective-C, but in Swift the idiomatic way of converting values is using a constructor for the type we are targeting. For example, if we got a numeric userId from an API, wrapped it in an NSNumber, and needed to display the userId as a string, in Objective-C we typed [userId stringValue]. Swift doesn’t use NSNumber anymore (except for backwards compatibility with Objective-C), because numeric structures in Swift don’t have the same limitations as Objective-C.

Disclaimer: We may have to work with NSNumber in codebases that bridge Objective-C or codebases that depend on older libraries without Swift wrappers. In that case, the NSNumber API remains largely unchanged.

Instead, in Swift we do the equivalent conversions with constructors. For example, if userId is an Int and we want a String, all we have to do is String(userId) to make the conversion. This is much easier than boxing and unboxing NSNumbers all the time, but the variety of conversions provided by NSNumber did provide a nice and simple API.

Reduce Boilerplate Code with Default Arguments

With Swift, function arguments can now have default values. These default arguments reduce clutter. If the callee of the function chooses to use the default values, the function call is shorter because the default arguments can be omitted. For example:

func printAlertWithMessage(message: String, title: String = "title") {
   print("Alert: \(title) | \(message)")

printAlertWithMessage("message")                                // prints: Alert: title | message

printAlertWithMessage("message", title: "non-default title")    // prints: Alert: non-default title | messagex

Swift Tips for the More Adept

Validate Methods with Guard

Swift's guard statement helps clean up code and make it safer. A guard statement checks one or more conditions and if any of the conditions are not met an else block is called. The else block requires a return, break, continue, or throw statement that ends the method's execution, or in other words, transfers program control out of scope.

We use guard statements to reduce code clutter and avoid nested if/else statements. Since code in the else block of a guard statement must transfer program control out of scope, it is safer than simply using an if statement that calls return statement if an invalid condition is met. These potential bugs can now be caught at compile time. If the conditions of the guard statement pass, optional values that were unwrapped are available within our scope.

class ProjectManager {
    func increaseProductivityOfDeveloper(developer: Developer) {
        guard let developerName = developer.name else {
            print("Papers, please!")
        let slackMessage = SlackMessage(message: "\(developerName) is a great iOS Developer!")
Manage Program Control Flow with Defer

A defer statement defers execution of the code contained in its block until the end of the current scope. What this means is that cleanup logic can be placed inside a defer statement, and it is guaranteed to be called when the current function leaves scope. This can help reduce redundant steps, and more importantly, increase safety.

func deferExample() {
    defer {
        print("Leaving scope, time to cleanup!")
    print("Performing some operation...")

// Prints:
// Performing some operation...
// Leaving scope, time to cleanup!
Simplify Singletons

The use of singletons in any language is a hotly debated topic, but it remains a pattern that is familiar to most developers. The implementation of a singleton in Objective-C involves several steps to ensure that the singleton class cannot be created more than once. Swift greatly simplifies this. As we see below, the Objective-C implementation takes more than twice as many lines as the Swift implementation. On top of that, its use of a dispatch token not only makes it less readable but also more difficult to commit to memory.


@implementation MySingletonClass
    +(id)sharedInstance {
        static MySingletonClass *sharedInstance = nil;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            sharedInstance = [[self alloc] init];
        return sharedInstance;


class MySingletonClass {
    static let sharedInstance = MySingletonClass()
    private init() {
Reduce Duplicate Code with Protocol Extensions

We can extend existing types with categories in Objective-C, but the same affordance isn't given to protocols. Swift allows us to add functionality to a protocol. With Swift we can extend a single protocol (even ones in the standard library!), and have it apply to any class that implements the protocol. There is enough raw power in protocol extensions to change our entire programming paradigm from object- to protocol-oriented. The meat of this concept is that we default to adding functionality through protocols, instead of classes, to make our code more reusable. To learn more about protocol-oriented programming, check out one of our favorite WWDC 2015 videos.

Create Global Helper Functions

Global variables and functions are often conflated as a “bad thing,” but the truth is that both can help us make clean code. What’s truly a bad thing is global state. Global functions often need global state to do their work, so it’s easy to see why they get such a bad rap. Here are some examples of Grand Central Dispatch helper functions that don’t operate on global state and are more or less syntactic sugar. Here we'll take the dispatch_after function and wrap it in a Swifty way:

import Foundation

    Executes the closure on the main queue after a set amount of seconds.

    - parameter delay:   Delay in seconds
    - parameter closure: Code to execute after delay
func delayOnMainQueue(delay: Double, closure: ()->()) {
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(delay * Double(NSEC_PER_SEC))), dispatch_get_main_queue(), closure)

    Executes the closure on a background queue after a set amount of seconds.

    - parameter delay:   Delay in seconds
    - parameter closure: Code to execute after delay
func delayOnBackgroundQueue(delay: Double, closure: ()->()) {
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(delay * Double(NSEC_PER_SEC))), dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), closure)

Here's an example of how our new wrapper function looks:

delayOnBackgroundQueue(5) {

Here's an example of how the unwrapped function looks:

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(delay * Double(NSEC_PER_SEC))), dispatch_get_global_queue(QOS_CLASS_UTILITY, 0)) {

Wrapping C functions with Swift syntax makes our code easier to understand at a glance. Find your favorite function and give it a shot! Future maintainers will be grateful as long as we remain responsible about naming the methods accurately. If we changed the above method signature to delay(delay: Double, closure: ()->()), it could be an example of an irresponsible method name because dispatch_after requires a GCD queue, and the name doesn’t give us any indication of which queue is being used. However, if the codebase we're working in has an established convention of executing all methods on the main thread unless otherwise indicated in the name or comments, delay(delay: Double, closure: ()->()) would be an accurate method name. Regardless of what we name our helper functions, their purpose is to save time by wrapping boilerplate code and make our code easier to read.

Expand our Collection Capabilities

Swift adds some methods that help us query and modify collections with brevity. These collection methods are inspired from functional languages. We use collections to save multiple values into a single data structure, and very often we query and modify collections. These functions are built into Swift's standard library and help simplify common tasks. To help explain the below functions, we'll use the following example: let ints = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].

  • map applies a closure to each value in the collection then returns an array of the map result type filled with the mapped values. Here we transform our Int array into a String array:
let strings = ints.map { return String($0) }
print("strings: \(strings)") // prints: strings: ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
  • filter applies a function to each value in our array and returns a Bool value. Values that return true are returned in the resulting array while values that returned false are not. Here we filter odd numbers from the ints array:
let evenInts = ints.filter { return ($0 % 2 == 0) }
print("evenInts: \(evenInts)") // prints: evenInts: [0, 2, 4, 6, 8]
  • reduce is more complex than map and filter, but the usefulness of reduce makes up for the learning curve. The first argument is the first reduced value ( 0 in the case below). Its second argument is a function that has access to the previously reduced value and the current value of the array. In this example, our function is + which simply adds the previous value of the function to the current item in the array.
let reducedInts = ints.reduce(0, combine: +)
print("reducedInts: \(reducedInts)") // prints: reducedInts: 45

// defined another way: 

let reducedIntsAlt = ints.reduce(0) { (previousValue: Int, currentValue: Int) -> Int in
    return previousValue + currentValue
print("reducedIntsAlt: \(reducedIntsAlt)") // prints: reducedIntsAlt: 45

Armed with the map, filter, and reduce tricks, we can reduce the amount of effort needed to filter and process collections while increasing readability for future maintainers!

Concluding Note

We compiled this list from the most common tips proposed by our team, and many of them have become standard across our codebases. As Swift evolves as a programming language, tip lists like this will grow too. We look forward to seeing how Swift changes in the coming year and are excited to use more of the language in our apps. If you're interested in seeing similar developer advice, check out our blog post of Xcode tips, and hit us up on Twitter @savvyapps with your own tricks of the dev trade.

Written By:

Nathan Hillyer

Nathan is a full-stack developer who works primarily on mobile apps. He's passionate about education and empowering people to take learning into their own hands.