My personal collection of tips, tricks, and patterns I've learned during iOS development so far and do not want to forget.
Feedback is always welcome! Feel free to reach out π
#65 β Tracking geometry changes in SwiftUI
#64 β Responding to enabled state in a custom ButtonStyle
#63 β Animating text color in SwiftUI
#62 β Creating custom localized date formats
#61 β Animate isHidden in a UIStackView
#60 β Making types expressible by literals
#59 β Customizing toggles with ToggleStyle in SwiftUI
#58 β Determining a view's size with Auto Layout
#57 β Decode array while filtering invalid entries
#56 β Codable cheat sheet
#55 β Respecting safe areas in SwiftUI while extending backgrounds
#54 β Rendering basic HTML tags in SwiftUI's Text
#53 β Combining Text views in SwiftUI
#52 β Animate a UITableView reload
#51 β Integrating Redux with SwiftUI
#50 β Exploring Combine: A couple of practical examples
#49 β Effortless unit conversion with Measurement
#48 β FloatingPoint protocol
#47 β Wait for multiple async tasks to complete
#46 β Snapshot testing
#45 β Pin a view to its superview
#44 β Animating with custom timing curves
#43 β Testing delegate protocols in Swift
#42 β Xcode multi-cursor editing
#41 β Create a dynamic color for light- and dark mode
#40 β Derive reuse identifiers from UITableViewCell type
#39 β Prefer "for .. in .. where" over filter() followed by forEach {}
#38 β Lightweight observable implementation
#37 β Running test cases in a playground
#36 β Displaying WKWebView loading progress with UIProgressView
#35 β Destructure tuples
#34 β Avoid huge if statements
#33 β Compare dates in tests
#32 β Understand the strong reference behavior of Timer targets
#31 β Initialize DateFormatter with formatting options
#30 β Mapping latitude and longitude to X and Y on a coordinate system
#29 β Encapsulation
#28 β Remove UITextView default padding
#27 β Name that color
#26 β Structure classes using // MARK: -
#25 β Structure test cases
#24 β Avoid forced unwrapping
#23 β Guard against division by zero
#22 β Animate alpha and update isHidden accordingly
#21 β Define a custom notification
#20 β Overriding UIStatusBarStyle the elegant way
#19 β Log extension on String using Swift literal expressions
#18 β Use Gitmoji for commit messages
#17 β Initialize a constant conditionally
#16 β Why viewDidLoad can be called before initialization completes
#15 β Capture iOS Simulator video
#14 β Xcode shortcuts
#13 β Handle optionals in test cases
#12 β Safe access to an element at index
#11 β Check whether a value is part of a given range
#10 β Use compactMap to filter nil values
#09 β Prefer Set instead of Array for unordered lists without duplicates
#08 β Adding and removing child view controllers
#07 β Animate image change on UIImageView
#06 β Change CALayer without animation
#05 β Override layerClass to reduce the total amount of layers
#04 β Handle notifications in test cases
#03 β Use didSet on outlets to set up components
#02 β A readable way to check whether a value exists in a set of candidates (isAny(of:))
#01 β Memory management: weak self in closures vs. tasks
π Starting in iOS 16, SwiftUI provides the onGeometryChange(for:of:action:) view modifier, which lets you respond to changes in a viewβs geometry β such as its size or position.
In the example below, the width of a rounded rectangle automatically matches the width of a Text view. As the textβs layout changes, SwiftUI updates the rectangle in sync.
struct ContentView: View {
@State
private var textSize: CGSize = .zero
var body: some View {
VStack {
Text("Hello World!")
.onGeometryChange(for: CGSize.self, of: \.size) { textSize in
self.textSize = textSize
}
RoundedRectangle(cornerRadius: 4)
.frame(
width: textSize.width,
height: 8,
)
.foregroundStyle(.indigo)
}
}
}The same approach can be used to track other geometry values, such as a scroll position. A complete example demonstrating scroll offset tracking is available here:
https://gist.github.com/fxm90/5bc949e4d6f2f56901b47250a25fc64d
π¨ The ButtonStyle protocol makes it easy to define consistent, reusable button designs across your app, without repeating code.
However, there is one subtle detail: a ButtonStyle doesnβt have direct access to the isEnabled environment value.
When you need to adjust your styling based on whether a button is enabled, you can move that logic into a supporting View. Because views participate fully in SwiftUIβs environment system, they can read @Environment(\.isEnabled) and adapt accordingly.
Hereβs one way to structure it:
struct PrimaryButtonStyle: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
PrimaryButtonStyleView(configuration: configuration)
}
}
private extension PrimaryButtonStyle {
struct PrimaryButtonStyleView: View {
// MARK: - Public Properties
let configuration: ButtonStyle.Configuration
// MARK: - Private Properties
@Environment(\.isEnabled)
private var isEnabled: Bool
private var foregroundColor: Color {
guard isEnabled else {
return .gray
}
return configuration.isPressed
? .white.opacity(0.5)
: .white
}
// MARK: - Render
var body: some View {
configuration.label
.foregroundColor(foregroundColor)
}
}
}In this approach, the style delegates its rendering to a view that reads from the environment. This allows the buttonβs appearance to automatically reflect its enabled state.
π¨ SwiftUI makes it easy to animate many visual properties. However, foregroundColor(_:) isnβt directly animatable.
When you need to smoothly transition text between colors, thereβs a simple workaround.
Instead of animating foregroundColor, apply a neutral base color (such as .white) and animate the colorMultiply(_:) modifier. Because colorMultiply participates in SwiftUIβs animation system, the color transition becomes fluid and seamless.
struct AnimateTextColor: View {
// MARK: - Private Properties
@State
private var textColor: Color = .red
// MARK: - Render
var body: some View {
Text("Lorem Ipsum Dolor Sit Amet.")
.foregroundColor(.white)
.colorMultiply(textColor)
.onTapGesture {
withAnimation(.easeInOut) {
textColor = .blue
}
}
}
}π When presenting dates in your app, it's important to consider the user's locale. Month names, day order, and punctuation can vary significantly across regions. Rather than hard-coding a format string, you can generate one dynamically using dateFormat(fromTemplate:options:locale:).
This approach lets the system determine the correct ordering and formatting for a given locale, based on a template like MMMd.
Hereβs a convenient Date extension that wraps this behavior:
extension Date {
/// Returns a localized string representation of the date,
/// generated from the provided date format template and locale.
///
/// - Parameters:
/// - template: A date format template (for example, "MMMd" or "yMMMMd").
/// - locale: The locale that determines the final date format.
///
/// - Returns: A locale-aware formatted date string.
func localizedString(from template: String, for locale: Locale) -> String {
let dateFormatter = DateFormatter()
dateFormatter.locale = locale
if let dateFormat = DateFormatter.dateFormat(
fromTemplate: template,
options: 0,
locale: locale
) {
dateFormatter.dateFormat = dateFormat
}
return dateFormatter.string(from: self)
}
}let template = "MMMd"
let now: Date = .now
let usLocale = Locale(identifier: "en_US")
print("United States:", now.localizedString(from: template, for: usLocale))
// United States: Oct 1
let deLocale = Locale(identifier: "de")
print("Germany:", now.localizedString(from: template, for: deLocale))
// Germany: 1. Okt.#61 β Animate isHidden in a UIStackView
π§ββοΈ When working with UIStackView, animating the visibility of an arranged subview is straightforward.
Because a stack view automatically manages the layout of its arranged subviews, changes to the isHidden property can be animated seamlessly alongside layout updates.
For example, setting isHidden to true removes the view from the stackβs layout, allowing the remaining content to smoothly adjust its position.
UIView.animate(withDuration: 0.3) {
viewInsideStackView.isHidden = true
stackView.layoutIfNeeded()
}By calling layoutIfNeeded() inside the animation block, the stack view animates to its updated layout, producing a smooth slide-out effect as the hidden view collapses within the stack.
π Swift includes a family of protocols that allow your custom types to be initialized using familiar literal syntax. This makes APIs feel natural, expressive, and consistent with the language itself.
For example, many standard library types conform to literal protocols:
let int = 0 // ExpressibleByIntegerLiteral
let string = "Hello World!" // ExpressibleByStringLiteral
let array = [0, 1, 2, 3, 4, 5] // ExpressibleByArrayLiteral
let dictionary = ["Key": "Value"] // ExpressibleByDictionaryLiteral
let boolean = true // ExpressibleByBooleanLiteralA complete list of these protocols can be found in the documentation: Initialization with Literals
Literal protocols are especially powerful when applied to your own types. By conforming to them, you enable readable initialization without sacrificing type safety.
Consider a simple StorageKey type:
struct StorageKey {
let path: String
}By conforming to ExpressibleByStringLiteral and ExpressibleByStringInterpolation, you can initialize StorageKey directly from string literals:
extension StorageKey: ExpressibleByStringLiteral, ExpressibleByStringInterpolation {
init(stringLiteral path: String) {
self.init(path: path)
}
}Now you can create instances using natural string syntax:
let storageKey: StorageKey = "/cache/"And because it also supports string interpolation:
let username = "f.mau"
let storageKey: StorageKey = "/users/\(username)/cache"This approach can also improve ergonomics when working with existing types. For example, you can make URL conform to ExpressibleByStringLiteral:
extension URL: ExpressibleByStringLiteral {
/// Initializes a URL from a string literal.
///
/// Example:
/// ```
/// let url: URL = "https://felix.hamburg"
/// ```
public init(stringLiteral value: StaticString) {
guard let url = URL(string: "\(value)") else {
fatalError("β οΈ β Failed to create a valid URL instance from `\(value)`.")
}
self = url
}
}In this case, the conformance is intentionally limited to ExpressibleByStringLiteral, using StaticString. This ensures only compile-time string literals are accepted, avoiding runtime failures from dynamic string interpolation.
Based on:
- Defining static URLs using string literals
- Making types expressible by string interpolation
- Expressible literals in Swift explained by 3 useful examples
π¨ SwiftUI provides a ToggleStyle protocol, giving you full control over the appearance and interaction of a Toggle.
By adopting this protocol, you can design a toggle that aligns perfectly with your appβs visual language β whether thatβs a refined switch, a checkbox, or something entirely unique.
When you create a custom toggle style, you take responsibility for rendering and managing its visual state.
The required method, makeBody(configuration:), provides a configuration value that includes:
configuration.isOn: A Boolean that reflects the current state of the toggle.configuration.label: The view representing the toggleβs label.
Because you are defining the entire visual representation, you are also responsible for clearly communicating the toggleβs state to the user.
The following examples demonstrate custom ToggleStyle implementations, complete with screenshots in the comments:
- A fully configurable toggle style for SwiftUI
https://gist.github.com/fxm90/6afe050ac331d8f719029d7fec87e961 - A toggle style for SwiftUI, making the Toggle look like a checkbox
https://gist.github.com/fxm90/b56d537d9fb8bf20d573a45367e18c4f
When you want to determine the optimal size of a view based on its constraints, UIView provides the systemLayoutSizeFitting(_:) method.
For example, if you know the width of a view and want to determine the height required to fit its content, you can specify a fixed horizontal dimension and allow Auto Layout to calculate the vertical one:
let size = view.systemLayoutSizeFitting(
CGSize(width: view.bounds.width, height: UIView.layoutFittingCompressedSize.height),
withHorizontalFittingPriority: .required,
verticalFittingPriority: .fittingSizeLevel
)In this configuration:
- The horizontal fitting priority is set to
.required, ensuring the width remains fixed. - The vertical fitting priority is set to
.fittingSizeLevel, allowing Auto Layout to determine the height that best fits the content.
πͺ Ideally, an API has a well-defined interface and the app knows exactly which data to expect. However, there are cases when you can't be 100% sure about a response.
Consider fetching a list of flights for an airport. If one flight includes an incorrectly formatted departure date, you likely donβt want the entire response to fail decoding. Instead, you may prefer to keep the valid flights and discard the problematic entry.
To support this pattern, you can introduce a lightweight wrapper that attempts to decode a value while allowing individual failures to resolve to nil. This enables partial success when decoding collections of potentially unreliable data.
/// A wrapper that attempts to decode a value of type `Base` but gracefully degrades to `nil` if decoding fails.
///
/// `FailableDecodable` is useful when working with unreliable or partially-invalid data (e.g. third-party APIs)
/// where you want decoding to continue even if a single field is malformed.
///
/// - Warning: Because decoding errors are swallowed, this can mask schema or data issues.
/// Use sparingly and only when partial failure is acceptable.
///
/// Source: <https://stackoverflow.com/a/46369152/3532505>
struct FailableDecodable<Base: Decodable>: Decodable {
/// The successfully decoded value, or `nil` if decoding failed.
let base: Base?
/// Attempts to decode `Base` from a single-value container.
/// If decoding throws, the error is ignored and `base` is set to `nil`.
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
base = try? container.decode(Base.self)
}
}In this example, we decode the array of Flights as FailableDecodable<Flight>. This way, invalid elements don't cause the entire decoding to fail β only the base property will be nil on failure.
Afterwards, we use compactMap(\.base) to filter out entries where base is nil.
/// Data Model
struct Flight: Decodable {
let number: String
let departure: Date
}
/// HTTP Client Method
func fetchDepartures(for url: URL) async throws -> [Flight] {
let (data, _) = try await URLSession.shared.data(from: url)
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let decodedFlights = try decoder.decode([FailableDecodable<Flight>].self, from: data)
return decodedFlights.compactMap(\.base)
}π Working with JSON data is a fundamental part of modern app development. Swift's Codable protocol provides a type-safe way to convert between your Swift data types and external representations like JSON.
Paul Hudson has created a helpful Codable cheat sheet that walks through the essentials β from simple encoding and decoding to handling more advanced scenarios.
π² In SwiftUI, itβs common to want a viewβs content to respect the deviceβs safe areas while allowing the background to extend to the edges of the screen. This pattern ensures your layout feels natural on all devices, from iPhones with notches to iPads with rounded corners.
Hereβs a simple example:
struct FullScreenBackgroundView: View {
var body: some View {
Text("Hello, World!")
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
.background(
Color.red.ignoresSafeArea()
)
}
}
#Preview {
FullScreenBackgroundView()
}In this example:
- The
Textview respects the safe area, staying visible and readable. - The
Color.redbackground ignores safe area insets, creating a full-bleed background effect.
This approach is a great way to combine a polished, safe content layout with immersive backgrounds that span the entire screen.
π With iOS 15, Text now fully embraces AttributedString, bringing rich text capabilities and Markdown support directly into your views.
This makes it simple to display content that originates from HTML. By mapping basic HTML tags to Markdown, you can render styled text and even interactive hyperlinks in SwiftUI.
For a practical example, see this SwiftUI+HTML.swift snippet, which demonstrates converting HTML to an AttributedString ready for SwiftUIβs Text.
π§ββοΈ In SwiftUI, you can concatenate multiple Text views using the + operator. This allows you to style each portion of text independently while presenting them as a single cohesive line.
Text("Hello ")
.foregroundStyle(.red)
+
Text("World")
.foregroundStyle(.green)
+
Text("!")Each Text segment retains its own modifiers, giving you fine-grained control over appearance and style.
Note: The plus operator for Text concatenation has been deprecated in iOS 26, and Apple recommends using text interpolation instead:
Text(
"""
\(Text("Hello ")
.foregroundStyle(.red))\
\(Text("World")
.foregroundStyle(.green))\
\(Text("!"))
"""
)π Refreshing the contents of a UITableView can be enhanced with smooth animations using UIView transitions.
By invoking tableView.reloadData() inside the animation block of UIView.transition(with:duration:options:animations:completion:), you can create a seamless, crossfade effect when updating table view cells.
UIView.transition(
with: tableView,
duration: 0.3,
options: .transitionCrossDissolve,
animations: { self.tableView.reloadData() }
)You can experiment with any of the UIView.AnimationOptions to achieve different transition effects.
Source: https://stackoverflow.com/a/13261683
π SwiftUIβs declarative design makes state management a core part of building robust apps. In this example, we explore how to implement a simple Redux-style architecture directly in SwiftUI, without relying on external frameworks.
Check out the full gist here: Redux.swift
You can easily copy the code into an Xcode Playground to experiment with state flow and see Redux in action. This is a great way to understand unidirectional data flow in SwiftUI.
π§ͺ Combine provides a declarative Swift API for processing values over time, making it easier to work with asynchronous events. To help you get started, here are two practical examples that illustrate common Combine patterns:
- PassthroughSubject vs. CurrentValueSubject
This example demonstrates the difference betweenPassthroughSubjectandCurrentValueSubject, two foundational building blocks in Combine for emitting and observing values over time. - Bridging Delegates to Combine
Here, youβll see how to convert a traditional delegate pattern into Combine publishers, usingCLLocationManagerDelegateas an example. This pattern makes it easier to integrate existing APIs with the reactive Combine framework.
Feel free to copy the code to a playground and get your hands dirty with Combine π
π With iOS 10 and later, Swift provides a unified and type-safe way to work with measurements through Measurement.
Whether youβre dealing with angles, areas, durations, speeds, temperatures, volumes, or other dimensions, Measurement makes conversions straightforward and expressive.
For example, using Measurement<UnitAngle> we can refactor the computed property shown in note #48 into a method that converts between any UnitAngle:
extension BinaryFloatingPoint {
/// Converts a value from one `UnitAngle` to another.
func converted(from fromUnit: UnitAngle, to toUnit: UnitAngle) -> Self {
let valueAsDouble = Double(self)
let convertedValue = Measurement(value: valueAsDouble, unit: fromUnit)
.converted(to: toUnit)
.value
return Self(convertedValue)
}
}This approach leads to a very clean call site:
let cameraBearing: CLLocationDegrees = 180
let bearingInRadians = cameraBearing.converted(from: .degrees, to: .radians)π² Swiftβs protocols are incredibly powerful. By extending the FloatingPoint protocol, you can seamlessly add functionality to all floating-point types (e.g. Double, Float, CGFloat) without writing repetitive code.
For example, converting degrees to radians is a common task in graphics and animations. With a simple protocol extension, you can make this conversion available on any floating-point value:
extension FloatingPoint {
/// Converts an angle in degrees to radians.
var degreesToRadians: Self {
self * .pi / 180
}
}
let angleDouble: Double = 90
let angleFloat: Float = 180
let angleCGFloat: CGFloat = 270
print("Double in radians:", angleDouble.degreesToRadians)
print("Float in radians:", angleFloat.degreesToRadians)
print("CGFloat in radians:", angleCGFloat.degreesToRadians)β° Apps frequently need to fetch data from multiple sources before updating the interface.
DispatchGroup allows you to track a collection of asynchronous tasks and receive a callback once theyβve all completed.
let dispatchGroup = DispatchGroup()
var profile: Profile?
dispatchGroup.enter()
profileService.fetchProfile {
profile = $0
dispatchGroup.leave()
}
var friends: Friends?
dispatchGroup.enter()
profileService.fetchFriends {
friends = $0
dispatchGroup.leave()
}
dispatchGroup.notify(queue: .main) {
guard
let profile = profile,
let friends = friends
else {
return
}
print("We've downloaded the user profile together with all friends!")
}Starting with iOS 13, Swift offers more expressive tools for handling asynchronous coordination.
If your APIs return Combine publishers, you can use Publishers.CombineLatest to wait until each publisher emits at least one value.
let fetchProfileFuture = profileService.fetchProfile()
let fetchFriendsFuture = profileService.fetchFriends()
cancellable = Publishers.CombineLatest(fetchProfileFuture, fetchFriendsFuture)
.sink { profile, friends in
print("We've downloaded the user profile together with all friends!")
}CombineLatest produces a tuple containing the latest values from both publishers once each has emitted.
Swiftβs structured concurrency model provides an even more concise and readable approach. With async let, you can start multiple asynchronous operations concurrently and await their results together.
Task {
async let profileTask = profileService.fetchProfile()
async let friendsTask = profileService.fetchFriends()
let (profile, friends) = await(profileTask, friendsTask)
print("We've downloaded the user profile together with all friends!")
}This approach keeps related asynchronous work clearly scoped and eliminates the need for manual bookkeeping. Itβs the preferred solution for modern Swift codebases targeting iOS 13 and later.
πΈ Snapshot tests are a powerful way to ensure your interface looks exactly the way you expect β and continues to do so over time.
As your app evolves, even small changes can introduce subtle visual regressions. Snapshot testing helps you catch those changes early by capturing a reference image (or representation) of your UI and comparing it against future test runs.
With the open-source SnapshotTesting library from Point-Free, you can verify snapshots of UIView, UIViewController, UIImage, and even URLRequest instances.
βοΈ A common pattern in Auto Layout is anchoring a view so it fully spans its container. With a small extension on UIView, you can make this intent reusable throughout your project.
This helper method pins a viewβs edges to its superview with optional spacing, reducing boilerplate while keeping your layout code easy to read.
extension UIView {
/// Constrains the viewβs edges to match its superviewβs edges.
/// - Parameter spacing: Optional inset applied to all edges. Defaults to 0.
func fillToSuperview(spacing: CGFloat = 0) {
guard let superview = superview else { return }
translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
topAnchor.constraint(equalTo: superview.topAnchor, constant: spacing),
leadingAnchor.constraint(equalTo: superview.leadingAnchor, constant: spacing),
superview.bottomAnchor.constraint(equalTo: bottomAnchor, constant: spacing),
superview.trailingAnchor.constraint(equalTo: trailingAnchor, constant: spacing)
])
}
}π Starting with iOS 10, UIViewPropertyAnimator gives you precise, interruptible control over your animations.
While the built-in timing curves such as .easeInOut work beautifully in many cases, there are times when you want to craft a more distinctive motion.
By using the init(duration:timingParameters:) initializer, you can provide your own object conforming to UITimingCurveProvider and define a fully custom timing curve.
One convenient way to do this is with UICubicTimingParameters, which lets you specify BΓ©zier control points for fine-tuned motion.
If youβre looking for inspiration, resources like Easings.net provide a variety of well-known timing curves that can help you achieve a specific feel.
For example, hereβs how you could implement an βeaseInBackβ curve β a motion that briefly moves in the opposite direction before accelerating forward:
extension UICubicTimingParameters {
static let easeInBack = UICubicTimingParameters(
controlPoint1: CGPoint(x: 0.6, y: -0.28),
controlPoint2: CGPoint(x: 0.735, y: 0.045)
)
}
final class CustomTimingAnimationViewController: UIViewController {
// ...
func userDidTapButton() {
let animator = UIViewPropertyAnimator(
duration: 1.0,
timingParameters: UICubicTimingParameters.easeInBack
)
animator.addAnimations {
// Update constraints, transforms, alpha, or other animatable properties.
// For example:
// self.someConstraint?.isActive = false
// self.someOtherConstraint?.isActive = true
// self.view.layoutIfNeeded()
}
animator.startAnimation()
}
}Because UIViewPropertyAnimator is interruptible and fully controllable, you can pause, reverse, or scrub through the animation as needed.
π§ͺ Delegation is a common Swift design pattern, enabling one-to-one communication between objects in a clean and modular way.
When building apps, it's essential to ensure that delegate callbacks are triggered correctly. One approach is to use a mock in your tests. By implementing an enum to track invoked methods, you can verify that e.g. your view models interact with their delegates exactly as expected.
Explore a practical example of this approach in action: Testing a Delegate Protocol with a Mock
πβ Since Xcode 10, the Source Editor has included multi-cursor support, making it easier than ever to edit multiple locations in your code simultaneously. This feature lets you insert, delete, or modify code across several lines at once, streamlining repetitive edits and improving your workflow.
To add additional cursors, simply use:
shift + control + click
shift + control + β
shift + control + β
π¨ Using the helper in UIColor+MakeDynamicColor.swift, we can define a custom UIColor that adapts automatically to the current userInterfaceStyle.
The color resolves itself at runtime, seamlessly matching Light or Dark Mode as the interface appearance changes.
On systems earlier than iOS 13, the implementation gracefully defaults to the provided light variant, ensuring consistent behavior across all supported OS versions.
π§ββοΈ When working with table views, reuse identifiers are an essential detail. Defining them as string literals, however, introduces unnecessary duplication and the risk of subtle typos.
You can eliminate both by deriving the reuse identifier directly from the cellβs type. The following extension adds a static identifier to UITableViewCell that reflects the class name automatically:
extension UITableViewCell {
static var identifier: String {
String(describing: self)
}
}With this in place, registering and dequeuing cells becomes simpler and more consistent.
Registering a cell:
tableView.register(CustomTableViewCell.self, forCellReuseIdentifier: CustomTableViewCell.identifier)Dequeuing a cell:
let cell = tableView.dequeueReusableCell(withIdentifier: CustomTableViewCell.identifier)π’ In performance-sensitive code, a for-in-where loop is often more efficient than chaining filter() with forEach, as it performs the conditional check during iteration rather than requiring an additional pass over the collection.
For example, instead of writing:
scooterList
.filter { !$0.isBatteryEmpty }
.forEach { scooter in
// Operate on each scooter with remaining battery.
}you can express the same intent more efficiently using a for-in-where loop:
for scooter in scooterList where !scooter.isBatteryEmpty {
// Operate on each scooter with remaining battery.
}This approach avoids the creation of an intermediate collection and can be significantly faster when working with large arrays.
π΅οΈββοΈ For a simple and lightweight observable implementation β suitable for UI bindings and similar use cases β refer to the LightweightObservable framework (also available as a CocoaPod).
Update 2026: Over time, Apple has introduced several frameworks that support reactive and asynchronous programming. Depending on your deployment target, consider adopting one of the following technologies:
- Combine (iOS 13.0+)
A declarative Swift API for processing values over time. - Async Sequence (iOS 13.0+)
A protocol that enables asynchronous iteration using Swiftβs concurrency features. - Observation (iOS 17.0+)
A modern observation system designed to integrate seamlessly with Swift.
In UIKit-based apps, you can respond to observation-driven changes by overriding theupdateProperties()lifecycle method (iOS 26.0+).
π§ͺ Swift Playgrounds are an easy way to explore ideas. As you prototype, itβs often useful to think through the expected behavior up front β or even take a test-driven approach from the start.
You can run XCTest-based test cases directly inside a playground by invoking the test suite explicitly. This makes it easy to validate behavior early, then move the code into your app or framework when itβs ready.
import XCTest
final class MyTestCase: XCTestCase {
func testFooBarShouldNotBeEqual() {
XCTAssertNotEqual("Foo", "Bar")
}
}
MyTestCase.defaultTestSuite.run()When you run the playground, the results of each test appear in the debug area.
If your tests rely on asynchronous work, enable indefinite execution to allow the playground to continue running:
PlaygroundPage.current.needsIndefiniteExecution = trueThis ensures that asynchronous expectations have time to complete before the playground exits.
Note: The Swift Testing framework is currently not supported in playgrounds.
π€ To reflect the loading progress of a WKWebView, you can observe its estimatedProgress property and present the value using a UIProgressView.
The complete implementation is available in the following example: WebViewExampleViewController.swift
In this example, the progress view is positioned along the bottom edge of the navigation bar.
π§β When a tupleβs elements are named β such as (firstName: String, lastName: String) β you can decompose it into individual constants in a single, expressive statement.
let (firstName, lastName) = accountService.fullName()
print(firstName)
print(lastName)β¨ Long conditional expressions can quickly become difficult to read and reason about β especially as a type grows more complex.
Consider the following example:
struct HugeDataObject {
let category: Int
let subCategory: Int
// Imagine many additional properties,
// making `Equatable` impractical in this case.
}
if hugeDataObject.category != previousDataObject.category ||
hugeDataObject.subCategory != previousDataObject.subCategory {
// ...
}While functionally correct, the intent of this condition isnβt immediately obvious at the call site. Breaking the logic into named Boolean values makes the code more expressive and easier to scan:
let isDifferentCategory = hugeDataObject.category != previousDataObject.category
let isDifferentSubCategory = hugeDataObject.subCategory != previousDataObject.subCategory
if isDifferentCategory || isDifferentSubCategory {
// ...
}By naming each comparison, you communicate why the condition exists β not just how itβs computed.
For early-exit scenarios, guard can further clarify intent by moving the βhappy pathβ out of the conditional:
let isDifferentCategory = hugeDataObject.category != previousDataObject.category
let isDifferentSubCategory = hugeDataObject.subCategory != previousDataObject.subCategory
let didChange = isDifferentCategory || isDifferentSubCategory
guard didChange else { return }
// Proceed knowing the data has changedThis pattern evaluates all conditions upfront. Unlike a single expression using || or &&, it does not short-circuit once the result is known.
If you have a computationally expensive check, it may be better to keep it as a single statement or check the lightweight condition first with an early return to avoid the expensive evaluation.
π Date stores time as a Double (seconds since a reference point). Because of floating-point precision, two logically equivalent dates can differ by a tiny fraction of a second, causing direct equality checks with == to fail unexpectedly.
Instead, compare their underlying time intervals with a small tolerance. The right tolerance depends on the context, but for most cases, 1 millisecond is a reasonable choice.
A shared helper keeps the tolerance consistent across your test suite:
private extension TimeInterval {
static let oneMillisecond = 0.001
}func testDatesAreEqual() {
// Given
let dateA = Date()
let dateB = Date()
// When
// ...
// Then
XCTAssertEqual(
dateA.timeIntervalSince1970,
dateB.timeIntervalSince1970,
accuracy: .oneMillisecond,
)
}@Test
func verifyDatesAreEqual() {
// Given
let dateA = Date()
let dateB = Date()
// When
// ...
// Then
let diff = abs(dateA.timeIntervalSince1970 - dateB.timeIntervalSince1970)
#expect(diff < .oneMillisecond)
}π When you create a timer using scheduledTimer(timeInterval:target:selector:userInfo:repeats:), the timer creates a strong reference to the target until the timer is invalidated. As a result, instances like the one below are never deallocated:
final class ClockViewModel {
// MARK: - Private Properties
weak var timer: Timer?
// MARK: - Instance Lifecycle
init(interval: TimeInterval = 1) {
timer = Timer.scheduledTimer(
timeInterval: interval,
target: self,
selector: #selector(timerDidFire),
userInfo: nil,
repeats: true
)
}
deinit {
print("β οΈ - This will never be called!")
timer?.invalidate()
timer = nil
}
// MARK: - Private Methods
@objc
private func timerDidFire() {
// Perform work at the specified interval.
}
}At first glance, this may look surprising. The timer property is declared as weak, and although the timer retains its target, there is no retain cycle. The issue lies elsewhere.
According to the documentation for Timer:
Timers work in conjunction with run loops. Run loops maintain strong references to their timers, so you donβt have to maintain your own strong reference to a timer after you have added it to a run loop.
And the documentation for init(timeInterval:target:selector:userInfo:repeats:) further clarifies:
target:
The timer maintains a strong reference to this object until it (the timer) is invalidated.
In other words, the run loop strongly retains the timer, and the timer strongly retains its target. As long as the timer remains valid, the view model remains alive.
Because invalidate() is called in deinit, and deinit is never reached, the timer is never invalidated.
The object is effectively kept alive by the run loop.
Starting in iOS 10, prefer the block-based API scheduledTimer(withTimeInterval:repeats:block:). By capturing self weakly, you avoid this retention issue entirely:
init(interval: TimeInterval = 1.0) {
timer = Timer.scheduledTimer(
withTimeInterval: interval,
repeats: true
) { [weak self] _ in
self?.timerDidFire()
}
}For earlier system versions, consider using DispatchSourceTimer instead. A detailed discussion of this approach can be found in Daniel Galaskoβs article: A Background Repeating Timer in Swift
Note: This behavior also applies to non-repeating timers. Even if a timer fires only once, its target will not be deallocated until the timer has fired or been invalidated.
π Basic formatting, which requires only setting dateStyle and timeStyle, can be achieved using the function localizedString(from:dateStyle:timeStyle:).
When you need additional customization, you can streamline configuration by initializing DateFormatter with a configuration closure. The following convenience initializer enables an expressive setup:
extension DateFormatter {
convenience init(configure: (DateFormatter) -> Void) {
self.init()
configure(self)
}
}E.g. creating a formatter configured with localized date and time styles:
let dateFormatter = DateFormatter {
$0.locale = .current
$0.dateStyle = .long
$0.timeStyle = .short
}Or specify a custom date format:
let dateFormatter = DateFormatter {
$0.dateFormat = "E, d. MMMM"
}This pattern generalizes well to other formatter types, including DateComponentsFormatter and DateIntervalFormatter, providing a consistent and readable configuration style across Foundation.
Starting with Swift 4, we can use key paths instead of closures:
protocol Builder {}
extension Builder {
func set<T>(_ keyPath: WritableKeyPath<Self, T>, to value: T) -> Self {
var mutableCopy = self
mutableCopy[keyPath: keyPath] = value
return mutableCopy
}
}
extension Formatter: Builder {}This approach enables a chainable configuration style:
let dateFormatter = DateFormatter()
.set(\.locale, to: .current)
.set(\.dateStyle, to: .long)
.set(\.timeStyle, to: .short)Or configure a number formatter similarly:
let numberFormatter = NumberFormatter()
.set(\.locale, to: .current)
.set(\.numberStyle, to: .currency)Based on: Vadim Bulavin β KeyPath Based Builder
π When working with CLLocationCoordinate2D, itβs important to be clear about how geographic coordinates map onto a 2D coordinate system.
On a standard, north-up map:
-
Latitude maps to the Y-axis.
Lines of latitude run eastβwest, but their values change as you move north or south. As a result, latitude corresponds to vertical movement along the Y-axis. -
Longitude maps to the X-axis.
Lines of longitude run northβsouth, but their values change as you move east or west. This aligns longitude with horizontal movement along the X-axis.
This can feel counterintuitive at first (especially since latitude lines are horizontal and longitude lines are vertical), but the key is to focus on which direction the values increase or decrease, not the orientation of the lines themselves.
The following graphics illustrate this relationship visually:
| Latitude | Longitude |
|---|---|
πͺ In a codebase thatβs constantly evolving, maintaining strong encapsulation is essential. Clearly defined APIs help limit surface area, keeping implementation details private and reducing unintended coupling.
Even notification observers and outlets can be declared private when theyβre not part of a typeβs public contract.
final class KeyboardViewModel {
// MARK: - Public Properties
/// Boolean flag, whether the keyboard is currently visible.
/// We assume that this property has to be accessed from the view controller,
/// therefore we allow public read-access.
private(set) var isKeyboardVisible = false
// MARK: - Instance Lifecycle
init(notificationCenter: NotificationCenter = .default) {
notificationCenter.addObserver(
self,
selector: #selector(didReceiveUIKeyboardWillShowNotification),
name: UIResponder.keyboardWillShowNotification,
object: nil
)
notificationCenter.addObserver(
self,
selector: #selector(didReceiveUIKeyboardDidHideNotification),
name: UIResponder.keyboardDidHideNotification,
object: nil
)
}
// MARK: - Private Methods
@objc
private func didReceiveUIKeyboardWillShowNotification(_: Notification) {
isKeyboardVisible = true
}
@objc
private func didReceiveUIKeyboardDidHideNotification(_: Notification) {
isKeyboardVisible = false
}
}β The following code removes the default padding from a UITextView:
// This brings the left edge of the text to the left edge of the container
textView.textContainer.lineFragmentPadding = 0
// This causes the top of the text to align with the top of the container
textView.textContainerInset = .zeroSource: https://stackoverflow.com/a/18987810/3532505
You can achieve the same result directly in Interface Builder using User Defined Runtime Attributes. Add the following entries to your UITextView:
| Key Path | Type | Value |
|---|---|---|
| textContainer.lineFragmentPadding | Number | 0 |
| textContainerInset | Rect | {{0, 0}, {0, 0}} |
π¨ While not iOS-specific, Name That Color is a helpful resource when you need a meaningful name for your Swift color constants. It automatically generates a descriptive name for any given hex color value.
π Use // MARK: to organize your Swift files with named sections that appear in Xcodeβs Jump Bar.
Adding a dash (// MARK: -) inserts a visual separator, making large files easier to scan and navigate.
final class StructuredViewController: UIViewController {
// MARK: - Types
typealias CompletionHandler = (Bool) -> Void
// MARK: - Public Properties
var completionHandler: CompletionHandler?
// MARK: - Private Properties
private let viewModel: StructuredViewModel
// MARK: - Instance Lifecycle
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
viewModel = StructuredViewModel()
// ...
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
}
deinit {
// ...
}
// MARK: - View Lifecycle
override func viewDidLoad() {
// ...
}
// MARK: - Private Methods
private func setupSubmitButton() {
// ...
}
}Given, When, Then improves readability and helps with understanding complex tests.
Givenestablishes the context by setting up preconditions, such as configuring mock objects or test data.Whenperforms the action under test.Thenverifies the outcome by asserting that the results match expectations.
final class MapViewModelTestCase: XCTestCase {
var locationServiceMock: LocationServiceMock!
var viewModel: MapViewModel!
var delegateMock: MapViewModelDelegateMock!
override func setUp() {
super.setUp()
// ...
}
func testLocateUser() {
// Given
let userLocation = CLLocationCoordinate2D(
latitude: 12.34,
longitude: 56.78
)
locationServiceMock.userLocation = userLocation
// When
viewModel.locateUser()
// Then
XCTAssertEqual(delegateMock.focusedUserLocation.latitude, userLocation.latitude)
XCTAssertEqual(delegateMock.focusedUserLocation.longitude, userLocation.longitude)
}
}struct MapViewModelTestCase {
let locationServiceMock: LocationServiceMock
let viewModel: MapViewModel
let delegateMock: MapViewModelDelegateMock
init() {
// ...
}
@Test
func locateUser() {
// Given
let userLocation = CLLocationCoordinate2D(
latitude: 12.34,
longitude: 56.78
)
locationServiceMock.userLocation = userLocation
// When
viewModel.locateUser()
// Then
#expect(delegateMock.focusedUserLocation.latitude == userLocation.latitude)
#expect(delegateMock.focusedUserLocation.longitude == userLocation.longitude)
}
}The only time you should be using implicitly unwrapped optionals is with @IBOutlets. In every other case, it is better to use a non-optional or regular optional property. Yes, there are cases in which you can probably "guarantee" that the property will never be
nilwhen used, but it is better to be safe and consistent. Similarly, don't use force unwraps.
Source: https://github.com/linkedin/swift-style-guide
Using the patterns shown below, we can safely unwrap optionals or use an early return to stop further code execution when an optional is nil.
if let value = value {
// Use the unwrapped value.
}guard let value = value else {
// Explain why execution cannot continue.
return
}
// Use the unwrapped value.By embracing optionals and handling them explicitly, you make failure states visible and your code more predictable.
π₯ Before performing a division, ensure the divisor is nonzero. This avoids undefined behavior, and can prevent crashes or incorrect program output.
final class ImageViewController: UIViewController {
// MARK: - Outlets
@IBOutlet private var imageView: UIImageView!
// MARK: - Private Methods
func someMethod() {
let bounds = imageView.bounds
guard bounds.height > 0 else {
// Avoid dividing by zero for calculating aspect ratio below.
return
}
let aspectRatio = bounds.width / bounds.height
}
}#22 β Animate alpha and update isHidden accordingly
π¦ This lightweight extension lets you animate a viewβs alpha value while automatically managing its isHidden state: fxm90/UIView+AnimateAlpha.swift
π When introducing custom notifications, adhere to established Cocoa naming conventions.
Notification names should be composed as follows:
[Name of associated class] + [Did | Will] + [UniquePartOfName] + Notification
This pattern improves clarity, avoids collisions, and aligns with Appleβs APIs.
Source: Coding Guidelines for Cocoa
Define custom notifications by extending Notification.Name:
extension Notification.Name {
static let AccountServiceDidLoginUser =
Notification.Name("AccountServiceDidLoginUserNotification")
}Using a static constant ensures the notification name is defined in one place and remains type-safe throughout your codebase.
Post the notification from the owning type:
final class AccountService {
func login() {
NotificationCenter.default.post(
name: .AccountServiceDidLoginUser,
object: self
)
}
}To make the notification available to Objective-C, extend NSNotificationName:
@objc
extension NSNotificationName {
static let AccountServiceDidLoginUser =
Notification.Name.AccountServiceDidLoginUser
}The notification can then be posted from Objective-C:
[[NSNotificationCenter defaultCenter] postNotificationName:NSNotification.AccountServiceDidLoginUser
object:self];
Note: The object parameter should always refer to the sender of the notification. Use the userInfo dictionary to attach additional contextual data when needed.
βοΈ A clean way to manage the status bar appearance is to introduce a dedicated property and update the system whenever it changes.
By combining a custom property with a didSet observer, you can call setNeedsStatusBarAppearanceUpdate() to prompt UIKit to re-evaluate the status bar style and apply the new appearance.
This approach keeps state changes explicit, localized, and easy to reason about.
final class SomeViewController: UIViewController {
// MARK: - Public Properties
override var preferredStatusBarStyle: UIStatusBarStyle {
customBarStyle
}
// MARK: - Private Properties
private var customBarStyle: UIStatusBarStyle = .default {
didSet {
setNeedsStatusBarAppearanceUpdate()
}
}
}π Swift provides several built-in literal expressions that capture contextual information at the call site:
| Literal | Type | Value |
|---|---|---|
| #file | String | The name of the file in which it appears. |
| #line | Int | The line number on which it appears. |
| #column | Int | The column number in which it begins. |
| #function | String | The name of the declaration in which it appears. |
Source: Swift.org β Expressions
These expressions are especially useful as default parameter values, since they are evaluated at the call site.
By combining this behavior with a simple extension on String, you can build a lightweight logging API that automatically captures file, function, and line information:
"Lorem Ipsum Dolor Sit Amet π".log(level: .info)This produces output similar to the following:
βΉοΈ 2026-02-09 21:35:00.000 [String+Log.swift:59] viewDidLoad() - Lorem Ipsum Dolor Sit Amet π
If you need more flexibility, consider using Apple's unified logging system (OSLog) for production apps.
π While not specific to iOS development, gitmoji provides a standardized set of emojis for commit messages β for example, TICKET-NUMBER - β»οΈ :: Description (credit to Martin Knabbe for that pattern).
To streamline emoji insertion for each commit type, you can use this Alfred workflow, which allows you to insert the appropriate emoji directly from the keyboard.
π Swift makes it easy to initialize a let constant using conditional logic, while keeping your code clear and safe from unintended mutation.
let startCoordinate: CLLocationCoordinate2D
if let userCoordinate = userLocationService.userCoordinate, CLLocationCoordinate2DIsValid(userCoordinate) {
startCoordinate = userCoordinate
} else {
// We don't have a valid user location, so we fall back to Hamburg.
startCoordinate = CLLocationCoordinate2D(
latitude: 53.5582447,
longitude: 9.647645
)
}This approach avoids using a var and prevents any accidental mutation of startCoordinate later on.
Starting from Swift 5.9, if and switch expressions allow an even more concise approach:
let startCoordinate = if let userCoordinate = userLocationService.userCoordinate, CLLocationCoordinate2DIsValid(userCoordinate) {
userCoordinate
} else {
// We don't have a valid user location, so we fall back to Hamburg.
CLLocationCoordinate2D(
latitude: 53.5582447,
longitude: 9.647645
)
}β‘οΈ Be aware that viewDidLoad() may be invoked if you access self.view from within a view controllerβs initializer.
At that point, the view hierarchy has not yet been loaded. However, the view property is guaranteed to return a non-nil value. To satisfy that guarantee, UIKit loads the view immediately, which in turn triggers viewDidLoad() β even though initialization has not yet completed.
As a result, code in viewDidLoad() may run earlier than expected if self.view is accessed during initialization.
final class SomeViewController: UIViewController {
// MARK: - Instance Lifecycle
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
view.isHidden = true
print("`\(#function)` did finish!")
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
view.isHidden = true
print("`\(#function)` did finish!")
}
// MARK: - View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
print("`\(#function)` did finish!")
}
}The code will output log statements in the following order:
`viewDidLoad()` did finish!
`init(nibName:bundle:)` did finish!
Source: https://stackoverflow.com/a/5808477
πΉ Starting with Xcode 12.5, the iOS Simulator includes built-in support for capturing screenshots and recording video.
- Press β + R to start or stop a screen recording.
- Press β + S to capture a screenshot.
These features are available directly in the Simulator app and remove the need to use the xcrun simctl command-line utility for basic capture workflows.
For more advanced use cases, such as configuring the simulator environment, the simctl command-line tool remains available.
For example, you can override the status bar time to produce consistent screenshots or recordings:
xcrun simctl status_bar booted override --time '9:41'
πββοΈ If you spend a lot of time in Xcode, a few well-chosen keyboard shortcuts can save you a lot of time. Here are some essential shortcuts that make everyday development more fluid.
-
β + β§ + O
Open Quickly lets you instantly search across your project for files, classes, methods, symbols, and more. -
β + β§ + J
Highlights the currently open file in the Project Navigator. This is especially useful when working in large or modular projects. -
β + L Jump directly to a specific line number. Ideal when reviewing logs, stack traces, or collaborating in code reviews.
-
β + β§ + F Search across your entire project for text matches, with powerful filtering options.
-
β + β + E
Select all occurrences within the current scope. A useful shortcut for local refactoring. -
β + β₯ + /
Insert a structured documentation comment template, making it easy to document APIs with consistency. -
β + M
Format arguments to multiple lines. -
β + β§ + Click
Create multiple cursors for simultaneous editing across different lines. (See also #42 β Xcode multi-cursor editing)
-
β + R
Build and run your app. -
β + B
Build without running β useful for quick validation. -
β + U
Run your test suite. -
β + β₯ + β + G
Repeat your most recent test or run action, whether it was a single test or an entire test class.
- β + β₯ + Enter
This toggles the SwiftUI preview.
β
Using XCTUnwrap, we can safely unwrap optionals in test cases. If the optional is nil, only the current test case fails, but the app does not crash and all other test cases continue to run.
In the example below, we initialize a view model with a list of bookings. The method findBooking(byUUID:) returns an optional, because an invalid identifier might be passed. Using XCTUnwrap, we can safely unwrap the result.
final class BookingViewModelTestCase: XCTestCase {
func test_findBookingByUUID_shouldReturnCorrectBooking() throws {
// Given
let mockedBooking = Booking(uuid: "some-uuid")
let viewModel = BookingViewModel(bookings: [mockedBooking])
// When
let receivedBooking = try XCTUnwrap(
viewModel.findBooking(byUUID: "some-uuid")
)
// Then
XCTAssertEqual(receivedBooking, mockedBooking)
}
}In Swift Testing, we can achieve similar behavior using the #require macro.
struct BookingViewModelTestCase {
@Test
func findBookingByUUID_shouldReturnCorrectBooking() throws {
// Given
let mockedBooking = Booking(uuid: "some-uuid")
let viewModel = BookingViewModel(bookings: [mockedBooking])
// When
let receivedBooking = try #require(
viewModel.findBooking(byUUID: "some-uuid")
)
// Then
#expect(receivedBooking == mockedBooking)
}
}β Using the range operator, we can create an Array extension that safely returns an element at the specified index, or nil if the index is out of bounds.
extension Array {
subscript(safe index: Index) -> Element? {
let isValidIndex = (0 ..< count).contains(index)
guard isValidIndex else {
return nil
}
return self[index]
}
}
let fruits = ["Apple", "Banana", "Cherries", "Kiwifruit", "Orange", "Pineapple"]
let banana = fruits[safe: 1]
let pineapple = fruits[safe: 5]
// Does not crash, but contains nil.
let invalid = fruits[safe: 7]π‘ Instead of writing verbose range checks like x >= 10 && x <= 100, Swift allows you to use the pattern match operator (~=) or the contains(_:) method for clearer, more readable code.
let statusCode = 200
let isSuccessStatusCode = 200 ... 299 ~= statusCode
let isRedirectStatusCode = 300 ... 399 ~= statusCode
let isClientErrorStatusCode = 400 ... 499 ~= statusCode
let isServerErrorStatusCode = 500 ... 599 ~= statusCodelet statusCode = 200
let isSuccessStatusCode = (200 ... 299).contains(statusCode)
let isRedirectStatusCode = (300 ... 399).contains(statusCode)
let isClientErrorStatusCode = (400 ... 499).contains(statusCode)
let isServerErrorStatusCode = (500 ... 599).contains(statusCode)π When working with collections in Swift, itβs common to encounter optional values.
Rather than manually unwrapping or filtering them, use compactMap to transform a collection while automatically discarding any nil values.
struct Product {
let name: String
let price: Double?
}
let products = [
Product(name: "MacBook Air", price: 999.99),
Product(name: "Mouse", price: nil),
Product(name: "Keyboard", price: 79.99),
Product(name: "Monitor", price: nil),
Product(name: "USB Cable", price: 12.99),
]
// Extract only products with valid prices.
let availablePrices = products.compactMap(\.price)
// Output: [999.99, 79.99, 12.99]
print(availablePrices)
// Output: Total value: $1092.97
let totalValue = availablePrices.reduce(0, +)
print("Total value: $\(totalValue)")π« Advantage over Array:
- Constant lookup time O(1), since a
Setstores its members based on hash value.
Disadvantage compared to Array:
- No guaranteed order.
- Cannot contain duplicate values.
- All stored elements must conform to the
Hashableprotocol.
For further examples and use cases, refer to "The power of sets in Swift" (by John Sundell).
πΆ This UIViewController extension provides a reusable API for adding and removing child view controllers, including lifecycle calls and full-size layout constraints.
extension UIViewController {
/// Inserts a child view controller and installs its view in the hierarchy.
func insert(_ child: UIViewController) {
guard child.parent == nil else { return }
addChild(child)
child.view.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(child.view)
NSLayoutConstraint.activate([
child.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
child.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
child.view.topAnchor.constraint(equalTo: view.topAnchor),
child.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
child.didMove(toParent: self)
}
/// Removes a child view controller and its view from the hierarchy.
func remove(_ child: UIViewController) {
guard child.parent === self else { return }
child.willMove(toParent: nil)
child.view.removeFromSuperview()
child.removeFromParent()
}
}The constraint setup pins the childβs view to all four edges of its parent and can be further extracted into a UIView extension if this pattern is used more broadly.
See #45 β Pin a view to its superview for more details on what this looks like.
βοΈ When updating the image of a UIImageView, a subtle cross-dissolve transition creates a smooth, polished effect. By using .transitionCrossDissolve, you can seamlessly animate between images with just a few lines of code:
extension UIImageView {
func updateImageWithTransition(_ image: UIImage?, duration: TimeInterval) {
UIView.transition(
with: self,
duration: duration,
options: .transitionCrossDissolve
) {
self.image = image
}
}
}π¨βπ¨ CALayer has a default implicit animation duration of 0.25 seconds. The following extension allows you to update layer properties instantly, without triggering these implicit animations:
extension CALayer {
final class func performWithoutAnimation(_ actionsWithoutAnimation: () -> Void) {
CATransaction.begin()
CATransaction.setAnimationDuration(0.0)
actionsWithoutAnimation()
CATransaction.commit()
}
}override class var layerClass: AnyClass {
return CAGradientLayer.self
}By overriding 'layerClass' you can tell UIKit what CALayer class to use for a UIView's backing layer. That way you can reduce the amount of layers, and don't have to do any manual layout. John Sundell
This is e.g. useful for adding a linear gradient behind an image. This way, we could change the gradient color based on the time of day, without bundling multiple images in the app.
You can see the full code for the example in my gist for the Vertical Gradient Image View.
π¬ When working with NotificationCenter, you often want to make sure the right notifications are posted. Hereβs a quick way to test them.
- XCTest β Assert notification (not) triggered
- XCTest β Use custom notification center in test case and assert notification (not) triggered
π Using didSet on @IBOutlets is a neat trick to configure your view components (declared in a storyboard or XIB) in a concise and readable manner:
final class ExampleViewController: UIViewController {
// MARK: - Outlets
@IBOutlet private var button: UIButton! {
didSet {
button.setTitle(viewModel.normalTitle, for: .normal)
button.setTitle(viewModel.disabledTitle, for: .disabled)
}
}
}β¨ A lightweight Equatable extension that improves readability when checking whether a value matches one of several candidates. This pattern was popularized by John Sundell.
extension Equatable {
func isAny(of candidates: Self...) -> Bool {
candidates.contains(self)
}
}enum Device {
case iPhone7
case iPhone8
case iPhoneX
case iPhone11
}
let device: Device = .iPhoneX
// Before
let hasSafeAreas = [.iPhoneX, .iPhone11].contains(device)
// After
let hasSafeAreas = device.isAny(of: .iPhoneX, .iPhone11)πΈ To prevent retain cycles, closures commonly capture self weakly. However, the implementation differs between traditional closures and modern Swift Concurrency.
For escaping completion handlers, capture self weakly to avoid retain cycles. Then, promote it to a strong reference for the duration of the closureβs execution.
Since Swift 5.7, this can be expressed using the shorthand optional binding syntax.
documentService.fetch { [weak self] document in
// Creates a strong reference for the duration of this closure.
guard let self else { return }
updateUI(document)
}Inside a Task, capturing self is implicit. You donβt need to explicitly write self. to reference instance members β but self is still strongly retained for the lifetime of the task.
That distinction becomes important for long-running or suspended tasks.
Task { [weak self] in
// `self` is retained strongly until the task completes.
guard let self else { return }
let document = await documentService.fetch()
updateUI(document)
}Although self is captured weakly, unwrapping it at the beginning creates a strong reference that remains alive until the entire task finishes.
If the task is long-running, self (e.g. a view controller) will be kept in memory even after it should have been deallocated.
To allow self to be released while the task is suspended, defer unwrapping until the moment you need it. Or use a conditional access.
Task { [weak self, documentService] in
let document = await documentService.fetch()
// `self` may deallocate while the fetch is in progress.
self?.updateUI(document)
}When working with long-running loops or AsyncSequence values, re-evaluate the existence of self on each iteration. This ensures self can be released between iterations.
Task { [weak self] in
for await value in stream {
guard let self else {
// Exit the loop when `self` has been deallocated.
break
}
process(value)
}
}