Skip to content

πŸ“– My personal collections of things, tips & tricks I've learned during iOS development so far and do not want to forget.

Notifications You must be signed in to change notification settings

fxm90/dev-notes

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

92 Commits
Β 
Β 
Β 
Β 

Repository files navigation

iOS Dev-Notes πŸ—’ πŸš€

My personal collections of things, tips & tricks I've learned during iOS development so far and do not want to forget.

I'm happy for any feedback, so feel free to write me on twitter.

Table of contents

#65 – Get the size of a child view in SwiftUI
#64 – Check for enabled state in ButtonStyle
#63 – Animate text-color with SwiftUI
#62 – Custom localized date format
#61 – Animate isHidden on a UIStackView
#60 – Making types expressible by literals
#59 – SwiftUI ToggleStyle Protocol
#58 – Getting the size of a view as defined by Auto Layout
#57 – Decode Array while filtering invalid entries
#56 – Codable cheat sheet
#55 – SwiftUI make a child view respect the safe area
#54 – Convert string with basic HTML tags to SwiftUI's Text
#53 – Concatenate two Texts in SwiftUI
#52 – Animated reload of a UITableView
#51 – Redux & SwiftUI Example
#50 – Basic Combine Examples
#49 – Convert units using Measurement<UnitType>
#48 – FloatingPoint Protocol
#47 – Wait for multiple async tasks to complete
#46 – Snapshot testing
#45 – Span subview to superview
#44 – Animate a view using a custom timing function
#43 – How to test a delegate protocol
#42 – Xcode multi-cursor editing
#41 – Create a dynamic color for light- and dark mode
#40 – UITableViewCell extension that declares a static identifier
#39 – Prefer "for .. in .. where"-loop over filter() and forach {}
#38 – Lightweight observable implementation
#37 – Run test cases in a playground
#36 – Show progress of a WKWebView in a UIProgressBar
#35 – Destructure tuples
#34 – Avoid huge if statements
#33 – Compare dates in test cases
#32 – Be aware of the strong reference to the target of a timer
#31 – Initialize DateFormatter with formatting options
#30 – Map 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 – Always check for possible dividing through zero
#22 – Animate alpha and update isHidden accordingly
#21 – Create custom notification
#20 – Override UIStatusBarStyle the elegant way
#19 – Log extension on String using swift literal expressions
#18 – Use gitmoji for commit messages
#17 – Initialize a constant based on a condition
#16 – Why viewDidLoad might be called before init has finished
#15 – Capture iOS Simulator video
#14 – Xcode open file in focused editor
#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 – Remove all sub-views from UIView
#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 setup components
#02 – Most readable way to check whether an array contains a value (isAny(of:))
#01 – Override self in escaping closure, to get a strong reference to self\

#65 – Get the size of a child view in SwiftUI

πŸ“ Using a PreferenceKey it's possible to get the size of a child view in SwiftUI.

struct SizePreferenceKey: PreferenceKey {
    static var defaultValue: CGSize = .zero

    static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
        value = nextValue()
    }
}

struct SizeModifier: ViewModifier {
    func body(content: Content) -> some View {
        content.background(
            GeometryReader { geometry in
                Color.clear.preference(key: SizePreferenceKey.self, value: geometry.size)
            }
        )
    }
}

In the following example the property textSize will contain the size of the Text view.

struct ContentView: View {

    @State
    private var textSize: CGSize = .zero

    var body: some View {
        Text("Hello World")
            .modifier(SizeModifier())
            .onPreferenceChange(SizePreferenceKey.self) { textSize in
                self.textSize = textSize
            }
    }
}

Further information on PreferenceKey can be found here: The magic of view preferences in SwiftUI

#64 – Check for enabled state in ButtonStyle

🎨 The ButtonStyle protocol allows us to customise buttons through our application without copy-pasting the styling code.

Unfortunately it's not possible to get the environment property isEnabled inside ButtonStyle. But it's possible to get it inside a View as a workaround.

struct PrimaryButtonStyle: ButtonStyle {
    func makeBody(configuration: Self.Configuration) -> some View {
        PrimaryButtonStyleView(configuration: configuration)
    }
}

private struct PrimaryButtonStyleView: View {

    // MARK: - Public properties

    let configuration: ButtonStyle.Configuration

    // MARK: - Private properties

    @SwiftUI .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)
    }
}

#63 – Animate text-color with SwiftUI

🎨 Unfortunately in SwiftUI the property foregroundColor can't be animated. But it's possible to animate colorMultiply instead.

Therefore we set foregroundColor to white and use colorMultiply to set the actual color we want. This color is then animatable.

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
                }
            }
    }
}

#62 – Custom localized date format

πŸ“ Using the method dateFormat(fromTemplate:options:locale:) we can further customise a date-format (e.g. MMMd) to a specific locale.

extension Date {

