Considerate Apps Sample Code

An example of a considerate component might make it easier to build considerate apps.

While the concepts behind the Considerate Apps talk were first tested in the App Store Connect for iOS app, I wanted to make certain I had something to share with developers to help clarify the ideas. That meant building what I’ve heard described as a micro-framework.

Let’s start with Component. This is a basic UIView subclass which arranges its subviews in a contentView according to constraints vended from constraintsForHorizontalOrientation or constraintsForVerticalOrientation. A Component has two values for orientation, preferredOrientation and effectiveOrientation. The preferredOrientation is the orientation the component would desire under ideal circumstances, e.g. type size is small and there’s plenty of space. The effectiveOrientation is the orientation determined by computeIdealOrientation in an attempt to make the content fit best.

When a Component receives either traitCollectionDidChange or didMoveToSuperview, it calls setNeedsUpdateEffectiveOrientation to update the value of effectiveOrientation. This ensures it will have the ideal layout as the content size category changes and when it is first added to the view hierarchy. It’s important not to call updateEffectiveOrientation too often, because it might not be the fastest thing we could do on the main thread.

Then we have either FlexibleHeader or AdaptiveHeader. These do essentially the same thing, but with a slightly different implementation of computeIdealOrientation.

In the FlexibleHeader implementation of computeIdealOrientation all that’s done is to determine whether the content size category represents an accessibility category size:

public override func computeIdealOrientation() -> Orientation {
    if traitCollection.preferredContentSizeCategory.isAccessibilityCategory {
        return .vertical
    } else {
        return .horizontal
    }
}

On the other hand, AdaptiveHeader uses the more sophisticated approach of determining whether the title label will truncate to determine which orientation to return:

public override func computeIdealOrientation() -> Orientation {
    if self.titleWillTruncate() {
        return .vertical
    } else {
        return .horizontal
    }
}

The implementation of titleWillTruncate is really quite simple:

func titleWillTruncate() -> Bool {
    guard let title = self._title, let text = title.text else {
        return false
    }

    guard text.count > 0 else {
        return false
    }

    let bounds = title.bounds
    let measureSize = CGSize(width: bounds.width,
        height: CGFloat.greatestFiniteMagnitude)
    let measureBounds = CGRect(origin: CGPoint.zero, size: measureSize)
    let fullRect = title.textRect(forBounds: measureBounds,
        limitedToNumberOfLines: 0)
    let titleRect = title.textRect(forBounds: measureBounds,
        limitedToNumberOfLines: title.numberOfLines)
    return fullRect.height > titleRect.height;
}

In essence, if the height of the full, unbounded rectangle for the text in the title is greater than the actual title rectangle, then the title label is truncated.

Take a look at the source on GitHub and feel free to let me know what you think.