Only assign new values when the right operand is not nil

It’s sometimes useful to create custom operators to avoid boilerplate code. For example in the following code, we check if a variable is not nil before assigning it to our object:

if let basedOn = basedOn {
    story.basedOn = basedOn
}

It would be much better if we could simplify and have this instead:

story.basedOn ?= basedOn

To do that in Swift, we can create an “Optional Assignment” custom operator, as short as ?= here, with a few lines of code:

import Foundation
precedencegroup OptionalAssignment {
associativity: right
}
infix operator ?=: OptionalAssignment
public func ?= <T>(variable: inout T, value: T?) {
if let unwrapped = unwrap(any: value) ?? nil {
variable = unwrapped
}
}
public func unwrap<T: Any>(any: T) -> T? {
let mirror = Mirror(reflecting: any)
guard mirror.displayStyle == .optional else { return any }
guard let child = mirror.children.first else { return nil }
return unwrap(any: child.value) as? T
}

Reveal in Project Navigator shortcut

Xcode 11 buried even more this option allowing to reveal the current file in the Project Navigator:

Right Click / Navigate / Reveal in Project Navigator

Good news, there is also a keyboard shortcut for this action: ⇧ + ⌘ + J

Registering and dequeuing cells in a collection view

When using custom cells in UICollectionViews, you usually have to go through two things:

  • registering the cell: usually in the viewDidLoad method, you have to let the collection view know how to create a cell of a given type
  • dequeue the cell: usually in the collectionView(_:, cellForItemAt:) method, you ask the collection view to dequeue a cell of a given type so that you can configure it and return it

What’s wrong with that?

  • there is a lot of boilerplate code here
  • uncertainty around cell identifiers
  • when dequeuing, you always have to deal with this optional even though there isn’t much you can do if the cell isn’t there

Making registration easier

To make cell registration easier, each cell has to provide two things:

  • a cell identifier is a string identifying the cell
  • a cell nib is an object of type UINib used by the collection view to instantiate the cell

I created a protocol that will make more sense later on:

protocol UICollectionViewRegisterable {
    static var cellIdentifier: String { get }
    static var cellNib: UINib { get }
}

So the cell just needs to conform to this protocol:

extension HomeCell: UICollectionViewRegisterable {
    static var cellIdentifier: String {
        return "HomeCellID"
    }

    static var cellNib: UINib {
        return UINib(nibName: "HomeCell", bundle: nil)
    }
}

Then we have to register the cell in the collection view, here an extension on UICollectionView will help us get rid of the optional and enforce type:

extension UICollectionView {
    func register(type: UICollectionViewRegisterable.Type) {
        register(type.cellNib, forCellWithReuseIdentifier: type.cellIdentifier)
    }

    func dequeueCell<CellType: UICollectionViewRegisterable>(at indexPath: IndexPath) -> CellType {
        guard let cell = dequeueReusableCell(withReuseIdentifier: CellType.cellIdentifier, for: indexPath) as? CellType else {
            fatalError("UICollectionView should dequeue a cell of type \(CellType.self)")
        }
        return cell
    }
}

Registering the cell becomes really easy:

override func viewDidLoad() {
    super.viewDidLoad()

    // Configure collection view
    collectionView.register(type: HomeCell.self)
}

To dequeue the cell, the extension is using generics, so we can’t let type inference do the work here, and have to specify the type of the cell for the code to compile:

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell: HomeCell = collectionView.dequeueCell(at: indexPath)
    // TODO: configure cell
    return cell
}

That’s it! Here is the final code:

import UIKit
protocol UICollectionViewRegisterable {
static var cellIdentifier: String { get }
static var cellNib: UINib { get }
}
extension UICollectionView {
func register(type: UICollectionViewRegisterable.Type) {
register(type.cellNib, forCellWithReuseIdentifier: type.cellIdentifier)
}
func dequeueCell<CellType: UICollectionViewRegisterable>(at indexPath: IndexPath) -> CellType {
guard let cell = dequeueReusableCell(withReuseIdentifier: CellType.cellIdentifier, for: indexPath) as? CellType else {
fatalError("UICollectionView should dequeue a cell of type \(CellType.self)")
}
return cell
}
}

SwiftLint & SwiftFormat

These two tools are now part of my tool belt when creating an iOS project, I don’t see how I can go back working without, and I recommend them for any type of project.

SwiftLint