    /// Returns a localized string from the current instance for the given `template` and `locale`.
    ///
    /// - Parameters:
    ///   - template: A string containing date format patterns (such as β€œMM” or β€œh”).
    ///   - locale: The locale for which the template is required.
    ///
    /// - SeeAlso: [dateFormat(fromTemplate:options:locale:)](https://developer.apple.com/documentation/foundation/dateformatter/1408112-dateformat)
    func localizedString(from template: String, for locale: Locale) -> String {
        let dateFormatter = DateFormatter()
        dateFormatter.locale = locale

        let localizedDateFormat = DateFormatter.dateFormat(fromTemplate: template, options: 0, locale: locale)
        dateFormatter.dateFormat = localizedDateFormat

        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 on a UIStackView

πŸ§™β€β™€οΈ It's easily possible to animate the visibility of an arranged subview inside a UIStackView. In this example the corresponding view will slide out when setting the property isHidden to true.

UIView.animateWithDuration(0.3) {
    viewInsideStackView.isHidden = true
    stackView.layoutIfNeeded()
}

#60 – Making types expressible by literals

πŸ–Œ Swift provides protocols which enable you to initialize a type using literals, e.g.:

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                // ExpressibleByBooleanLiteral

A complete list of these protocols can be found in the documentation: Initialization with Literals

Here we focus on ExpressibleByStringLiteral and ExpressibleByStringInterpolation for initialising a custom type.

struct StorageKey {
    let path: String
}

extension StorageKey: ExpressibleByStringLiteral, ExpressibleByStringInterpolation {
    init(stringLiteral path: String) {
        self.init(path: path)
    }
}

Build an instance of StorageKey using ExpressibleByStringLiteral:

let storageKey: StorageKey = "/cache/"

Build an instance of StorageKey using ExpressibleByStringInterpolation:

let username = "f.mau" 
let storageKey: StorageKey = "/users/\(username)/cache"

This pattern is especially handy when creating an URL instance from a string:

extension URL: ExpressibleByStringLiteral {
    /// Initializes an URL instance from a string literal, e.g.:
    /// ```
    /// 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
    }
}

For safety reason we only conform to ExpressibleByStringLiteral and thereof use StaticString, as we don't want any dynamic string interpolation to crash our app.

Based on

#59 – SwiftUI ToggleStyle Protocol

🎨 SwiftUI provides a ToggleStyle protocol to completely customize the appearance of a Toggle.

Important: When customizing a Toggle using this protocol, it’s down to you to visualize the state! Therefore the method makeBody(configuration:) is passed with a parameter configuration that contains the current state and allows toggling it by calling configuration.isOn.toggle().

To demonstrate custom Toggle styles I've added two gists with screenshots in the comments:

#58 – Getting the size of a view as defined by Auto Layout

↔️ Using the systemLayoutSizeFitting(targetSize:) method on UIView, we can obtain the size of a view as defined by Auto Layout.

For example we could ask for the height of a view, using a given width:

let size = view.systemLayoutSizeFitting(
    CGSize(width: view.bounds.width, height: UIView.layoutFittingCompressedSize.height),
    withHorizontalFittingPriority: .required,
    verticalFittingPriority: .fittingSizeLevel
)

#57 – Decode Array while filtering invalid entries

πŸͺ„ Usually an API should have a clear interface and the App should know which data to receive. But there are cases when you can't be 100% sure about a response.

Imagine fetching a list of flights for an airport. You don't want the entire decoding to fail in case one flight has a malformed departure date.

As a workaround we define a helper type, that wraps the actual data-model, in our case a Flight data-model.

/// Helper to filter-out invalid array entries when parsing a JSON response.
///
/// This way we prevent the encoding-failure of an entire array, if the decoding of a single element fails.
///
/// Source: https://stackoverflow.com/a/46369152/3532505
private struct FailableDecodable<Base: Decodable>: Decodable {

  let base: Base?

  init(from decoder: Decoder) throws {
    let container = try decoder.singleValueContainer()
    base = try? container.decode(Base.self)
  }
}

In our service we decode the array of Flights to FailableDecodable<Flight>, to filter out invalid array elements, but don't let the entire decoding fail (only the property base will be nil on failure).

Afterwards we use compactMap { $0.base } to filter out array-values where the property base is nil.

/// Data-model
struct Flight {
    let number: String
    let departure: Date
}

/// Service-method
func fetchDepartures(for url: URL) -> AnyPublisher<[Flight], Error> {
  URLSession.shared
      .dataTaskPublisher(for: url)
      .map { $0.data }
      // We explicitly use `FailableDecodable<T>` here, to filter out invalid array elements afterwards.
      .decode(type: [FailableDecodable<Flight>].self, decoder: JsonDecoder())
      .map {
        // Map the array of type `FailableDecodable<Flight>` to `Flight`, while filtering invalid (`nil`) elements.
        $0.compactMap { $0.base }
      }
      .eraseToAnyPublisher()
  }
}

