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
}
👍 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 LiveTicketService
implementation 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 PhotoCollectionViewCell
and 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 returnstrue
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. ❤️