Automatically enforce Swift style, conventions and best practices. It’s a good idea to just install it when creating a new project and treat all warnings and errors as they come. Everything can be configured or disabled using a .swiftlint.yml file at the root of the project.

Repository: https://github.com/realm/SwiftLint

SwiftFormat

You have a preferred style for formatting your code, your teammates eventually have a different preferred style when formatting their code, and you find yourself spending too much time arguing about the format and/or asking others to update their code during reviews over format issues?

It’s helpful to agree on a common coding style with your team, and it’s even better if you can enforce it. SwiftFormat to the rescue, it enforces the coding style by automatically formatting the code.

I personally installed it for it to run during each build, the overhead is small and I don’t have to think about formatting anymore.

Repository: https://github.com/nicklockwood/SwiftFormat

Detecting when UIScrollView is bouncing

Let’s say you want to have your UI change when a scroll view is bouncing (or over-scrolling).

First you need to conform to the UIScrollViewDelegate methods, more specifically scrollViewDidScroll(_ scrollView: UIScrollView).

extension ViewController: UIScrollViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        // TODO
    }
}

For example, let’s change the background color of the scroll view depending on bouncing status. If user is over-scrolling from the top, let’s change it to red, green if over-scrolling from the bottom, and white otherwise.

First, let’s detect when bouncing from the top:

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    let topInsetForBouncing = scrollView.safeAreaInsets.top != 0.0 ? -scrollView.safeAreaInsets.top : 0.0
    let isBouncingTop: Bool = scrollView.contentOffset.y < topInsetForBouncing - scrollView.contentInset.top

    if isBouncingTop {
        scrollView.backgroundColor = .red
    } else {
        scrollView.backgroundColor = .white
    }
}

It’s fairly easy, if the current vertical contentOffset is smaller than all space at the top, then we are over-scrolling. Because the top space will differ depending on devices, we have to use the safeAreaInsets, but mind that -0.0 is different than 0, thus the ternary operator.

Now let’s detect when the scroll view is bouncing from the bottom:

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    let topInsetForBouncing = scrollView.safeAreaInsets.top != 0.0 ? -scrollView.safeAreaInsets.top : 0.0
    let isBouncingTop: Bool = scrollView.contentOffset.y < topInsetForBouncing - scrollView.contentInset.top

    let bottomInsetForBouncing = scrollView.safeAreaInsets.bottom
    let threshold: CGFloat
    if scrollView.contentSize.height > scrollView.frame.size.height {
        threshold = (scrollView.contentSize.height - scrollView.frame.size.height + scrollView.contentInset.bottom + bottomInsetForBouncing)
    } else {
        threshold = topInsetForBouncing
    }
    let isBouncingBottom: Bool = scrollView.contentOffset.y > threshold

    if isBouncingTop {
        scrollView.backgroundColor = .red
    } else if isBouncingBottom {
        scrollView.backgroundColor = .green
    } else {
        scrollView.backgroundColor = .white
    }
}

It’s a bit more tricky in this case, scroll views are bouncing wether or not there is enough content for them to reach them, so we need to define what threshold the content offset needs to pass. When the content size is not big enough, then any movement that is not bouncing from the top is bouncing from the bottom. If the content size is big enough, then we can use a similar logic than before.

Let’s cleanup a little, this looks like a good candidate for an extension:

extension UIScrollView {
var isBouncing: Bool {
return isBouncingTop || isBouncingBottom
}
var isBouncingTop: Bool {
return contentOffset.y < topInsetForBouncing – contentInset.top
}
var isBouncingBottom: Bool {
let threshold: CGFloat
if contentSize.height > frame.size.height {
threshold = (contentSize.height – frame.size.height + contentInset.bottom + bottomInsetForBouncing)
} else {
threshold = topInsetForBouncing
}
return contentOffset.y > threshold
}
private var topInsetForBouncing: CGFloat {
return safeAreaInsets.top != 0.0 ? -safeAreaInsets.top : 0.0
}
private var bottomInsetForBouncing: CGFloat {
return safeAreaInsets.bottom
}
}

Now we simply need to call isBouncingTop or isBouncingBottom on the scroll view:

extension ViewController: UIScrollViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        if scrollView.isBouncingTop {
            scrollView.backgroundColor = .red
        } else if scrollView.isBouncingBottom {
            scrollView.backgroundColor = .green
        } else {
            scrollView.backgroundColor = .white
        }
    }
}

And because UITableView and UICollectionView inherit from UIScrollView, it will work for these as well.