#56 – Codable cheat sheet

πŸ“ Paul Hudson has written a great cheat sheet about converting between JSON and Swift data types.

#55 – SwiftUI make a child view respect the safe area

πŸ“² Neat trick for having the content of a View respect the safe-area, while having the background covering the entire device.

struct FullScreenBackgroundView: View {
    var body: some View {
        Text("Hello, World!")
            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
            .background(Color.red.edgesIgnoringSafeArea(.all))
    }
}

struct FullScreenBackgroundViewPreviews: PreviewProvider {
    static var previews: some View {
        FullScreenBackgroundView()
            .previewDevice(PreviewDevice(rawValue: "iPhone 11 Pro"))
    }
}

#54 – Convert string with basic HTML tags to SwiftUI's Text

πŸ–Œ Using the underneath shown + operator we can build an extension on SwiftUI's Text, that allows us to parse basic HTML tags (like <strong>, β€Œ<em> etc).

Please have a look at the comments for some usage examples.

Update 28.05.2021

iOS 15.0 brings AttributedString to SwiftUI including Markdown support.

Converting basic HTML formatting tags to Markdown is not too difficult, so I added a second gist showing exactly that and further adds support for hyperlinks: SwiftUI+HTML.swift

#53 – Concatenate two Texts in SwiftUI

πŸ§™β€β™€οΈ The + operator can concatenate two Text in SwiftUI.

Text("Note:")
    .bold() + 
Text(" Lorem Ipsum Dolor Sit Amet.")

This will render: "Note: Lorem Ipsum Dolor Sit Amet."

#52 – Animated reload of a UITableView

πŸš€ Calling tableView.reloadData() inside the animation block of UIView.transition(with:duration:options:animations:completion:) will result in an animated reload of the table view cells.

UIView.transition(with: tableView,
                  duration: 0.3,
                  options: .transitionCrossDissolve,
                  animations: { self.tableView.reloadData() })

You can pass any UIView.AnimationOptions mentioned here.

Source: https://stackoverflow.com/a/13261683

#51 – Redux & SwiftUI Example

πŸ”„ The following gist shows you how to integrate basic Redux functionality in SwiftUI (without using any additional frameworks): Redux.swift

Feel free to copy the code into a Xcode Playground and give it a try πŸ˜ƒ

#50 – Basic Combine Examples

πŸ§ͺ Here are two Gists regarding Apple's new Combine framework:

Feel free to copy the code a playground and get your hands dirty with Combine πŸ˜ƒ

#49 – Convert units using Measurement<UnitType>

πŸ” Starting from iOS 10 we can use Measurement to convert units like e.g. angles, areas, durations, speeds, temperature, volume and many many more.

Using e.g. Measurement<UnitAngle> we can refactor the computed property shown in note #48 to a method, that allows us to convert between any UnitAngle:

extension BinaryFloatingPoint {
    func converted(from fromUnit: UnitAngle, to toUnit: UnitAngle) -> Self {
        let selfAsDouble = Double(self)
        let convertedValueAsDouble = Measurement(value: selfAsDouble, unit: fromUnit)
            .converted(to: toUnit)
            .value

        return type(of: self).init(convertedValueAsDouble)
    }
}

Furthermore this approach leads to a very clean call side:

let cameraBearing: CLLocationDegrees = 180
cameraBearing.converted(from: .degrees, to: .radians)

#48 – FloatingPoint Protocol

🎲 By extending the protocol FloatingPoint we can define a method / computed property on all floating point datatypes, e.g. Double, Float or CGFloat:

extension FloatingPoint {
    var degToRad: Self {
        self * .pi / 180
    }
}

let double: Double = 90
let float: Float = 180
let cgFloat: CGFloat = 270

print("Double as radians", double.degToRad)
print("Float as radians", float.degToRad)
print("CGFloat as radians", cgFloat.degToRad)

#47 – Wait for multiple async tasks to complete

⏰ Using a DispatchGroup we can wait for multiple async tasks to finish.

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()
}

// We need to define the completion handler of our `DispatchGroup` with an unbalanced call to `enter()` and `leave()`,
// as otherwise it will be called immediately!
dispatchGroup.notify(queue: .main) {
    guard let profile = profile, let friends = friends else { return }

    print("We've downloaded the user profile together with all friends!")
}

Update for Projects targeting iOS >= 13.0 Starting from iOS 13 we can use CombineLatest to wait for multiple publishers to at least fire publish one message.

let fetchProfileFuture = profileService.fetchProfile()
let fetchFriendsFuture = profileService.fetchFriends()

cancellable = Publishers.CombineLatest(fetchProfileFuture, fetchFriendsFuture)
    .sink { result in
        let (profile, friends) = result
        print("We've downloaded the user profile together with all friends!")
    }

Starting from iOS 15 iOS 13 we can also use async let to wait for multiple async values.

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!")
}

46 – Snapshot testing

