One minute Swift reads to get Swifty-er
Updated for Swift 5.8
May 26, 2023
As of Swift 5.7, there is a new shorthand for unwrapping optional values for later use.
func greet(userName: String?) {
// Previous way -> if let userName = userName
if let userName {
print("Hello \(userName)!")
} else {
print("Hello!")
}
}
func addMoney(amount: Double?, to otherAmount: Double) -> Double {
// Previous way -> guard let amount = amount else {...
guard let amount else { return otherAmount }
return amount + otherAmount
}
May 20, 2023
Swift ships with several ways to get random values ranging from picking a number within a range to random within an array that you've defined.
let numberLine = Double.random(in: 1 ... 100)
let flipCoin = Bool.random()
let rollDie = Int.random(in: 1 ... 6)
let drinks = ["water", "cola", "apple juice", "orange juice"]
let drinkRoulette = drinks.randomElement()
May 12, 2023
The typical way to get values out of a dictionary is via subscript using the dictionary's "key." This access method returns an optional value in return since the key may not exist. If you want a "fallback" value when the key doesn't exist, you can provide default value for that situation in the subscript. Also in this case, you don't have to deal with the returned value being wrapped in an optional!
enum FoodGroup {
case fruits, grains, protein, vegetables
}
// no food to eat :(
let availableFood: [FoodGroup: [String]] = [:]
// Using a default value - we don't have to deal with the value being optional
func quantityToEat(for group: FoodGroup) -> Int {
availableFood[group, default: []].count
}
// Without a default value - we need to deal with the optional
func quantityToEat2(for group: FoodGroup) -> Int {
(availableFood[group] ?? []).count
}
April 26, 2023
A lot of backend APIs expect JSON to use snake case (e.g., "user_id"), but the Swift convention is to use camel case (e.g. "userId"). Turns out there is a simple way to convert between the two when generating JSON.
import Foundation
struct Transaction: Encodable {
let id: String
let userId: String
let startDate: Date
let endDate: Date
}
func uploadLatestsTransaction() {
// get the latest transaction
let transaction = Transaction(id: "123", userId: "456", startDate: Date(), endDate: Date())
let jsonEncoder = JSONEncoder()
// >>> This line tells the encoder to convert between the two styles <<<
jsonEncoder.keyEncodingStrategy = .convertToSnakeCase
guard let jsonData = try? jsonEncoder.encode(transaction) else { return }
print(String(decoding: jsonData, as: UTF8.self))
/* e.g.,
{
"id":"123",
"end_date":704374697.15610504,
"user_id":"456",
"start_date":704374697.15610504
}
*/
// upload `jsonData`
}
April 21, 2023
Swift's type system is powerful. As long as the type on the right hand side can be inferred, you can leave off the type from the left when declaring a constant or variable.
// Leveraging Swift's type inference (Swifty-er!)
let seven = 7 // type is an int
let sevenPointZero = 7.0 // type is a double
let luckyNumbers = [7, 7, 7] // type is an Array<Int>
// Mostly bias away from doing this
let five: Int = 5
let fivePointZero: Double = 5.0
let maybeLuckyNumbers: [Int] = [5, 5, 5]
April 15, 2023
This can be a "scary" subject/phrase, but it really boils down to something quite simple and nice: Give your entities (e.g., classes, structs, functions) what they "need" to do their job. You "give" your entities what they "need" via functions like initializers. If you do, reasoning about your code, writing tests, and future support will be much easier.
import Foundation
struct Event {
let Date: Date
}
func time(fromNow now: () -> Date, to event: Event) -> TimeInterval {
// Injecting a way to get the current date will be useful for testing, it makes
// the dependency more obvious for this function, and helps the readability.
// By "injecting" the current time, we can "control" the current time, the resulting
// returned time interval, and create a reliable test for the internals here.
// Calculate the time interval
.zero
}
April 8, 2023
Optionals are great, however information can be lost when used to represent the absence of something. An enum with as few as two cases can be very useful in conveying why something is missing, and is more accomidating in the future as things change.
struct Profile {}
struct Stats {}
// Weak - Using a returned optional hides failure or error information
func stats(for profile: Profile) -> Stats? {
// Get stats (e.g., database, network, cache)
// If the above fails, the returned value of nil hides why
nil
}
// Preferred - Using an enum helps convey more information
enum RetrievedStats {
case stats(Stats)
case noneFound
// More cases can be easily added over time as new ways to get stats are added!
case databaseConnectionIssue
case networkFailure // maybe with status code
}
func stats(for profile: Profile) -> RetrievedStats {
// Get stats (e.g., database, network, cache)
.noneFound
}
March 31, 2023
The "willSet" and "didSet" property observers will get called every time a property is changed. This allows you react to changes in a property's value. E.g., saving that value to disk each time it is changed. They are not called on init however.
struct ViewData {
let title: String
let onAccept: () -> Void
var userInput: String? {
// Called before _userInput_ is set (the new value is provided)
// Note: "newValue" can be renamed like this -> willSet(<name>)
willSet {
print("willSet: \(newValue ?? "nil")")
}
// Called after a _userInput_ is set (the old value is provided)
// Note: "oldValue" can be renamed like this -> didSet(<name>)
didSet {
print("didSet: \(oldValue ?? "nil")")
}
}
}
March 25, 2023
Swift offers different ways to get the first element from an array depending on what you want to accomplish. For example, we can remove the first item from an array while returning it for use.
struct Follower {
// ...
}
func firstFollower() -> Follower {
// get followers from somewhere like a backend API call
var followers: [Follower] = [.init(), .init(), .init()]
// == Way 1 ==
// Using a subscript is a common way languages support getting the first item.
// Note: This can crash if the array is empty.
let firstFollower = followers[0]
// == Way 2 ==
// Use this to mutate the array itself and return the removed item for use.
// Note: This can crash if the array is empty.
let _ = followers.removeFirst()
// == Way 3 ==
// This is the safest way to get the first element since the returned item
// is optional.
// Note: This won't mutate the original array. Also, it will return nil if the
// array is empty.
let _ = followers.first
return firstFollower
}
March 17, 2023
Optionals are a powerful language construct that force us to address if something exists or not, but did you know that it's just an enum with two cases behind the scenes?
func printName() {
let name: String? = Optional.some("Bob")
switch name {
case .none:
print("Name doesn't exist")
case let .some(name):
print("Name is: \(name)")
}
}
March 10, 2023
One way to get the most out of enums in Swift is taking advantage of exhaustive switch statement checking. The compiler can check to ensure all cases are handled in a switch statement. This means if you add another case to an enum, the compiler will "guide" you through addressing that case addition by raising errors where that new case isn't handled. Using a "default" will silently handle that new enum case, which may not be what you want.
enum Shapes {
case square
case circle
case triangle
// Using a default can lead to future bugs.
// For example, adding a "pentagon" case would by default
// return false below.
var hasMoreThan3Sides: Bool {
switch self {
case .square: return true
default: return false
}
}
// This version is much better. The compiler will "ask"
// you, via an error, to address how many sides that new
// shape has below (aka Switch must be exhaustive).
var betterHasMoreThan3Sides: Bool {
switch self {
case .square: return true
case .circle, .triangle: return false
}
}
}
March 3, 2023
When you have a do/try/catch, there is an implicit error available in the "catch block" that can be used to better understand what went wrong. That local implicit error can also be explicitly named if desired.
import Foundation
// Implicit error in the catch block
func retrievePost(url: URL) {
do {
let post = try String(contentsOf: url)
print(post) // do something with the post
} catch {
// notice no error was defined
print(error.localizedDescription)
}
}
// The error can be explicitly named
func retrievePost2(url: URL) {
do {
let post = try String(contentsOf: url)
print(post) // do something with the post
} catch let postError {
print(postError.localizedDescription)
}
}
February 24, 2023
As an alternative to a full switch statement to match an enum and get an enum's associated value out, a regular if statement can be used for a single case at a time. This is handy to know, but the "if statement" syntax to check an enum and get its associated value is tricky and hard to remember. Is it "if case let variable = enum" or "if let case enum = variable" or...?
enum Animal {
case dog(breed: String)
case cat
}
// If you want an enum's associated value
func breedIfDog(_ maybeDog: Animal) -> String? {
if case let .dog(breed) = maybeDog {
return breed
}
return nil
}
// Or, if you don't want the associated value
func isDog(_ maybeDog: Animal) -> Bool {
// a `guard` statement might be better here (e.g., "guard case .dog = maybeDog else { return false }")
if case .dog = maybeDog {
return true
} else {
return false
}
}
February 17, 2023
If you need to transform an optional when the value exists, making use of the higher order "map" function on optionals will make your code shorter and clearer. The map function on optional will apply a function to transform a value if it exists, then pack it back up in an optional.
// Without using "map"
func upperCase(userInput input: String?) -> String? {
if let input {
return input.uppercased()
}
return nil
}
// Using "map"
func upperCase2(userInput input: String?) -> String? {
// this can probably just be inlined where needed now
input.map { $0.uppercased() }
}
February 10, 2023
The guard statement allows execution to continue if the condition provided is met, or early returns if that condition is not met. Using a guard, we can easily know by inspection that a statement may return early, and therefore better understand the control flow. This is because the compiler "checks" and enforces an early return for guard statements.
enum PasswordValidation: Error {
case invalidAndUnknown
}
func isPasswordValid(_ password: String?) throws -> Bool {
// Throwing is a possible way to guarantee an early exit (if the function is throwing)
// If password isn't nil, the guard passes and unwraps "password" to be used as non-optional
guard let password else { throw PasswordValidation.invalidAndUnknown }
// These guards provide boolean early returns (per the return type of this function)
guard password.isEmpty else { return false }
guard password.count > 8 else { return false }
// The compiler can still guarantee that this guard will exit early - because this will crash...
guard password == "1234" else { fatalError() }
// Below will not compile because there is no early exit in the "else" clause
// Error: 'guard' body must not fall through, consider using a 'return' or 'throw' to exit the scope
// guard password.contains(where: { char in char == "!"}) else { }
return true
}
February 3, 2023
Using `.enumerated()` on an array will return a pair of index & element for use in each iteration of the loop.
Note: Not all collections use zero-based and integer indicies. For example, if you have a set, then `.enumerated()` won't function the same. There isn't an order to sets. In this case, the "index" mentioned above is more of a "counter" for an undefined order.
let favoriteColors = ["Blue", "Yellow", "Red", "Orange"]
func printFavoriteColors() {
// Using enumerated: for loop
for (index, favorite) in favoriteColors.enumerated() {
let place = index + 1 // add 1 since enumerated starts at 0
print("\(place): " + favorite) // 1: Blue, 2: Yellow, 3: Red, 4: Orange
}
// Using enumerated: `forEach` higher-order function
favoriteColors.enumerated().forEach { index, favorite in
let place = index + 1
print("\(place): " + favorite) // same result as the for loop
}
}
January 27, 2023
We can make code a little cleaner by using point-free style. This means that we avoid referring to the value in a function (aka a "point"). Key paths let us use this style since they allow us to access a nested property.
import Foundation
// example entity
struct User {
let id: UUID
let name: String
let email: String
}
extension User {
// convenience
static var randomId: User {
User(
id: UUID(),
name: "Rob",
email: "[email protected]"
)
}
}
let users: [User] = [.randomId, .randomId, .randomId, .randomId]
// This is not point-free since the value `$0` is used
let userIds = users.map { $0.id }
// No mention of the value on which the function is acting
let userIds2 = users.map(\.id)
January 20, 2023
Constants (i.e., "let"s) can have a deferred assignment (aka definite initialization) as long as the compiler can guarantee that it is assigned in every code path and only once. Useful to avoid using a "var", and a safer pattern to use since a constant can't be changed later.
import Foundation
enum Route {
case url(URL)
case fallback
}
func showStats(using route: Route) {
// Deferred constant assignment
let url: URL // The compiler can guarantee that this is assigned below
switch route {
case let .url(routeUrl):
url = routeUrl
case .fallback:
url = URL(string: "api.example.com")! // (force unwraps should be avoided)
}
// Make use of the url
print(url)
}
January 13, 2023
Constants and Variables need to start with the "let" and "var" keywords respectively. Constants cannot be changed after being set (immutable) while variables can be changed (mutable).
// Constant
let name1 = "Swifty"
// constantName = "SwiftyNotes" // <- The Swift compiler won't allow this
// Variable
var name2 = "Swifty"
func changeName() {
name2 = "SwiftyNotes" // <- Change the variable
}