The basics of Swift are easy to learn, but the language has evolved significantly over the past few years. The more I use the language, the more I learn about it and discover its lesser known features. In this episode, I would like to share a handful of tips and patterns I have picked up over the years. They are easy to implement and have the potential to transform messy code into elegant, readable code.
Switching on a Boolean
In Swift, the condition of an if
statement needs to evaluate to true
or false
and that is why most developers use an if
statement if they need to evaluate the value of a boolean.
To make the code I write more readable, I sometimes use a switch
statement instead of an if
statement to evaluate the value of a boolean. Take a look at this example. We use an if
statement to evaluate the value of isActive
. This is textbook Swift. Right?
if isActive {
worker.deactivate()
} else {
worker.activate()
}
In some scenarios, it is more convenient or easier on the eyes if you replace the if
statement with a switch
statement.
switch isActive {
case true:
worker.deactivate()
case false:
worker.activate()
}
The if
statement is still my default for evaluating the value of a boolean, but I sometimes use a switch
statement if I feel it improves the readability of the implementation. Give it a try. You may like it.
Code Hygiene
Object literals have the tendency to clutter a project and I am not only referring to string literals. We use number literals for animations, sizing and positioning views, and many other seemingly trivial use cases. The problem is that those number literals can introduce inconsistencies and code duplication. I would like to share two strategies for managing and reducing number literals in your projects.
The first option may seem odd at first. I use it frequently and it works very well. In this example, we define the size and position of several subviews in code using SnapKit. SnapKit is a lightweight library for working with Auto Layout in code. We define the size and position of the views using number literals. You may notice that several number literals appear more than once. This isn't a major problem, but it is easy to avoid this problem.
private func setupView() {
// Table View
tableView.snp.makeConstraints {
$0.edges.equalToSuperview()
}
// Email Text Field
emailTextField.snp.makeConstraints {
$0.width.equalTo(200.0)
$0.height.equalTo(30.0)
}
// Password Text Field
passwordTextField.snp.makeConstraints {
$0.width.equalTo(200.0)
$0.height.equalTo(30.0)
}
// Title Label
titleLabel.snp.makeConstraints {
$0.width.equalTo(200.0)
$0.leading.equalToSuperview().offset(20.0)
$0.trailing.equalToSuperview().offset(-20.0)
}
}
We create a private extension for the BillingViewController
class at the bottom of the file and define an enum with name Layout
. We create an enum for each view or collection of views we want to size and position. The enum defines one or more static properties of type CGFloat
. We use these properties instead of number literals.
private extension BillingViewController {
enum Layout {
enum TextField {
static let width: CGFloat = 200.0
static let height: CGFloat = 30.0
}
enum TitleLabel {
static let leading: CGFloat = 20.0
static let trailing: CGFloat = -20.0
}
}
}
The benefits are obvious. The implementation of the setupView()
method no longer contains number literals and that makes the code you write more readable. Refactoring also becomes painless because you immediately see which values are used in several places. Autocompletion is another benefit you get for free. This technique is simple but surprisingly effective. The only drawback is that you need to write a few more lines of code.
private func setupView() {
// Table View
tableView.snp.makeConstraints {
$0.edges.equalToSuperview()
}
// Email Text Field
emailTextField.snp.makeConstraints {
$0.width.equalTo(Layout.TextField.width)
$0.height.equalTo(Layout.TextField.height)
}
// Password Text Field
passwordTextField.snp.makeConstraints {
$0.width.equalTo(Layout.TextField.width)
$0.height.equalTo(Layout.TextField.height)
}
// Title Label
titleLabel.snp.makeConstraints {
$0.leading.equalToSuperview().offset(Layout.TitleLabel.leading)
$0.trailing.equalToSuperview().offset(Layout.TitleLabel.trailing)
}
}
The second technique I would like to show you applies to number literals that are used in various, unrelated places of the project. The duration of an animation is a common example. We often use the same duration for most animations in a project and that means we use the same number literal in various, unrelated places of the project. Keeping the values synchronized across the project can becomes tedious and prone to mistakes as the project grows. This is easy to resolve, though.
func hidePaymentView() {
UIView.animate(withDuration: 0.5) {
// Hide Payment View
self.paymentView.alpha = 0.0
}
}
The first solution is simple. We create an extension for the TimeInterval
struct and define a static, constant property, animationDuration
, of type TimeInterval
. We assign a value to the animationDuration
property and access the value through the TimeInterval
struct. You can optionally use an enum to namespace the static, constant property to avoid naming collisions.
extension TimeInterval {
static let animationDuration: TimeInterval = 0.5
}
As with the previous solution, code completion kicks in and you don't run the risk of making a typo.
func hidePaymentView() {
UIView.animate(withDuration: .animationDuration) {
// Hide Payment View
self.paymentView.alpha = 0.0
}
}
Another more advanced option is the use of an enum. We create an enum with name AnimationDuration
and define several cases, slow
, normal
, and fast
. The raw value of the enum is TimeInterval
.
enum AnimationDuration: TimeInterval {
// MARK: - Cases
case slow = 1.5
case normal = 0.5
case fast = 0.25
}
The code you write becomes more readable and typos are a thing of the past.
func hidePaymentView() {
UIView.animate(withDuration: AnimationDuration.normal.rawValue) {
// Hide Payment View
self.paymentView.alpha = 0.0
}
}
Enums and Properties
Enums can't have stored properties, but they can have computed properties. This is a feature you should take advantage of whenever possible. Let's say we have an enum, Environment
, that defines three cases, development
, staging
, and production
.
enum Environment {
// MARK: - Cases
case development
case staging
case production
}
You could use the enum as is, but that isn't what I recommend. The enum should expose an API that makes it easy to work with the Environment
enum. We start simple by defining three computed properties of type Bool
, isDevelopment
, isStaging
, and isProduction
.
enum Environment {
// MARK: - Cases
case development
case staging
case production
// MARK: - Properties
var isDevelopment: Bool { return self == .development }
var isStaging: Bool { return self == .staging }
var isProduction: Bool { return self == .production }
}
I usually define these computed properties when I need them. They make your code more readable and more concise. This is just a start, though. We can define another computed property, baseUrl
, of type URL
, that returns the base URL of the backend the application communicates with. This avoids code duplication and it makes sense to make the Environment
enum the owner of this information.
enum Environment {
// MARK: - Cases
case development
case staging
case production
// MARK: - Properties
var isDevelopment: Bool { return self == .development }
var isStaging: Bool { return self == .staging }
var isProduction: Bool { return self == .production }
// MARK: -
var baseUrl: URL {
switch self {
case .development:
return URL(string: "http://0.0.0.0:3000")!
case .staging:
return URL(string: "https://staging.cocoacasts.com")!
case .production:
return URL(string: "https://cocoacasts.com")!
}
}
}
The use of computed properties makes even more sense when you're working with an enum with associated values. The DownloadState
enum defines three cases, notDownloaded
, downloading
, and downloaded
. The downloading
case has an associated value of type Float
. It represents the progress of the download operation.
enum DownloadState {
// MARK: - Cases
case notDownloaded
case downloading(Float)
case downloaded
}
It isn't elegant to compare enums using an if
statement if the enum has cases with associated values. Without computed properties, your code can become messy very quickly. Computed properties resolve this problem elegantly. The solution requires a few additional lines of code since we are dealing with an enum with associated values.
enum DownloadState {
// MARK: - Cases
case notDownloaded
case downloading(Float)
case downloaded
// MARK: - Properties
var isNotDownloaded: Bool {
switch self {
case .notDownloaded:
return true
default:
return false
}
}
var isDownloading: Bool {
switch self {
case .downloading:
return true
default:
return false
}
}
var isDownloaded: Bool {
switch self {
case .downloaded:
return true
default:
return false
}
}
}
Enums and computed properties are a powerful combination and I recommend using them whenever it makes sense.
What's Next?
Swift continues to evolve and I try to improve the code I write every single day. It is satisfying to clean up a complex piece of code, but it also improves the readability of the code you write and the maintainability of the project.