πŸ“Έ Snapshot tests are a very useful tool whenever you want to make sure your UI does not change unexpectedly.

Using the library SnapshotTesting from Point-Free you can easily start testing snapshots of your UIView, UIViewController, UIImage or even URLRequest.

45 – Span subview to superview

βš“οΈ A small extension to span a subview to the anchors of its superview.

extension UIView {
    /// Adds layout constraints to top, bottom, leading and trailing anchors equal to superview.
    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)
        ])
    }
}

44 – Animate a view using a custom timing function

πŸš€ Starting from iOS 10 we can use a UIViewPropertyAnimator to animate changes on views.

Using the initializer init(duration:timingParameters:) we can pass a UITimingCurveProvider, which allows us to provide a custom timing function. You can find lots of these functions on Easings.net.

Using e.g. "easeInBack" your animation code could look like this:

extension UICubicTimingParameters {
    static let easeInBack = UICubicTimingParameters(controlPoint1: CGPoint(x: 0.6, y: -0.28),
                                                    controlPoint2: CGPoint(x: 0.735, y: 0.045))
}

class CustomTimingAnimationViewController: UIViewController {

    // ...

    func userDidTapButton() {
        let animator = UIViewPropertyAnimator(duration: 1.0,
                                              timingParameters: UICubicTimingParameters.easeInBack)

        animator.addAnimations {
            // Add your animation code here. E.g.:
            // `self.someConstraint?.isActive = false`
            // `self.someOtherConstraint?.isActive = true`
        }

        animator.startAnimation()
    }
}

43 – How to test a delegate protocol

πŸ§ͺ Delegation is a common pattern whenever one object needs to communicate to another object (1:1 communication).

The following gist shows you how to test a delegate-protocol from a view-model, by creating a mock and validate the invoked method(s) using an enum: Example on how to elegantly test a delegate protocol

42 – Xcode multi-cursor editing

πŸƒβ€ Since Xcode 10 the Source Editor supports multi-cursor editing, allowing you to quickly edit multiple ranges of code at once. You can place additional cursors with the mouse via:

shift + control + click
shift + control + ↑
shift + control + ↓

41 – Create a dynamic color for light- and dark mode

🎨 Using the gist UIColor+MakeDynamicColor.swift we can create a custom UIColor that generates its color data dynamically based on the current userInterfaceStyle.

Furthermore this method falls back to the lightVariant color for iOS versions prior to iOS 13.

#40 – UITableViewCell extension that declares a static identifier

πŸ§™β€β™€οΈ Using the extension below we can automatically register and dequeue table view cells. It prevents typos and declaring a static string on each cell.

extension UITableViewCell {
    static var identifier: String {
        return String(describing: self)
    }
}

Register a cell:

tableView.register(CustomTableViewCell.self, forCellReuseIdentifier: CustomTableViewCell.identifier)

Dequeue a cell:

let cell = tableView.dequeueReusableCell(withIdentifier: CustomTableViewCell.identifier)

#39 – Prefer "for .. in .. where"-loop over filter() and forach {}

🎒 For iterating over a large array using a "for .. in .. where" loop is two times faster than combing filter() and forach {}, as it saves one iteration.

So instead of writing:

scooterList
    .filter({ !$0.isBatteryEmpty })
    .forEach({ scooter in
        // Do something with each scooter, that still has some battery left.
    })

it is more efficient to write:

for scooter in scooterList where !scooter.isBatteryEmpty {
    // Do something with each scooter, that still has some battery left.
}

#38 – Lightweight observable implementation

πŸ•΅οΈβ€β™‚οΈ If you need a simple and lightweight observable implementation for e.g. UI bindings check out the following gist: Observable.swift

For re-usability reasons I've moved the code into a framework and released it as a CocoaPod. Please check out https://github.com/fxm90/LightweightObservable πŸ™‚

#37 – Run test cases in playground

πŸ§ͺ Playgrounds are an easy way to try out simple ideas. It is a good approach to directly think about the corresponding test-cases for the idea or even start the implementation test driven.

By calling MyTestCase.defaultTestSuite.run() inside the playground we can run a test-case and later copy it into our "real" project.

import Foundation
import XCTest

class MyTestCase: XCTestCase {

    override func setUp() {
        super.setUp()
    }

    override func tearDown() {
        super.tearDown()
    }

    func testFooBarShouldNotBeEqual() {
        XCTAssertNotEqual("Foo", "Bar")
    }
}

MyTestCase.defaultTestSuite.run()

You can see the result of each test inside the debug area of the playground.

For running asynchronous test cases you have to add the following line:

PlaygroundPage.current.needsIndefiniteExecution = true

#36 – Show progress of WKWebView in UIProgressBar

πŸ€– For showing the loading-progress of a WKWebView on a UIProgressBar, please have a look at the following gist: WebViewExampleViewController.swift

