Practical Tips For Junior IOS Devs Master
Practical Tips For Junior IOS Devs Master
A little bit about myself and then we'll jump into the code.
My name is Aryaman Sharda and I've been making iPhone apps since 2015. My career
has included working for a variety of companies, from a few lesser-known startups in
San Francisco to well-known companies like Turo, Scoop Technologies, and Porsche.
Ever since I wrote my first line of Objective-C code, I started maintaining a notebook
where I'd write down anything meaningful I learnt. Regardless of whether it was a
particularly challenging bug, a novel code snippet, or noteworthy insight from a more
senior engineer - it would go in the notebook.
Now, 5 years later, it's closing in on 200 pages and is full of practical tips derived from
actual day to day experience working as an iOS developer. It now serves as the
foundation for this book.
My hope is that the following tips and practical suggestions will help you succeed in
your new role.
The book will be updated on a periodic basis for no additional cost. It presents topics
ranging from optimal build settings and design patterns to Swift best practices and
conventions. There's no particular order and most tips are standalone. Feel free to
jump around.
The ideas expressed here are my own and you may not agree with them - that's fine
too. I'm only here to share my experience.
Apologies for any unconventional code formatting. It's my attempt to keep the
code readable within the constraints of the Markdown editor used to write this
book.
Table of Contents
Preface
Table of Contents
#1: Embrace Generics
#2: Use switch Pattern Matching
#3: There's Always More To Be Tested
PR Checklist
#4: Leverage Functional Programming
#5: Abstraction Between 3rd Party Dependencies
#6: Know When To Use PDF > PNG
#7: Increase Readability With Extensions
Implementing Protocols
Extending Class Functionality
Creating Optional Protocol Functions
Extensions & Initializers
#8 Dependency Injection & Creating Testable View Controllers
#9: Periodically Clean Up Your Imports & Code
#10: Conflicting Constraints + UIStackViews & Cells
#11: Build An Actual Logging System
#12: Structuring Your Endpoints
#13: You Probably Don't Need That Dependency
Network Layer (e.g. Alamofire)
Image View + URL (e.g. Kingfisher / SDWebImage)
#14: Start With A Stylesheet
#15: Comments Need To Go Beyond "What"
#16: DateFormatter & Locales
#17: Use Xcode's Documentation Features
#18: Keychain > UserDefaults
#19: Follow Delegate Conventions
#20: Avoiding Hardcoded Strings
#21: Working With Access Levels
#22: Computed Properties
#23: Project & App Optimization Tips
Opaque Views
Unused Resources
Don't print() In Release
Image View
Structs > Classes
Build Settings
Instruments
Modular Architecture
#24: Static vs. Dynamic Libraries
Static Libraries
Dynamic Libraries
#25: Careful with default
#26: Establish A Pull Request (PR) Template
#27: Moving Navigation Out Of The UIViewController
#28: @discardableResult
#29: Knowing When To Use Structs vs. Classes
#30: Child View Controllers
#31: Increase Readability With typealias
#32: Use Singletons Sparingly
#33: Utilizing Code Snippets
#34: Learn Some Basic DevOps (Fastlane, CircleCI, BitRise)
#35: Making the Happy Path Clear
#36: Remove Unnecessary self References
Conclusion
#1: Embrace Generics
Creating classes and functions with generics is a great way to write general purpose
code. In simple terms, generics allow you to write the code once, but apply it to
a variety of different data types.
In reality, though the syntax appears unusual at first, after some practice it becomes a
lot more friendly.
Generics can greatly reduce the total amount of code you need to write when used
correctly.
In the following example, we are saying we are going to pass something into
exists() that is Equatable . We don't know what yet, but we promise it will
implement the Equatable protocol when the time comes. Additionally, the second
parameter will be an array of items that are also Equatable and we're simply looking
for a Bool in response.
Since these items all implement the Equatable protocol, we can use == to
verify equality.
Using this method, we can pass an input array of any Equatable type, and easily
check if it contains the target element - item . We can use custom objects, structs,
primitive data types, or anything else that implements Equatable .
return false
}
// The main takeaway here is that we've written the code once, but
// we can apply it over a wide variety of data types.
// Output: true
exists(item: "1", elements: ["1", "2", "3", "4"]))
// Output: false
exists(item: -1, elements: [1, 2, 3, 4])
// Output: true
exists(item: CGPoint(x: 0, y: 0), elements: [CGPoint(x: 0, y: 0),
CGPoint(x: 0, y: 1),
CGPoint(x: 0, y: 2)])
extension UIView {
// In this code, we're saying that `T` is eventually going to
// be a `UIView`. It could be a `UIButton`, `UIImageView`, or
// an ordinary `UIView`. Doesn't matter.
//
// Now, we can load any `UIView` or subclass from a `.nib`.
class func fromNib<T: UIView>() -> T {
return Bundle(for: T.self).loadNibNamed(
String(describing: T.self), owner: nil, options: nil
)![0] as! T
}
}
Your application may often make decisions using the combined state of multiple
properties.
Consider the function below, which shows a message based on the user's role and
payment status.
Imagine how unruly the above code would be if we had to consider more than just
two properties (i.e. user.isAdmin , user.hasActiveMembership ,
user.isEmailVerified , etc).
This could be written much more elegantly by using computed properties and
switch pattern matching.
This approach is especially useful when working with optionals, multiple elements in a
tuple, or different input types.
Letting obvious mistakes slip through the cracks will have a more lasting
impression on your colleagues than taking a few extra minutes to check your
work.
I struggled with this habit for a long time. I was always impatient to move on to the
next problem.
I'd encourage you to start your own checklist and include cases that you routinely
forget to check.
PR Checklist
Functional programming is one of the best ways to write readable safe code.
Let's say you wanted to create a function to double all of the numbers in an array.
for i in 0..<input.count {
input[i] = input[i] * 2
}
However, we've immediately run into a problem. What happens if I wanted to access
the original values in input ?
Functional programming, instead, tries to prevent making any changes to the existing
state of the application or introduce any side effects.
You'll notice that in all of the following examples, the input variable's values are
never changed allowing us to avoid mutability. Instead, they return an entirely new
value rather than modifying the inputted one.
.map {}
// We'll take every number in the input, double it, and return it in
a new
// array.
//
// Notice that the input array is never modified.
//
// Output: [2, 4, 6, 8, 10, 12]
// $0 refers to the current element in the input array `map` is
operating on.
let mapOutput = input.map { $0 * 2 }
.compactMap {}
// compactMap: Works the same way as map does, but it removes nil
values.
let input = ["1", "2", "3", "4.04", "aryamansharda"]
.flatMap {}
// This will flatten our array of arrays structure into just a single
array.
// Output: [1, 2, 2, 3, 3, 3, 4, 4, 4, 4]
let flatMapOutput = input.flatMap { $0 }
.reduce {}
.filter {}
.forEach {}
// This will print out the numbers in `input` just like a traditional
for-loop.
input.forEach {
print($0)
}
When you use forEach it is guaranteed to go through all items in sequence order,
but map is free to process items in any order.
.sorted {}
These functions can be applied to objects of any type - even custom ones. In addition,
combining these calls can deliver impressive functionality for a minimal amount of
code:
// Let's return valid cars (not nil) that have a horsepower greater
than 300
// and are sorted by descending price.
cars.compactMap { $0 }.filter { $0.horsepower > 300}
.sorted { $0.price > $1.price }.forEach {
print($0)
}
// Output
Car(name: "Porsche 911", horsepower: 379, price: 101200)
Car(name: "Porsche Panamera", horsepower: 325, price: 87200)
Car(name: "Porsche Taycan", horsepower: 402, price: 79900)
Car(name: "Porsche Cayenne", horsepower: 335, price: 67500)
Functional programming in iOS is becoming more and more mainstream and is critical
to the SwiftUI and Combine ecosystems. Levering these language features and new
paradigm correctly allows you to write optimized, thread-safe, easily testable, and
readable code.
#5: Abstraction Between 3rd Party Dependencies
One of the most useful patterns I encountered as a junior developer was the concept
of layering abstractions between your codebase and external dependencies.
A simple implementation would involve calling the Firebase SDK wherever needed:
import FirebaseAnalytics
Analytics.logEvent("analytic_event_name", parameters: [:])
A better implementation would be one that does not even expose the provider
to the codebase.
All analytic events have the same structure; a name and optional metadata. So, let's
start by defining this requirement in a protocol.
Now, let's complete the implementation for a class that uses this protocol.
import FirebaseAnalytics
This will be the only time we import FirebaseAnalytics . We could have just as
easily created a MixpanelAnalyticsEngine or any other variant instead.
We can now use AnalyticsManager to send events out without revealing the
provider.
This is not intended to be production ready code, but rather to demonstrate the
separation between the FirebaseSDK and the rest of the codebase.
class AnalyticsManager {
private var engine: AnalyticsEngine?
static var shared = AnalyticsManager()
private init() {}
AnalyticsManager.shared.track(eventName:
"user_clicked_forgot_password",
metadata: ["userID": "aryamansharda"])
Whenever we need to replace the provider, we can simply create a new class that
implements the AnalyticsEngine protocol and pass it in to the
AnalyticsManager 's shared instance:
AnalyticsManager.shared.configure(engine: MixpanelAnalyticsEngine())
With this approach, our codebase is not aware which providers we are using and
swapping them out becomes a straightforward task.
#6: Know When To Use PDF > PNG
Back in Xcode 6, Apple introduced a feature that allowed you to add .pdf files
instead of just .png or .jpg files to your Xcode asset catalogs. This enabled you to
support all image sizes without manually exporting @1x , @2x , and @3x
resolutions. At build time, Xcode would generate the appropriate resolutions.
With Xcode 9+, this functionality is even more robust. However, there are two edge
cases to consider.
First, make sure that Xcode preserves vector data for your .pdf assets.
Otherwise, these images will not be treated as vectors at runtime.
Additionally, in some circumstances you may prefer a .png over a .pdf . In most
cases, this is done when you want to minimize your application's size.
A complex illustration, like the one below, may result in a .pdf file larger in
size than several smaller .png ones combined. In addition, if you export images
from an application such as Sketch or Figma, the default output size may exceed what
the phone will ever be able to display. So, before including an asset in your project,
make sure you check its file size. You could be increasing the executable size without
any reason and thus degrading your app's performance.
Generally, I stick to .pdf files unless the .pdf is exceeding a few hundred KB in size
(which can be the case for complex illustrations). In that case, I prefer to use multiple
.png files instead. I usually use .png for illustrations and .pdf for any type of
graphic, symbol, or icon asset. However, the primary deciding factor here should be
the file size - we want to keep the app as small as possible.
#7: Increase Readability With Extensions
Extensions are incredibly useful in Swift and allow you to easily add new functionality
to a class, struct, enumeration, or protocol without access to the source code. We can
also use them to easily group related sections of code together thereby improving the
code's readability. Though this doesn't do much to prevent the Massive View
Controller issue, leveraging extensions correctly can make an already large
class a bit more manageable.
// MARK: - UI
private func showFiltersView() {}
private func updateUserCurrentLocation() {}
}
// MARK: - Events
extension MapViewController {
@IBAction func didTapRecenterButton( _ sender: UIButton)
}
Now, a new programmer seeing this class for the first time would be able to easily
understand the structure of the code and all of the responsibilities of the
MapViewController .
Implementing Protocols
This can be accomplished within the same source code file (see above) by using
MARK:// to easily delineate different sections of the code.
import UIKit
Using extensions, you can easily add functionality to existing classes which you can
then leverage in any future project. It has been useful for me to create a private
repository of Swift extensions which I can then integrate into any new project through
Cocoapods.
extension UIView {
// Provides a type-safe way of loading a view from a .nib
// Usage: let restaurantView = RestaurantView.fromNib()
class func fromNib<T: UIView>() -> T {
return Bundle(for: T.self).loadNibNamed(
String(describing: T.self), owner: nil, options: nil
)![0] as! T
}
}
extension UIView {
// Helper function for defining auto-layout constraints
func pinEdges(to other: UIView) {
translatesAutoresizingMaskIntoConstraints = false
leadingAnchor.constraint(equalTo:
other.leadingAnchor).isActive = true
topAnchor.constraint(equalTo: other.topAnchor).isActive =
true
bottomAnchor.constraint(equalTo: other.bottomAnchor).isActive
= true
trailingAnchor.constraint(equalTo:
other.trailingAnchor).isActive =
true
}
}
My personal repository has over a hundred such commonly used extensions. Starting
your own collection will save you countless hours in the long run.
If you are defining a protocol and you want to make certain functions optional,
you can use a protocol extension to provide default implementations.
Then, a future class or struct that implements this protocol may optionally override
the default implementation.
extension CameraPickerVCDelegate {
func cameraPickerControllerDidTakePhoto(
_ cameraPickerController: CameraPickerVC, photo: UIImage) {
// Save to disk
}
}
Therefore, future classes that implement this protocol won't have to re-implement the
most common use case. In the event that we would prefer to do something different -
for instance, upload the picture to a server - we have the freedom to do that as well.
By utilizing this approach, you will be able to write less code and re-use common
implementations.
You can also add new initializers to existing types through extensions. This enables
you to extend other types to accept your own custom types as initialization
parameters or to provide additional initialization options that were not included
as part of the type’s original implementation.
struct Rocket {
let weight: Double
let height: Double
let width: Double
}
extension Rocket {
init(dictionary: [String: Double]) {
self.weight = dictionary["weight"]
self.height = dictionary["height"]
self.width = dictionary["width"]
}
}
// Usage:
// This is created by default in Swift.
Rocket(weight: 1000, height: 2000, width: 3000)
func loadData() {
let response = networkManager.makeNetworkCall()
dataManager.save(response: response)
updateUI(response: response)
}
}
func loadData() {
let response = networkManager.makeNetworkCall()
dataManager.save(response: response)
updateUI(response: response)
}
}
// Usage:
viewController.injectDependencies(networkManager: NetworkManager(),
dataManager: DataManager(),
locationManager: LocationManager())
It's up to you how you inject these dependencies. You can create a designated
function, public getters / setters for the dependencies, pass them through the
initializer, etc.
// Alternative:
viewController.injectDependencies(networkManager:
OfflineNetworkManager(),
dataManager: LocalDataManager(),
locationManager:
FakeLocationManagerForTesting())
With this simple change, we have greatly extended the view controller's reusability
and made it far more testable and extensible.
Dependency injection allows the same code to be compatible with any of the concrete
types or subclasses referenced above.
Tests need to be deterministic in order to run reliably - they should produce the same
output for the same input. As a result, we would prefer to avoid network calls in our
tests as they are often unreliable.
/************************************************/
// Now, in our testing target:
func testSlowNetworkCall() {
let viewController = ViewController()
viewController.injectDependencies(
networkManager: FakeSlowNetworkManager(), ....))
By simply passing in the dependencies rather than initializing them directly, the
ViewController class becomes more modular and testable.
#9: Periodically Clean Up Your Imports & Code
When classes undergo extensive refactoring, unused imports are often present.
Unfortunately, Xcode doesn't flag this as an issue.
The use of unnecessary imports interferes with the next developer's ability to
understand the intent behind the code. Keeping the imports and dependencies in line
with the purpose of the class makes understanding and debugging the code much
simpler. Additionally, unnecessary imports can increase the size of your application
and the time it takes to compile.
This is also true for any deprecated code and resources in your application. Even if the
code is commented out, there's no reason to keep it in the project - that's what
version control is for.
There have been instances where we've kept a 3rd party dependency in a project far
longer than we needed to simply because we saw extraneous import statements -
the library itself was never being used.
#10: Conflicting Constraints + UIStackViews & Cells
In most cases, if the view works, a ticket is created to resolve the conflicting constraint,
and then never revisited. These can cause unpredictable errors in your app if they're
not caught as part of the code review process.
I was working on an issue a couple of weeks ago in which a small view on a cell
should've been hidden, but it wasn't responding to changes to the isHidden
property. Moreover, it was showing in an incorrect position and only on some of the
cells in the UITableView .
Initially, I thought I had made a logic error and was accidentally toggling the
isHidden property somewhere. As it turns out, it was due to conflicting constraints.
A particular UIStackView was unable to honor all the constraints and upon trying to
recover, it broke a constraint that resulted in this odd behavior.
The point here is that failing to resolve conflicts is just kicking the problem
down the road. They'll eventually manifest as a bug and often in a way you
wouldn't suspect. Moreover, resolving these conflicts becomes even more important
when dealing with UIStackViews and reusable cells since their use tends to
exacerbate the problem.
In the CPU Profiler , you'll see small spikes in CPU usage when the system is
trying to resolve these constraints at runtime. If you encounter conflicting
constraints in a UITableViewCell or in a UICollectionViewCell then you are
incurring this performance hit every time the cell is reused.
Just because the view appears to be 'okay' despite some conflicting constraints, does
not mean it will continue to "work" on future iOS updates or devices.
You'll also find that by giving proper titles to each view in your .storyboard / .xib
you'll be able to debug conflicting constraints more clearly. And finally, when dealing
with UIStackViews , a solid understanding of Content Hugging Priorities and
Content Compression Resistance Priorities will be your go-to tools in resolving
conflicting constraints.
#11: Build An Actual Logging System
Most junior developers, myself included, rely heavily on print and NSLog
statements to aid them in their debugging. While this approach has its place, you
will greatly benefit from taking the time to create a proper logging utility.
import Foundation
enum Log {
enum LogLevel {
case info, warning, error
fileprivate var prefix: String {
switch self {
case .info: return "INFO"
case .warning: return "WARN ⚠ "
case .error: return "ALERT ❌ "
}
}
}
struct Context {
let file: String
let function: String
let line: Int
var description: String {
return "\((file as NSString).lastPathComponent):\(line) \
(function)"
}
}
// Usage:
Log.error("Failed to identify user location.", error)
#12: Structuring Your Endpoints
Managing a growing codebase requires that all API calls are organized properly.
There should be no need for you to make multiple calls to URLSession across
your codebase. Instead, you should rely on some type of networking layer to
take care of these HTTP requests.
We will take a look at a sample networking layer shortly. But first, let’s take a look at an
option for organizing our endpoints. The following approach is scalable and can be
applied to your own private API or to any external API your app relies on.
import Foundation
// Example: "maps.googleapis.com"
var baseURL: String { get }
// "/maps/api/place/nearbysearch/"
var path: String { get }
// "GET"
var method: HTTPMethod { get }
}
For example, if we wanted to implement the Google Places API , we could easily do
so:
As a new developer, I had a bad habit of introducing too many external dependencies.
Adding something to my Podfile was so easy and it'd save me hours of work - it
seemed like a no brainer.
However, as you transition to more professional work, 3rd party dependencies can
dramatically raise the risk and maintenance costs for your codebase.
Occasionally, a new version of iOS would break one of our dependencies, and we'd
have to patiently wait for the developer to provide a fix before we could continue
releasing our application.
Often, we would rely on large libraries only to use a small portion of their capabilities
(usually frameworks like Alamofire and Kingfisher).
Be pragmatic about whether or not the risk associated with the dependency is
worthwhile. Bringing in a new dependency might solve your current problem,
but will increase your application's size, upkeep costs, potentially block
releases, be difficult to de-integrate, and will likely be a black box of
functionality.
It is often easy to create a version of a library's functionality that focuses on just the
functionality you need. The network layer here can easily be extended to handle most
common scenarios without the use of external dependencies.
import Foundation
/// Executes the web call and will decode the JSON response into
/// the Codable object provided.
/// - Parameters:
/// - endpoint: the endpoint to make the HTTP request against
/// - completion: the JSON response converted to the provided
Codable
/// object, if successful, or failure otherwise
class func request<T: Decodable>(endpoint: API,
completion: @escaping (Result<T,
Error>)
-> Void) {
let components = buildURL(endpoint: endpoint)
guard let url = components.url else {
Log.error("URL creation error")
return
}
dataTask.resume()
}
}
Image View + URL (e.g. Kingfisher / SDWebImage)
The advantage of creating your own solution is that it keeps you in full control of your
code rather than you having to depend on whatever magic happens within the
external dependency's implementation. Here's an example of a lightweight
UIImageView that can fetch remote images asynchronously with just a few lines of
code:
import Foundation
import UIKit
extension UIImageView {
/// Loads an image from a URL and saves it into an image cache,
returns
/// the image if already available in the cache.
/// - Parameter urlString: String representation of the URL to
load the
/// image from
/// - Parameter placeholder: An optional placeholder to show
while the
/// image is being fetched
/// - Returns: A reference to the data task in order to pause,
cancel,
/// resume, etc.
@discardableResult
func loadImageFromURL(urlString: String,
placeholder: UIImage? = nil) ->
URLSessionDataTask? {
self.image = nil
task.resume()
return task
}
}
One of my first freelance experiences involved working with an individual who would
continuously alter the look and feel of the iOS application. As a result, I spent so much
time in the initial days of that project changing font and color attributes in each
.xib and .storyboard manually.
Don't make the same mistake - you're better off creating a "stylesheet" from the start
than trying to implement one later on.
These days, any new project I start begins like this. You can include as many properties
and sections as you require.
import Foundation
import UIKit
enum Theme {
enum Constants {
static var standardMargin: CGFloat {
16
}
}
enum Colors {
static var companyDarkBlue: UIColor {
UIColor(named: "CompanyDarkBlue")!
}
}
enum Font {
static func regular(size: CGFloat) -> UIFont {
getCustomFont(withName: "CompanyCustomFont", size: size)
}
// Usage: Theme.Colors.companyDarkBlue
// Usage: Font.regular(size: 18.0)
One of the advantages of this approach is that you can define styling properties in one
location and they will propagate throughout the rest of the application.
Consider, for example, changing the look of the application to reflect changes in your
company's branding or changes to support night mode. Just by changing a few
properties in the code above, you would be able to overhaul the entire
appearance of your iOS app. Otherwise, you might have to go into each
.storyboard , .xib , and source-code file and update these properties
manually.
Next, you could then create subclasses of UIKit components and custom views you
intend to reuse in your application: such as UnderlineLabel , HeaderLabel ,
StandardTableViewFooterView . This will reduce the amount of repeated code in
your codebase and offer a single source of truth on all UI related topics. This greatly
speeds up development as you can define this class once and use it anywhere in this
project or easily carry it over into a new one.
With .storyboards , you'll often see clashes between what the .storyboard shows
and what the code specifies. It's common for a developer to specify a custom
UIView or custom style on the .storyboard only to override some properties later
in the code (i.e. cornerRadius , clipsToBounds , alpha ).
Therefore debugging becomes complex, as the programmer must now check multiple
locations to figure out the expected behavior. Instead, my suggestion would be to lay
out and specify the elements on the .storyboard / .xib , but any of the other
customizations (fonts, text colors, attributed text) should be specified in code. The
code - not the .storyboard - should be the single source of truth. Additionally, this
approach allows you to still retain features like "Search & Replace" and visibility in
Xcode's refactoring tool which wouldn't be possible if you were using the
.storyboard .
#15: Comments Need To Go Beyond "What"
When writing comments, explaining why the code is written a certain way is
arguably more important than explaining what it does. Programmers will
understand what a given piece of code does, but they might find it harder to infer the
context in which it was written and its possible side effects.
Here's some examples of good comments; they clearly indicate why the code was
written a certain way instead of what it does:
For some bad examples, you'll see they don't provide any additional information or
insight the code doesn't already convey:
Mention alternate approaches that you considered and explain why they were
not used.
Be used to provide a brief summary for a longer piece of code.
Be used to address questions or concerns the code is incapable of answering on
its own.
Clearly communicate any possible side effects that execution of the code might
cause.
Be tailored to other developers who may not have had the same experiences or
knowledge as you and may be less familiar with the code.
#16: DateFormatter & Locales
When an API expects dates and times in a specific format, it's important to
include the locale information in your DateFormatter .
We'd expect "April 6th, 2021 10:59 pm" to be formatted like this:
However, if you were to use the following code as is, there is an edge case that isn't
handled.
If a user's region typically uses 12 hour time and they have specified in their iOS
settings to use 24 hour time or vice-versa, iOS will add an "AM" / "PM" suffix to
the date.
For example, France typically uses 24 hour time. If a French user has manually
enabled 12 hour time instead, the code above would have "AM" or "PM" tacked on the
end.
The simple fix here is to include locale information in your DateFormatter setup.
This will help standardize the formatting of your dates across various regions and
locales.
As you are probably aware, you can easily create comments using the // or
/*...*/ .
However, there is another option in Xcode that is perhaps less well known - /// . In
general, this is meant to provide more context around a section of code - either the
inputs / outputs, the side effects, or the return values.
With this syntax, you can easily view the documentation for a method or class, without
having to use the Xcode shortcut to jump to its definition. This has saved me a lot of
time and enabled me to create more useable libraries and frameworks for other
developers.
We're not doing anything special here. It's just a helper function that lets you easily
create and present a UIAlertController . If we put our cursor on the line where we
have declared the showAlert method, and then hit Command + Option + / , Xcode
will generate this:
/// Description
/// - Parameters:
/// - alertText: alertText description
/// - alertMessage: alertMessage description
func showAlert(alertText: String, alertMessage: String) {
let alert = UIAlertController(title: alertText,
message: alertMessage,
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: L10n.alertConfirmation,
style: .default, handler: nil))
self.present(alert, animated: true, completion: nil)
}
Any comment with /// will be surfaced in multiple locations across the Xcode
IDE. Your team would lose out on a major convenience if you were to opt out of
using this documentation style.
Now, pressing Option + Left Mouse Click on the call to showAlert() results in:
When I first started making iOS apps, I would abuse UserDefaults . I would store
large amounts of information there and used it as my primary method of passing data
between views. Obviously, that's not how it was meant to be used. Even more
damning was the fact that I was storing sensitive user data in UserDefaults .
You may also think, as I did, that storing your information there is safe enough. The
possibility that someone would have my phone, know my password, and reverse-
engineer the data seemed unlikely. However, this isn't as difficult as you might expect.
If you follow DEFCON and the jailbreaking community, you'll find plenty of tools and
approaches for retrieving this information. Instead, I should've been using Keychain .
Essentially, the Keychain is a system level mechanism that persists across app
updates and reinstalls for storing and encrypting sensitive data.
Instead of directly working with the Keychain API in C, you can use a Swift wrapper
like KeychainAccess.
#19: Follow Delegate Conventions
When you work with delegates, make sure you honor all of the following Apple
recommendations and conventions.
Additionally, when creating custom delegate methods, the first argument should be a
reference to the entity that is triggering the delegate function.
Method names for delegate function should also include keywords like should ,
will , has , did to indicate to the programmer if the event will occur in the near
future or already has.
The first parameter is always unnamed and a reference to the entity from which the
delegate function is being called (e.g. tableView , manager , and scene ). With this
approach, naming conflicts can be avoided with other delegate functions that might
have the same function declarations.
It's easy for these hard-coded strings to become out of date or contain typos that
crash the application. I'm sure we've all experienced the crash that would occur in the
following situation:
We'll look at some extensions and tools developed by the iOS community to
manage this issue and make our code more type-safe.
So, what happens now if we change the name of the class? We'd have to go in and
manually update the identifier in any location we reference the HotelCell . In
situations like this, Xcode's refactor tool is far from comprehensive and reliable.
By adding a small extension, we can deal with custom cells and their reuse identifiers
in a more type-safe manner:
extension UITableViewCell {
static var identifier: String {
// Returns a String version of the current class's name.
return String(describing: self)
}
}
The change may seem minor, but it helps us prevent easy avoidable crashes and is
more suited to future refactoring.
It's easy to forget about updating and managing these hardcoded strings, now it's a
non-issue.
Let's take this one step further using tools like SwiftGen (https://github.com/SwiftGen/
SwiftGen).
By using this tool, we're able to add type-safety to our asset catalogs, colors, fonts,
storyboard files, and more.
Here's a simple configuration file (see GitHub for more detailed instructions):
strings:
inputs: Resources/Base.lproj
outputs:
- templateName: structured-swift5
output: Generated/Strings.swift
xcassets:
inputs:
- Resources/Images.xcassets
- Resources/MoreImages.xcassets
- Resources/Colors.xcassets
outputs:
- templateName: swift5
output: Generated/Assets.swift
If you add it to your project's Build Phases , it will run every time you compile. End
result here is that this utility will automatically generate the following Swift files for
you allowing you to access all your resources in a type-safe manner.
Remember, all of this code is automatically generated and updated every time
you compile.
This is just scratching the surface of the possibilities with this tool. I encourage you to
try it out on a small project to see if you like it.
Since this runs at compile time, any changes - especially removals - to the
.xcassets , .storyboard , .strings , etc are raised as compilation errors
instead of runtime errors. Plus, we get auto-completion on our strings, images,
segue identifiers, assets, etc. for free!
#21: Working With Access Levels
Swift provides five access levels we can use with various entities (functions, types,
enums, extensions, classes, etc.) in our code.
private implies that something can be seen only inside the entity in which it is
defined, or in extensions of that type in the same file.
fileprivate refers to something that is visible to all of the classes and
functions declared in the same source code file (even in extensions), but is
hidden from all other code.
internal is the default access level for Swift classes and properties. It limits
the access level to within the module that defined it. If you were building a
framework, for example, and marked a property internal , code that uses the
framework could not access it because it was not part of the same module.
Instead, you'd have to use public or open .
public enables entities to be used within any source file from their defining
module and also in a source file from another module that imports the defining
module.
open is similar to public with the addition that the entity can now be
subclassed or overridden by something that exists outside of its defining
module.
These access modifiers allow us to easily separate and modularize our codebase.
It is extremely important that our code behaves predictably as we would not
want one part of the code changing state in another unrelated part.
If an unrelated class has the potential to change your object's properties, then
ensuring you have accounted for all of the potential edge cases becomes much
harder. Correctly leveraging access modifiers makes code more testable, reduces the
potential for bugs, and helps prevent "spaghetti" code.
Additionally, consider the "Law of Demeter" - a popular programming rule that states:
Each unit should have only limited knowledge about other units: only units
"closely" related to the current unit.
Each unit should only talk to its friends; don't talk to strangers.
Only talk to your immediate friends.
class BikeStore {
let unicycle = Unicycle()
}
class Unicycle {
let title: String = "Hello, I have one wheel!"
let wheel = Wheel()
}
class Wheel {
let circumference: Double = 20.0
}
Knowing the Law of Demeter and utilizing the correct access levels can protect you
from opening up the codebase more than you should.
I typically don't specify internal as it's the default; I'd rather be concise.
For @IBOutlets , I generally use fileprivate(set) .
Generally, I'll start of with the most restrictive access level - private - on all
definitions (instance variables, classes, properties, computed variables, etc.) and
re-evaluate on a case-by-case basis.
When changing access levels on any entity, be sure to consider the possible side
effects and any potential other options. Should I really change this access level or is
there another solution? Am I putting myself at risk of having bugs or introducing
"spaghetti" code later on if I make this change?
The article continues on to say that using final , private , and Whole Module
Optimization will help improve the compile time and performance of your
application. Another reason to start leveraging more restrictive access levels.
#22: Computed Properties
Swift's computed properties are one of my favorite features, but it's worth discussing
when they are appropriate to use. Unlike traditional properties that you assign a value
directly to, computed properties in Swift don't have any "storage" ability.
var x = 5
Instead, when computed properties are referenced, they generate a return value, for
example:
radius = 2
print(circleArea) // 12.57
This means we need to be careful when we use them and ensure we implement
them in an efficient manner.
When To Use
struct Context {
let file: String
let function: String
let line: Int
var description: String {
return "\((file as NSString).lastPathComponent):\(line) \
(function)"
}
}
You'll notice that the implementations in the closures are simple and do not alter the
state of other properties in the application.
Here are some examples of situations where you should opt for a stored property or a
traditional function instead:
The following example is a bad use of computed properties. Since the API key within
the .plist won't change, there's no reason to read from disk every time you want to
use the apiKey property. As you can imagine, apiKey might be referenced many
times throughout the lifetime of the application and unnecessarily reading from disk
will only slow the program down.
// Slow operation, incurring a performance cost every time this is
accessed.
static var apiKey: String {
return loadKeyFromPlist(key: "GOOGLE_API_KEY")
}
If you find yourself regularly reading the property and the value hasn't changed, then
you should opt for a stored property instead.
Generally speaking, your main concern should be whether the code behind the
computed property:
If none of these concerns apply, and the value of the computed property changes
overtime, then it's a reasonable implementation choice.
#23: Project & App Optimization Tips
Here are some general tips you can use to optimize your Swift projects.
Opaque Views
Unused Resources
Every now and then it's good practice to go through your project and remove unused
assets, classes, functions, colors, and compiler directives. These can be "quick wins"
and often help in reducing app size and app launch time.
While they're often useful in development, they aren't as useful in production. Instead,
you could easily use the Log class shown previously and use a compiler directive to
disable unnecessary print() calls when the release build of the app is running.
Additionally, if an attacker is running your iOS app on a jailbroken phone, they may be
able to reverse-engineer sensitive data by looking at the log output of your
application.
Image View
Resizing images at runtime is an expensive process. So, if you are exporting assets
from Figma / Sketch, double-check that the resolution of the exported assets is
proportional to the display size in the app. The application does not need to include
images that are larger than the image view they are intended to appear in. This simple
fix can help decrease app size and improve performance.
Due to the absence of inheritance and the "pass by value" nature of structs, Swift is
able to make certain compiler optimizations that are unavailable on "pass by
reference" types.
When dealing with reference types, using private and final allow for additional
compiler optimizations with regards to dynamic dispatch.
Dynamic dispatch is the process of selecting which implementation of a polymorphic
operation to call at run time. When we use the final keyword, we know that no
subclass / alternative implementation exists, so Swift is able to execute the function
call directly without this extra computation step of checking for alternative
implementations.
Build Settings
Now, we'll look at some of the best settings for compilation performance here.
Build Phases
Check your Build Phases and make sure only the absolutely required scripts are
running for your Debug and Release builds, respectively.
Compilation Mode
Whole Module compilation will rebuild all files in the project regardless of
whether they were modified since the last build.
Incremental only recompiles files that have changed since the previous build.
For a faster development flow, we can choose Incremental for our debug release
and defer Whole Module compilation for our production release.
Debug: Incremental
Release: Whole Module
Optimization Level
As the name suggests, this property instructs the compiler about the degree of
optimizations it should make.
No Optimization
Optimization for Speed (refers generally to the build time)
Optimization for Size (refers to the overall size of the machine code
generated)
Release: Optimize for Speed - This would improve the performance of the
version of the app we intend to distribute.
Build Options
A .dSYM file is used for re-symbolicating your crash reports (converting memory
addresses of objects and methods into more readable data).
Symbolication allows us to better protect our code from hackers and reverse-
engineers. We don't need such a service during development as we'd have access to
the debugger in Xcode. Instead, the benefits of a .dSYM file are more evident in
production builds.
Instruments
Instruments is an incredibly powerful developer tool that comes with Xcode. I'd
recommend taking the time to play around with it and better understand how you can
use the Time Profiler , Energy Log , Network , and Leaks utilities to better
understand shortcomings in your application.
Modular Architecture
As your codebases becomes larger and larger, you may benefit from chunking
relevant sections of your codebase into separate modules (UI, Data Persistence,
Authentication, etc.). You can use tools like Cocoapods to share them between
projects and easily manage updates.
Modularing your code in this manner allows Xcode to only compile the modules that
have changed - similar to the way Incremental compilation works.
#24: Static vs. Dynamic Libraries
As your application matures and your application size and launch speed start to
suffer, you'll likely find yourself re-evaluating how you integrate libraries into your
project.
Static libraries are collections of object files which are essentially the machine code
output after compilation. If you’ve ever seen a file ending in .a , it’s a static library.
These files are copied into the larger executable that eventually runs on your
phone. Imagine a suitcase filled with everything you need for your vacation. A static
library is similar; everything you need in order to run is included in the executable
itself.
Dynamic libraries ( .dylib files) are loaded into memory when needed, instead of
being included in the executable itself. All iOS and macOS system libraries are
actually dynamic. The main advantage here is that any application that relies on these
dynamic libraries will benefit from all future speed improvements and bug fixes in
these libraries without having to create a new release. Additionally, dynamic libraries
are shared between applications, so the system only needs to maintain one copy of
the resource. Since it’s shared and only loaded when needed, invoking code in a
dynamic library is slower than a static one.
Static Libraries
Pros
Cons
Dynamic Libraries
Pros
Cons
Application may crash if the library updates are not compatible with your
application (i.e. business logic / iOS version).
Application may crash if the dynamic library cannot be loaded / found.
Calls to dynamic library functions are slower than with static libraries.
There’s no one size fits all answer. You’ll have to make a pragmatic decision and weigh
how much you value performant library calls, app size, launch time, etc. and pick the
approach or a hybrid approach that best suits your needs.
#25: Careful with default
When you use default cases in your code, you introduce new states to your
application that are not explicitly handled. Instead, you should opt to specify all
cases and rely on the compile errors to notify you about states you are not explicitly
handing.
switch UIDevice.current.userInterfaceIdiom {
case .phone:
// It's an iPhone
case .pad:
// It's an iPad (or macOS Catalyst)
@unknown default:
// Uh, oh! What could it be?
}
Imagine we're on iOS 13.0 and iOS 14.0 is just released. Had we used a
default case in our switch , we wouldn't have even know about the existence of
UIUserInterfaceIdiomMac and would have lost the opportunity to make a different
product decision on that platform.
Let's say we're developing a navigation app that utilizes the user's location, but tries to
ensure it's not polling too frequently for the available battery charge.
It's conceivable that Apple may one day provide more granular battery updates. By
using the default case in our switch , we would have missed the opportunity to
recognize this new state. As a result, we would miss the chance to make optimizations
in our application - i.e. changing the user location polling frequency.
Or, one of your 3rd party dependencies may introduce a new case that your
default statement happily accommodates. This approach can easily introduce an
unknowable number of new states into your application - none of which you've
explicitly handled.
Although there are exceptions to every rule, I'd caution you against using default
unless you're absolutely confident you've mitigated the risk.
#26: Establish A Pull Request (PR) Template
A simple PR (pull request) template can save your team a lot of time and it takes very
little effort to set up. It will keep everyone on the same page and helps
standardize your PR structures and code review philosophies. It can also serve
as a useful checklist before you ask your team for a review. If you don't already
have one, it is very useful once in place.
Here's a starting template you can use - feel free to customize it as needed.
If you're using GitHub to manage your repository, simply save the following
template in your project's root folder as pull_request_template.md .
<!--
Add overview of changes including technical details here
- Include links to the ticket, design, and relevant documentation
-->
<!--
Remove this section if there are no UI changes
-->
### Note
In the past few years, Soroush Khanlou has popularized the use of the Coordinator
design pattern in iOS applications, and you'll now find variations of this pattern in
many modern iOS codebases.
By doing so, we can greatly increase the usability of that UIViewController as it can
now be dropped into many different user flows without any changes.
Imagine a social media app like Facebook or Instagram. In these application, you have
multiple paths you can take in the app in order to capture an image.
Rather than creating a new image capture related UIViewController for every flow
a user could take through the app, it would make much more sense to create a single
image capture UIViewController that you then introduce into the needed flows.
The key takeaway from the following example should be that the
UIViewControllers are standalone entities. They would have no understanding of
what came before them or what would come after them. Navigation logic now
belongs strictly to the Coordinator class.
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func showSecondOnboardingPage() {
// Present second onboarding page
secondVC.delegate = self
navigationController.present(secondVC, animated: true,
completion: nil)
}
func finishOnboardingPage() {
// Start the next Coordinator
// Maybe the BookingCoordinator to guide the user through all
// the screens to book a hotel, for example.
let bookingCoordinator = BookingCoordinator(
navigationController: self.navigationController
)
bookingCoordinator.start()
}
}
If you kept this navigation logic in the UIViewControllers directly, they would
become tightly coupled with that specific user flow.
Now, you could easily re-use the UIViewControllers in this example to create an
ExpeditedOnboardingCoordinator or EditHotelBookingCoordinator instead.
Since all the UIViewControllers know how to do is report on actions / events that
occur, you can string them together in whatever flow you wish.
Forgot Password
Account Registration
Surveys / Questionnaire (sequences of UIViewControllers you want to present to
the user and record results along the way)
Bookings (AirBnB, Turo, Lime, hotels, etc)
Onboarding (tutorial composed of a series of UIViewControllers)
Moreover, Coordinators are well suited to support A/B testing. A series of screens
can be created to match the experiment you're running or any other feature flags you
may have set in place.
Though understanding this pattern can be tricky, almost every company uses some
variation of this pattern. Feel free to choose the variation that appeals most to you.
#28: @discardableResult
Basically, if you have a function that returns a value that you may not always
need, you can mark it as having a @discardableResult . When you use this
keyword the compiler warning will be suppressed and you will no longer be
notified that the function you're using has a return value that's not being used.
The exact value dropped may not matter to you at all. For example, you might only
want to drop the first element in an array and then continue on with the remaining
elements. Since the function is marked with @discardableResult , you are under no
obligation to do anything with the return value if all you need is the side effect of
calling this function.
import Foundation
import UIKit
extension UIImageView {
/// Loads an image from a URL and saves it into an image cache,
returns
/// the image if already available in the cache.
/// - Parameter urlString: String representation of the URL to
load the
/// image from
/// - Parameter placeholder: An optional placeholder to show
while the
/// image is being fetched
/// - Returns: A reference to the data task in order to pause,
cancel,
/// resume, etc.
@discardableResult
func loadImageFromURL(urlString: String,
placeholder: UIImage? = nil) ->
URLSessionDataTask? {
self.image = nil
let key = NSString(string: urlString)
if let cachedImage = imageCache.object(forKey: key)) {
self.image = cachedImage
return nil
}
task.resume()
return task
}
}
// You can use either depending on your use case, but now it's not
// required to "save" the return value.
Using this keyword clarifies the intent of your function, its side effects, and prevents
warnings for the programmer that calls it.
#29: Knowing When To Use Structs vs. Classes
When you encounter situations where both a struct and a class are plausible options,
it is important to have some guidelines to help determine which one to use.
When in doubt, Apple recommends starting with a struct and transitioning to a class
later on if needed.
#30: Child View Controllers
Leveraging them lets you easily divide a UIViewController 's functionality into a few
modular pieces. This helps prevent the Massive View Controller problem and
allows you to create more reusable components.
Check out the following UIViewController , which displays a map view and a list of
turn-by-turn directions across the bottom of the screen.
func setupMap() {}
func setupDirectionsCarousel() {}
func getUserLocation() {}
func drawRouteLine() {}
func fetchDrivingInformationFromAPI() {}
}
Clearly, this UIViewController is doing too much. It's making network calls,
managing user location, updating the UI, etc.
We can make this a lot cleaner and more re-useable with child view controllers.
func setup() {
// Adding 2 child view controllers
addChild(mapViewController, to: self.view)
addChild(carouselViewController, to: self.view)
mapViewController.setupMap()
carouselViewController.setupDirectionsCarousel()
}
func fetchDrivingInformationFromAPI() {
// Now this parent view controller is only responsible for making
// the network call and passing the relevant data down to its
children
// view controllers - the MapViewController and
// DirectionsCarouselViewController - to display.
}
}
extension UIViewController {
// The following code tells iOS that the current view controller
will
// now be managing the view for a new child view controller as
well.
// Then, we add the child view controller's view to our view and
// finally call didMove(toParent:) which informs the child view
// controller that it has a parent.
func addChild(_ child: UIViewController, to view: UIView) {
addChild(child)
view.addSubview(child.view)
child.view.translatesAutoresizingMaskIntoConstraints = false
child.view.pinEdges(to: view)
child.didMove(toParent: self)
}
func removeChild() {
guard parent != nil else {
return
}
willMove(toParent: nil)
view.removeFromSuperview()
removeFromParent()
}
}
Now, the responsibility and scope of each view controller is more clear. With separate
UIViewControllers to manage the map and carousel, they can be reused
independently within your application. Or, the
TurnByTurnDirectionsViewController could itself be a child of a more
sophisticated view controller.
#31: Increase Readability With typealias
One simple way to make your code more readable is to use Swift's typealias
keyword. As the name suggest, it provides an alias for an existing type.
By the way, did you notice that you can combine protocols with & in Swift?
Now, whenever the compiler sees Codable it will replace it with Decodable &
Encodable and continue compilation.
If you wish to make the typealias accessible throughout your codebase, declare it
outside of any class. If not, the typealias will be limited in scope, as any other
variable would be.
class HelloWorld {
// Only available within this class
typealias Greeting = String
It's easy to overuse typealiases and thereby limit the discoverability of the code,
but as long as you exercise a little restraint, you'll be fine.
#32: Use Singletons Sparingly
Singletons are exceeding popular in iOS. For those unfamiliar with the idea, a
singleton is simply a class that is instantiated once and only once. Or, more formally -
singletons provide a globally accessible shared instance of a class.
UIApplication.shared
UserDefaults.standard
OperationQueue.main
No matter where you call these classes in your code, you're always referring to the
same instance.
The use of this pattern is a contentious topic in the software development community
and many developers consider this to be an "anti-pattern" - a design pattern that
should be avoided, not endorsed.
What often happens with singletons is that their sheer convenience ends up creating
code that is untestable, lacks modularity, and isn't stateful.
We'll take a look at exactly how that happens shortly, but let's first understand how to
implement one.
The code is fairly simple. Our main objective is to make sure that there is only one
instance of a class during its lifetime.
class Singleton {
private init() {}
// Usage: Singleton.shared
My making a private init , we ensure that no other class is able to create an
instance of Singleton . Additionally, the static reference allows it be globally
accessible.
Imagine you create a singleton to manage the currently logged in user in your app,
let's call it UserManager .
With an approach like this, we've relinquished all control. It'd be exceedingly difficult to
debug and identify what part of the code is causing changes to the properties on the
UserManager singleton - it could be anywhere across the entire codebase. As the
project matures, you'll inevitably lose track of what objects have access to the
UserManager singleton and how they modify its properties.
Remember Swift isn't thread safe by default, so having different areas of your code all
potentially mutating the same property is asking for unpredictable behavior in your
app. Not to mention how untestable something like this would become.
Another issue to keep in mind is that since singletons are globally accessible, they end
up housing logic that exceeds their initial scope. In the same way that a
UIViewController tends to be responsible for more topics than it should be, the
same problem can happen with singletons.
They become a dumping ground for all sorts of properties and helper functions. And,
since they're accessible everywhere it can cause the codebase to quickly devolve into a
big mess of "spaghetti" code with no clear separation of concerns. Furthermore, if you
have to eventually remove one of these singletons it can be quite painful as they tend
to be referenced hundreds of times across an application.
We can still use singletons provided we go about integrating them into the code
correctly. The recommend approach for doing this is dependency injection. Instead of
passing a reference to the full UserManager class to something that relies on it, you
just pass the select properties it needs instead. This will help prevent side-effects and
will make the code more testable.
Let's continue with our UserManager singleton. Previously, we might have had code
like this:
func sendPasswordResetEmail() {
sendEmail(email)
}
}
Can you see how it's much more testable now? We can easily inject any email address
for testing purposes instead of having to simulate a logged in user.
So, singletons should only be used if absolutely necessary - the should not be your
default solution. In addition, instead of giving unlimited access to the singleton, you
should carefully consider whether a stricter bound on its properties would be
sufficient (e.g. dependency injection). Typically, this would enhance the testability of
your code and clarify the interface of the class that would be using the singleton's
data.
#33: Utilizing Code Snippets
One of the fastest and easiest ways to speed up your development workflow is
to use Code Snippets in Xcode. Simply press Command + Shift + L or go to View
Menu - Show Library to see the default snippets that come with Xcode.
You can start typing and autocomplete will provide relevant snippets for you to insert
into your code. You can also create your own snippets for your personal use or to
share with your team.
Just select some code, right-click and select Create Code Snippet . If you notice any
common patterns or Swift extensions that you tend to use repeatedly, you can easily
add them here.
Simply save the snippet with a title and description and it'll be ready for immediate
use without restarting Xcode.
My snippets often relate to:
Custom initializers
Setup of testing code
Creating an HTTP request
Loading from a .xib or .storyboard
Date formatting and conversions
User permission requests (camera, notifications, microphone, etc.)
Furthermore, code snippets can reduce the number of comments in code review
about not adhering to conventions by standardizing the code as a whole.
#34: Learn Some Basic DevOps (Fastlane, CircleCI, BitRise)
When I started my first job out of college, I was the only iOS developer on the team.
Even though I was just a junior developer, I had to take on some DevOps (software
development (Dev) + IT operations (Ops)) responsibilities.
fastlane (http://fastlane.tools/)
As a new developer, every time I needed to upload to the App Store, I would trigger
the build from my laptop. Obviously, this isn't sustainable. What if a hotfix needs to be
released and I'm not available to trigger a build?
Or, what happens if my laptop breaks? I'd lose access to my private keys and other
development certificates.
That's where tools like fastlane come in. It's meant to help iOS teams with every
aspect of their release. The learning curve is steep, but the payoff is worth it.
This is just the tip of the iceberg. I highly recommend practicing setting up fastlane
on one of your personal projects as this tool is widely used across iOS teams.
Perhaps you work for a large company with a dedicated DevOps team, but for those
who are interested in the startup space, this will certainly be something you should
know and might be expected to implement.
I've also found that in the process of setting up these services, I've gained a much
deeper understanding of how multiple targets, code signing, provisioning profiles, and
certificates work. Relying on Xcode to "Automatically manage signing" left a lot of
opportunity for technical understanding on the table.
Often, new versions of iOS or Xcode can break your pipeline or at the very least
introduce some unintended side effect.
Those failures can be very expensive - both financially and as a time cost to your team.
If the CI/CD pipeline fails, then this can easily result in a backlog of pull requests that
cannot be safely integrated into the codebase and can make releasing a build of your
application more difficult as well.
You'll be miles ahead of your colleagues if you learn how to set up, customize, and
debug CI/CD pipelines.
I have been using these tools everyday since my first freelance client to my first day as
a junior engineer and now as a senior engineer.
Demonstrating this type of competency will not only increase your skill set and
understanding, but will also make you a more valuable member of your team.
#35: Making the Happy Path Clear
By using guard statements, you can write self-documenting code that's easy to read.
When you use them to stop execution early when conditions aren't met, you
demonstrate to other programmers the ideal set of circumstances under which
the code is meant to run.
Take a look at this function, for example. The nested if statements make it hard to
read and it's even harder to figure out what the ideal input values should be.
Instead, we should only use self when it's required by the compiler. In practice,
this means @escaping closures, initializers, completion blocks, etc.
Unlike Objective-C, Swift does not require us to use self when accessing an object's
properties or methods. So, if the compiler doesn't require it, the code will likely be
more readable without it.
Instead of this:
Our code can be written more concisely without any loss of clarity or functionality:
Thanks for making it to the end! Hopefully, you learnt something from this book. I'll
continue to make free updates over the next few months.