Swifty Tips ⚡️

Subtle best practises that Swift developers are keeping secret.

When I first started iOS development, I was always curious about best practices used by giant companies. How does their project structure look like? What architecture are they using? Which third party libraries are the most popular? This was my urge to build upon other people’s experience and don’t waste time on problems that are already solved.

It has been 4 years since. I worked with many clients and had many smart people on my team to discuss about these coding practices. So in this post, I want to talk about the not-very-obvious practices that I am using right now for iOS development.

You are more than welcome to use them, criticize them or improve them.

Let’s begin.🚀

1- Avoid overusing reference types

You should only use reference types for live objects. What do I mean by “live”? Let’s look at the example below.

struct Car {
  let model: String
}

class CarManager {
  private(set) var cars: [Car]
  func fetchCars()
  func registerCar(_ car: Car)
}

🚗 is just a value. It represents data. Like 0. It’s dead. It doesn’t manage anything. So it doesn’t have to live. There is no point defining it as a reference type.

On the other hand;

CarManager needs to be a live object. Because it’s the object that starts a network request, waits for the response and stores all fetched cars. You cannot execute any async action on a value type, because, again, they are dead. We expect to have a CarManager object, which lives in a scope, fetches cars from server and registers new cars like a real manager to a real company.

This topic deserves its own blog post so I will not go any deeper. But I recommend watching this talk from Andy Matuschak or this WWDC talk to understand why this is so important to build bulletproof apps.

2- Never(?) use implicitly unwrapped properties

You should not use implicitly unwrapped properties by default. You can even forget them in most cases. But there may be some special cases in which you need this concept to please the compiler. And it’s important to understand the logic behind it.

Basically, if a property must be nil during initialization, but will be assigned later on to a non-nil value, you can define that property as implicitly unwrapped. Because you will never access it before it’s set so you wouldn’t want compiler to warn you about it being nil.

If you think about view — xib relationship, you can understand it better. Let’s say we have a view with nameLabel outlet.

class SomeView: UIView {
  @IBOutlet let nameLabel: UILabel
}

If you define it like this, compiler will ask you to define an initializer and assign nameLabel to a non-nil value. Which is perfectly normal because you claimed that SomeView will always have a nameLabel. But you cannot do it because the binding will be done behind the scenes in initWithCoder. You see the point? You are sure that it will not be nil, so there is no need to do a nil-check. But at the same time, you cannot (or should not) populate it.

In this case, you define it as an implicitly unwrapped property. It’s like signing a contract with the compiler:

You: “This will never be nil, so stop warning me about it.

Compiler: “OK.”
class SomeView: UIView {
  @IBOutlet var nameLabel: UILabel!
}
Popular question: Should I use implicitly unwrapping while dequeueing a cell from table view?

Not very popular answer: No. At least crash with a message:
guard let cell = tableView.dequeueCell(...) else {
  fatalError("Cannot dequeue cell with identifier \(cellID)")
}

3- Avoid AppDelegate overuse

AppDelegate is no place to keep your PersistentStoreCoordinator, global objects, helper functions, managers, etc. It’s just like any class which implements a protocol. Get over it. Leave it alone.

I understand you have important stuff to do in applicationDidFinishLaunching but it is too easy to get out of control as the project grows. Always try to create separate classes (and files) to manage different kind of responsibilities.

👎 Don’t:

let persistentStoreCoordinator: NSPersistentStoreCoordinator
func rgb(r: CGFloat, g: CGFloat, b: CGFloat) -> UIColor { ... }
func appDidFinishLaunching... {
  Firebase.setup("3KDSF-234JDF-234D")
  Firebase.logLevel = .verbose
  AnotherSDK.start()
  AnotherSDK.enableSomething()
  AnotherSDK.disableSomething()
  AnotherSDK.anotherConfiguration()
  persistentStoreCoordinator = ...
  return true
}
Developer in AppDelegate.swift

👍 Do:

func appDidFinishLaunching... {
  DependencyManager.configure()
  CoreDataStack.setup()
  return true
}

#FreeAppDelegate

4- Avoid overusing default parameters

You can set default values to parameters in a function. It’s very convenient because otherwise you end up creating different versions of the same function as below just to add syntax sugar.

func print(_ string: String, options: String?) { ... }

func print(_ string: String) {
  print(string, options: nil)
}

With default parameters, it becomes:

func print(_ string: String, options: String? = nil) { ... }

Easy, right? It’s super simple to set a default color for your custom UI component, to provide default options for your parse function or to assign a default timeout for your network component. But… you should be careful when it comes to dependency injection.

Let’s look at the following example.

class TicketsViewModel {
  let service: TicketService
  let database: TicketDatabase
  init(service: TicketService,
       database: TicketDatabase) { ... }
}

Usage in App target:

let model = TicketsViewModel(
  service: LiveTicketService()
  database: LiveTicketDatabase()
)

Usage in Test target:

let model = TicketsViewModel(
  service: MockTicketService()
  database: MockTicketDatabase()
)

The very reason you have protocols here for service (TicketService) and database (TicketDatabase) is to abstract away from any concrete types. This enables you to inject whatever implementation you like in TicketsViewModel. So if you inject LiveTicketService as a default parameter into TicketsViewModel, this would actually make TicketsViewModel dependent to LiveTicketService, which is a concrete type. It conflicts with what we are trying to achieve in the first place, right?

Not convinced yet?

Image that you have App and Test targets. TicketsViewModel normally will be added to both targets. Then you would add LiveTicketServiceimplementation into App target, and MockTicketService implementation into Test target. If you create a dependency between TicketsViewModel and LiveTicketService, your Test target wouldn’t compile because it doesn’t (shouldn’t) know about LiveTicketService!

Aside from this, I think it’s also self-documenting and safe by design to inject dependencies manually.

5- Use variadic parameters

Because it’s cool, super easy to implement and powerful.

func sum(_ numbers: Int...) -> Int {
  return numbers.reduce(0, +)
}
sum(1, 2)       // Returns 3
sum(1, 2, 3)    // Returns 6
sum(1, 2, 3, 4) // Returns 10

6- Use nested types

Swift supports inner types so you can (should) nest types wherever it makes sense.

👎 Don’t:

enum PhotoCollectionViewCellStyle {
  case default
  case photoOnly
  case photoAndDescription
}

You will never use this enum outside a PhotoCollectionViewCell so there is no point putting it in global scope.

👍 Do:

class PhotoCollectionViewCell {
  enum Style {
    case default
    case photoOnly
    case photoAndDescription
  }
  let style: Style = .default
  // Implementation...
}

This makes more sense because Style is a part of PhotoCollectionViewCelland is 23 characters shorter than PhotoCollectionViewCellStyle.

7- Go final by default 🏁

Classes should be final by default because you generally don’t design them to be extendible. So it’s actually an error not to make them final. For example, how many times you subclassed your PhotoCollectionViewCell?

Bonus: You get slightly better compile times.

8- Namespace your constants

Did you know that you can namespace your global constants properly instead of using ugly prefixes like PFX or k?

👎 Don’t:

static let kAnimationDuration: TimeInterval = 0.3
static let kLowAlpha = 0.2
static let kAPIKey = "13511-5234-5234-59234"

👍 Do:

enum Constant {
  enum UI {
    static let animationDuration: TimeInterval = 0.3
    static let lowAlpha: CGFloat = 0.2
  }
  enum Analytics {
    static let apiKey = "13511-5234-5234-59234"
  }
}

My personal preference is to use only C instead of Constant because it’s obvious enough. You can choose whichever you like.

Before: kAnimationDuration or kAnalyticsAPIKey

After: C.UI.animationDuration or C.Analytics.apiKey

9- Avoid _ misuse

_ is a placeholder variable which holds unused values. It’s a way of telling “I don’t care about this value” to the compiler so that it wouldn’t complain.

👎 Don’t:

if let _ = name {
  print("Name is not nil.")
}

Optional is like a box. You can check if it’s empty just by peeking into it. You don’t have to take everything out if you don’t need anything in it.

👍 Do:

  • Nil-check:
if name != nil {
  print("Name is not nil.")
}
  • Unused return:
_ = manager.removeCar(car) // Returns true if successful.
  • Completion blocks:
service.fetchItems { data, error, _ in
  // Hey, I don't care about the 3rd parameter to this block.
}

10- Avoid ambiguous method names

This actually applies to any programming language that needs to be understood by humans. People should not put extra effort in understanding what you mean, it is already hard to understand computer language!

For example, check this method call:

driver.driving()

What does it really do? My guesses would be:

  • It marks driver as driving.
  • It checks if driver is driving and returns true if so.

If someone needs to see the implementation to understand what a method does, it means you failed naming it. Especially, if you are working in a team, handing over old projects, you will read more than you write code. So be crystal clear when naming things not to let people suffer understanding your code.

11- Avoid extensive logging

Stop printing every little error or response you get. Seriously. It’s equivalent to not printing at all. Because at some point, you will see your log window flowing with unnecessary information.

👍 Do:

  • Use error log level in frameworks you use.
  • Use logging frameworks (or implement it yourself) which let you set log levels. Some popular frameworks: XCGLogger, SwiftyBeaver
  • Stop using logging as a primary source for debugging. Xcode provides powerful tools to do that. Check this blog post to learn more.

12- Avoid disabling unused code

Stop commenting-out code pieces. If you don’t need it, just remove it! That simple. I have never solved a problem by enabling legacy code. So clean up your mess and make your codebase readable.


What if I told you…

…that you can achieve most of it with automation? See Candost’s post on Using SwiftLint and Danger for Swift Best Practices.


Thanks for scrolling all the way!

Please let me know if you have any other secret practices and help spread the word. ❤️