In the example code, the UIProgressBar is attached to the bottom anchor of an UINavigationBar (see method setupProgressView() for further layout details).

#35 – Destructure tuples

πŸ§™β€ Image having a tuple with the following properties: (firstName: String, lastName: String). We can destructure the tuple into two properties in just one line:

let (firstName, lastName) = accountService.fullName()

print(firstName)
print(lastName)

#34 – Avoid huge if statements

✨ Instead of writing long "if statements" like this:

struct HugeDataObject {
    let category: Int
    let subCategory: Int

    // Imagine lots of other properties, so we can't simply conform to `Equatable` ...
}

if hugeDataObject.category != previousDataObject.category || hugeDataObject.subCategory != previousDataObject.subCategory {
    // ...
}

We can split the long statement into several properties beforehand, to increase readability:

let isDifferentCategory = hugeDataObject.category != previousDataObject.category
let isDifferentSubCategory = hugeDataObject.subCategory != previousDataObject.subCategory

if isDifferentCategory || isDifferentSubCategory {
    // ...
}

Or use guard to do an early return:

let isDifferentCategory = hugeDataObject.category != previousDataObject.category
let isDifferentSubCategory = hugeDataObject.subCategory != previousDataObject.subCategory

let didChange = isDifferentCategory || isDifferentSubCategory
guard didChange else { return }

Notice: By using that pattern we do not skip further checks on failure (e.g. if we use OR in the statement and one condition returns true / we use AND in the statement and one condition returns false). So if you're having a load intensive method, it might be better to keep it as a single statement. Or, first check the "lighter" condition and then use an early return to prevent the load intensive method from being executed.

#33 – Compare dates in test cases

πŸ“† Small example on how to compare dates in tests.

func testDatesAreEqual() {
    // Given
    let dateA = Date()
    let dateB = Date()

    // When
    // ...

    // Then
    XCTAssertEqual(dateA.timeIntervalSince1970,
                   dateB.timeIntervalSince1970,
                   accuracy: 0.01)
}

#32 – Be aware of the strong reference to the target of a timer

πŸ” Creating a timer with the method scheduledTimer(timeInterval:target:selector:userInfo:repeats:) always creates a strong reference to the target until the timer is invalidated. Therefore, an instance of the following class will never be deallocated:

class ClockViewModel {
    // MARK: - Private properties

    weak var timer: Timer?

    // MARK: - Initializer

    init(interval: TimeInterval = 1.0) {
        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() {
        // Do something every x seconds here.
    }
}

But didn't we declare the variable timer as weak? So even though we have a strong reference from the timer to the view-model (via the target and selector), we should not have a retain cycle? Well, that's true. The solution is mentioned in the documentation for the class "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 the method "timerWithTimeInterval"

target: The timer maintains a strong reference to this object until it (the timer) is invalidated.

Therefore the run loop contains a strong reference to the view-model, as long as the timer is not invalidated. As we call invalidate inside the deinit of the view-model method, the timer gets never invalidated.

Workaround:

From iOS 10.0 we can use the method scheduledTimer(withTimeInterval:repeats:block:) instead and pass a weak reference to self in the closure, in order to prevent a retain cycle.

    init(interval: TimeInterval = 1.0) {
        timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true, block: { [weak self] _ in
            self?.timerDidFire()
        })
    }

For iOS version below 10.0, we can use DispatchSourceTimer instead. There is a great article from Daniel Galasko on how to do that: A Background Repeating Timer in Swift

Notice: Even for non repeating timers, you should be aware of that strong reference, cause the corresponding object won't get deallocated until the timer has fired.

#31 – Initialize DateFormatter with formatting options

πŸš€ Basic formatting, which requires only setting dateStyle and timeStyle, can be achieved with the class function localizedString(from:dateStyle:timeStyle:).

In case you need further formatting options, the following extension allows you to directly initialize a DateFormatter with all available options:

extension DateFormatter {
    convenience init(configure: (DateFormatter) -> Void) {
        self.init()

        configure(self)
    }
}

Use it like this:

let dateFormatter = DateFormatter {
    $0.locale = .current
    $0.dateStyle = .long
    $0.timeStyle = .short
}
let dateFormatter = DateFormatter {
    $0.dateFormat = "E, d. MMMM"
}

Feel free to bring this extension to other formatters, like e.g. DateComponentsFormatter or DateIntervalFormatter, as well.

Update: 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 {}

Use it like this:

let dateFormatter = DateFormatter()
    .set(\.locale, to: .current)
    .set(\.dateStyle, to: .long)
    .set(\.timeStyle, to: .short)
let numberFormatter = NumberFormatter()
    .set(\.locale, to: .current)
    .set(\.numberStyle, to: .currency)

Based on: Vadim Bulavin – KeyPath Based Builder

#30 – Map latitude and longitude to X and Y on a coordinate system

🌍 Not really an iOS specific topic but something to keep in mind πŸ˜ƒ

On a standard north facing map, latitude is represented by horizontal lines, which go up and down (North and South) the Y axis. It's easy to think that since they are horizontal lines, they would be on the x axis, but they are not. So similarly, the X axis is Longitude, as the values shift left to right (East and West) along the X axis. Confusing for the same reason since on a north facing map, these lines are vertical.

https://gis.stackexchange.com/a/68856

The following graphics illustrate the quote above:

Latitude Longitude
Latitude Longitude

Further iOS related information:

#29 - Encapsulation

πŸšͺ When working on a continuously evolving code base, one of the biggest challenges is to keep things nicely encapsulated. Having clear defined APIs avoids sharing implementation details with other types and therefore prevent unwanted side-effects.

Even notification receivers or outlets can be marked as private.

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: - Initializer

    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
    }
}

#28 – Remove UITextView default padding

↔ With the following code the default padding from an UITextView can be removed:

// 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 = .zero

Source: https://stackoverflow.com/a/18987810/3532505

The above code can also be applied inside the interface builder within the "User Defined Runtime Attributes" section. Just add the following lines there:

Key Path Type Value
textContainer.lineFragmentPadding Number 0
textContainerInset Rect {{0, 0}, {0, 0}}

#27 – Name that color

🎨 Not an iOS specific topic, but if your designer comes up with the 9th gray tone and you somehow need to find a proper name inside your code, check out this site: Name That Color. It automatically generates a name for the given color πŸ§™β€

#26 – Structure classes using // MARK: -

πŸ”– Using // MARK: we can add some additional information that is shown in the quick jump bar. Adding a dash at the end (// MARK: -) causes a separation line to show up. Using this technique we can structure classes and make them easier to read.

class StructuredViewController: UIViewController {

    // MARK: - Types

    typealias CompletionHandler = (Bool) -> Void

    // MARK: - Outlets

    @IBOutlet private var submitButton: UIButton!

    // MARK: - Public properties

    var completionHandler: CompletionHandler?

    // MARK: - Private properties

    private let viewModel: StructuredViewModel

    // MARK: - Initializer

    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        viewModel = StructuredViewModel()

        // ...

        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
    }

    deinit {
        // ...
    }

    // MARK: - Public methods

    override func viewDidLoad() {
        // ...
    }

    // MARK: - Private methods

    private func setupSubmitButton() {
        // ...
    }
}

#25 – Structure test cases

⚠️ Splitting test cases into Given, When, Then increases the readability and helps understanding complex tests.

  • In the Given phase we setup all preconditions for the test, e.g. configuring mock objects.
  • In the When phase we call the function we want to test.
  • In the Then phase we verify the actual results against our expected results using XCTAssert methods.

Example:

class MapViewModelTestCase: XCTestCase {
    var locationServiceMock: LocationServiceMock!

    var viewModel: MapViewModel!
    var delegateMock: MapViewModelDelegateMock!

    override func setUp() {
        super.setUp()

        // ...
    }

    override func tearDown() {
        // ...

        super.tearDown()
    }

    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)
    }
}

#24 – Avoid forced unwrapping

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 nil when 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 underneath, we can easily unwrap optionals or use early return to stop further code executing, if an optional is nil.

if let value = value {
    // Do something with value here..
}
guard let value = value else {
    // Write a comment, why to exit here.
    return
}

// Do something with value here..

#23 – Always check for possible dividing through zero

πŸ’₯ We should always make sure that a certain value is NOT zero before dividing through it.

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 diving through zero for calculating aspect ratio below.
            return
        }

        let aspectRatio = bounds.width / bounds.height
    }
}

#22 – Animate alpha and update isHidden accordingly

πŸ¦‹ Using the following gist we can animate the alpha property and update the isHidden flag accordingly: fxm90/UIView+AnimateAlpha.swift

#21 – Create custom notification

πŸ“š For creating custom notifications we first should have a look on how to name them properly:

[Name of associated class] + [Did | Will] + [UniquePartOfName] + Notification

Source: Coding Guidelines for Cocoa

We create the new notification by extending the corresponding class:

extension Notification.Name {
    static let AccountServiceDidLoginUser = Notification.Name("AccountServiceDidLoginUserNotification")
}

And afterwards post it like this:

class AccountService {
    func login() {
        NotificationCenter.default.post(name: .AccountServiceDidLoginUser,
                                        object: self)
    }
}

For Objective-C support we further need to extend NSNotification:

@objc extension NSNotification {
    static let AccountServiceDidLoginUser = Notification.Name.AccountServiceDidLoginUser
}

Then, we can post it like this:

[NSNotificationCenter.defaultCenter post:NSNotification.AccountServiceDidLoginUser
                                  object:self];

By extending Notification.Name we make sure our notification names are unique.

Notice: The object parameter should always contain the object, that is triggering the notification. If you need to pass custom data, use the userInfo parameter.

#20 – Override UIStatusBarStyle the elegant way

✌️ Using a custom property, combined with the observer didSet we can call setNeedsStatusBarAppearanceUpdate() to apply a new status-bar style:

class SomeViewController: UIViewController {

    // MARK: - Public properties

    override var preferredStatusBarStyle: UIStatusBarStyle {
        return customBarStyle
    }

    // MARK: - Private properties

    private var customBarStyle: UIStatusBarStyle = .default {
        didSet {
            setNeedsStatusBarAppearanceUpdate()
        }
    }
}

19 – Log extension on String using swift literal expressions

πŸ‘Œ Swift contains some special literals:

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

Especially with default parameters those expressions are really useful, as in that case the expression is evaluated at the call site. We could use a simple extension on String to create a basic logger:

"Lorem Ipsum Dolor Sit Amet πŸ‘‹".log(level: .info)

That would create the following output:

ℹ️ – 2018/09/16 19:46:45.189 - ViewController.swift - viewDidLoad():15
> Lorem Ipsum Dolor Sit Amet πŸ‘‹

18 – Use gitmoji

πŸ˜ƒ Not an iOS specific topic, but I'd like to use gitmoji for my commit messages, e.g. TICKET-NUMBER - ♻️ :: Description (Credits go to Martin Knabbe for that pattern). To easily create the corresponding emojis for the type of commit, you can use this alfred workflow.

#17 – Initialize a constant based on a condition

πŸ‘ A very readable way of initializing a constant after the declaration.

let startCoordinate: CLLocationCoordinate2D
if let userCoordinate = userLocationService.userCoordinate, CLLocationCoordinate2DIsValid(userCoordinate) {
    startCoordinate = userCoordinate
} else {
    // We don't have a valid user location, so we fallback to Hamburg.
    startCoordinate = CLLocationCoordinate2D(latitude: 53.5582447,
                                             longitude: 9.647645)
}

This way we can avoid using a variable and therefore prevent any mutation of startCoordinate in further code.

#16 – Why viewDidLoad might be called before init has finished

⚑️ Be aware that the method viewDidLoad is being called immediately on accessing self.view in the initializer.

This happens because the view is not loaded yet, but the property self.view shouldn't return nil.

Therefore the view controller will load the view immediately and call the corresponding method viewDidLoad afterwards.

Example:

class ViewDidLoadBeforeInitViewController: UIViewController {
    // MARK: - Initializer

    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: - Public methods

    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

More on view life cycle: Work with View Controllers

#15 – Capture iOS Simulator video

πŸ“Ή A small tutorial on how create a video of what's happening in the simulator.

  1. Run your App in the simulator
  2. Open terminal
  3. Run one of the following commands:
  • To take a screenshot: xcrun simctl io booted screenshot
  • To take a video: xcrun simctl io booted recordVideo <filename>.<file extension>
  1. Press ctrl + c to stop recording the video.

For example:

xcrun simctl io booted recordVideo ~/appVideo.mp4

Source: https://stackoverflow.com/a/41141801

In case you want to further customise the simulator, e.g. by setting a custom battery level, check out this amazing tool by Paul Hudson: ControlRoom

#14 – Xcode open file in focused editor

πŸƒβ€β™‚οΈ Shortcuts are a great way to increase productivity. I often use CMD[⌘] + Shift[⇧] + O to quickly open a file or CMD[⌘] + Shift[⇧] + J to focus the current file in the project navigator etc.

But when you β€˜Quick Open’ a file via cmd-shift-O, it opens in the β€˜Primary Editor’ on the left β€” even if the right editor pane is currently focused.

By going to Settings Β» Navigation Β» Navigation and there checking Uses Focused Editor, we can tell Xcode to always open files in the currently focused pane.

Source: Jesse Squires – Improving the assistant editor

#13 – Handle optionals in test cases

βœ… Using XCTUnwrap we can safely unwrap optionals in test-cases. If the optional is nil, only the current test-case will fail, but the app won't crash and all other test-cases will continue to be executed.

In the example below, we initialize a view model with a list of bookings. Using the method findBooking(byIdentifier:) we search for a given booking. But as we might pass an invalid identifier, the response of the method is an optional booking object. Using XCTUnwrap we can easily unwrap the response.

class BookingViewModelTestCase: XCTestCase {
    func testFindBookingByIdentifierShouldReturnMockedBooking() throws {
        // Given
        let mockedBooking = Booking(identifier: 1)
        let viewModel = BookingViewModel(bookings: [mockedBooking])

        // When
        let fetchedBooking = try XCTUnwrap(
            viewModel.findBooking(byIdentifier: 1)
        )

        // Then
        XCTAssertEqual(fetchedBooking, mockedBooking)
    }
}

Prior to Xcode 11

Require is a simple, yet really useful framework for handling optionals in test cases (by John Sundell again πŸ˜ƒ). He also wrote a great blog post explaining the use-case for this framework: Avoiding force unwrapping in Swift unit tests

#12 – Safe access to an element at index

β›‘ Using the range operator, we can easily create an extension to safely return an array element at the specified index, or nil if the index is outside the 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: 2]
let pineapple = fruits[safe: 6]

// Does not crash, but contains nil
let invalid = fruits[safe: 7]

#11 – Check whether a value is part of a given range

πŸ’‘ Instead of writing x >= 10 && x <= 100, we can write 10 ... 100 ~= x.

Example:

let statusCode = 200

let isSuccessStatusCode = 200 ... 299 ~= statusCode
let isRedirectStatusCode = 300 ... 399 ~= statusCode
let isClientErrorStatusCode = 400 ... 499 ~= statusCode
let isServerErrorStatusCode = 500 ... 599 ~= statusCode

Another (more readable way) for checking whether a value is part of a given range can be achieved using the contains method:

let 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)

#10 – Use compactMap to filter nil values

πŸŽ› Using compactMap we can filter out any nil values of an array.

struct ItemDataModel {
    let title: String?
}

struct ItemViewModel {
    let title: String
}

extension ItemViewModel {
    /// Convenience initializer, that maps the data-model from the server to our view-model
    /// if all required properties are available.
    init?(dataModel: ItemDataModel) {
        guard let title = dataModel.title else {
            return nil
        }

        self.init(title: title)
    }
}

class ListViewModel {
    /// ...

    func mapToItemViewModel(response: [ItemDataModel]) -> ([ItemViewModel]) {
        // Using `compactMap` we filter out invalid data-models automatically.
        response.compactMap { ItemViewModel(dataModel: $0) }
    }
}

#09 – Prefer Set instead of array for unordered lists without duplicates

πŸ‘« Advantage over Array:

  • Constant Lookup time O(1), as a Set stores its members based on hash value.

Disadvantage compared to Array:

  • No guaranteed order.
  • Can't contain duplicate values.
  • All items we want to store must conform to Hashable protocol.

For further examples and use-cases please have a look at "The power of sets in Swift" (by John Sundell).

#08 – Remove all sub-views from UIView

πŸ“­ A small extension to remove all sub-views.

extension UIView {
    func removeAllSubviews() {
        subviews.forEach { $0.removeFromSuperview() }
    }
}

#07 – Animate image change on UIImageView

✍️ Easily (ex)change an image with using a transition (note that the .transitionCrossDissolve is the key to get this working).

extension UIImageView {
    func updateImageWithTransition(_ image: UIImage?, duration: TimeInterval) {
        UIView.transition(with: self, duration: duration, options: .transitionCrossDissolve, animations: { () -> Void in
            self.image = image
        })
    }
}

#06 – Change CALayer without animation

πŸ‘¨β€πŸŽ¨ CALayer has a default implicit animation duration of 0.25 seconds. Using the following extension we can do changes without an animation:

extension CALayer {
    class func performWithoutAnimation(_ runWithoutAnimation: () -> Void) {
        CATransaction.begin()
        CATransaction.setAnimationDuration(0.0)

        runWithoutAnimation()

        CATransaction.commit()
    }
}

#05 – Override layerClass to reduce the total amount of layers

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 useful to e.g. add a linear gradient behind an image. Furthermore we could change the gradient-color based on the time of the day, without having to add multiple images to our app. Example

You can see the full code for the example in my gist for the Vertical Gradient Image View.

#04 – Handle notifications in test cases

πŸ“¬ Examples on how to test notifications in test cases:

#03 – Use didSet on outlets to setup components

πŸ‘ By using didSet on outlets we can setup our view components (declared in a storyboard or xib) in a very readable way:

class FooBarViewController: UIViewController {

    // MARK: - Outlets

    @IBOutlet private var button: UIButton! {
        didSet {
            button.setTitle(viewModel.normalTitle, for: .normal)
            button.setTitle(viewModel.disabledTitle, for: .disabled)
        }
    }

    // MARK: - Private properties

    private var viewModel = FooBarViewModel()
}

#02 – Most readable way to check whether an array contains a value (isAny(of:))

✨ A small extension to check whether a value is part of a list of candidates, in a very readable way (by John Sundell)

extension Equatable {
    func isAny(of candidates: Self...) -> Bool {
        return candidates.contains(self)
    }
}

Example:

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)

#01 – Override self in escaping closure, to get a strong reference to self

🚸 To avoid retain cycles we often have to pass a weak reference to self into closures. By using the following pattern, we can get a strong reference to self for the lifetime of the closure.

someService.request() { [weak self] response in
    guard let self = self else { return }

    self.doSomething(with: response)
}

Notice: The above works as of Swift 4.2. Before you have to use:

guard let `self` = self else { return }

There is a great article about when to use weak self and why it's needed.

About

πŸ“– My personal collections of things, tips & tricks I've learned during iOS development so far and do not want to forget.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published