Open Source Diaries: Y—Matter Type

Y—Matter Type

This is the second article in a series that details the Open Source Work for iOS that I have been leading at YML.

Typography

The inspiration for the Y—Matter Type library came from the most common mistake I see when converting designs to code on iOS: overlooking line height. Designs define typographies (or text styles), which describe how text should be rendered on screen. The four basic parameters of a typography are:

  • font family
  • font weight
  • font size
  • line height
Font family: Helvetica Neue
Font style: Regular
Font size: 16px
Line height: 24px

For example, a design might call for a regular weight Helvetica Neue font with 16 pt font size and 24 pt line height. The first three parameters tell you which font to load on iOS, but the fourth parameter is typically just ignored outright.

let font = UIFont(name: "HelveticaNeue", fontSize: 16)

The code above generates the correct font, but what is its line height? Is it 16? It turns out that for a 16pt regular Helvetica Neue font, it is 18.64. A UILabel generated with that font will have a height of 19 on a 2x retina device (rounding up to the nearest pixel). But the designs expect that label to be 24 points tall per line of text. Of course you could constrain the label to have a height of 24, but then it could not support Dynamic Type. Also if it contains multiple lines of text, you don’t necessarily know how many lines it will wrap to and so couldn’t possibly constrain the height correctly.

The way you get line height to render in UILabel, UIButton, UITextView is by using paragraph styles and attributed strings. (To my knowledge SwiftUI does not yet support line height in any of its text views, which is one reason why I continue to utilize UIKit.) But applying paragraph styles via attributed strings to every button and label in an application would also be burdensome (which is why it typically just gets ignored).

To solve this, we created a Typography model object that captures all the parameters of design system typography and then accurately renders text in UIKit (which can then be hosted in SwiftUI if necessary). In addition to the most common parameters listed above, Typography also supports:

  • letter spacing (kerning)
  • paragraph indent
  • paragraph spacing
  • text case (uppercase, lowercase, title case)
  • text decoration (underline, strikethrough)

Typography objects are determined by the design (and with proper transformation can be automatically generated from design files) and then rendered via subclasses for all the text-based UIView classes.

Y—Matter Type declares TypographyLabel, TypographyButton, TypographyTextField, and TypographyTextView classes to replace use of UILabel, UIButton, UITextField, and UITextView, respectively.

Accessibility Features

Typography supports Dynamic Type scaling and the Accessibility Bold Text feature by default for any custom font.

Adopting Typography is a bit of a paradigm shift because you no longer work with fonts, only typographies. That’s because the exact font chosen will be determined at run time based on the current trait collection. For example, the following label might actually use a Helvetica Medium font instead of Helvetica Regular if the user has enabled the Bold Text setting.

let body = Typography(
    familyName: "HelveticaNeue",
    fontWeight: .regular,
    fontSize: 16,
    lineHeight: 24
)
let label = TypographyLabel(typography: body)

The Bold Text feature works by having information about an entire font family (what weights are available) and not just a single font. If we know all the available weights, then we can vend the next heavier weight (if any) when Bold Text is enabled.

Dynamic Type scales Typography proportionately with line height. So the 16/24 Typography shown above scaled to 200% would use a 32pt font rendered with a 48pt line height.

Overview

The goals of the Y—Matter Type package are to:

  • Support line height and other typographical properties (letter spacing, text decorations, text cases, and more) across labels, buttons, text fields, and text views
  • Support Dynamic Type scaling and Accessibility Bold Text on custom fonts
  • Accelerate accurate translation of Figma designs into code by having text-based elements with the exact same intrinsic size (based upon line height) as the corresponding elements in Figma

I am proud that we delivered low-level components that make it trivial to accurately render text as per designs while at the same time including Dynamic Type and Bold Text support with little additional burden for the developer. To my knowledge no client has ever asked for Bold Text support (or even knew it existed really), but we build that in for their users anyway.

Together with Y—CoreUI, Y—Matter Type is part of how we build iOS apps at YML.

Open Source Diaries: Y—CoreUI

Y—Core UI

This is the first article in a series going into detail about the Open Source Work for iOS that I have been leading at YML.

One of the first Swift package we created and released was Y—CoreUI. As its name suggests, it is one of our core libraries for doing UI work on iOS. It consists primarily of:
1. UIView extensions for declarative Auto Layout
2. UIColor extensions for WCAG 2.0 contrast ratio calculations
3. Protocols to aid loading string, color, and image assets
4. Model objects to declare drop shadows and animations

UIView extensions for declarative Auto Layout

I’m a big proponent of building UI’s entirely in code when working in UIKit. NSLayoutConstraint is an old friend, but cumbersome to use. These layout extensions just create NSLayoutContraint‘s but in a more declarative and less verbose manner and with smart defaults for most parameters. Want to constrain a subview to all 4 sides of its superview?

subview.constrainEdges()

Want to pin a subview to the center of its superview?

subview.constrainCenter()

It’s that simple. Need to do something more complex like centering a view horizontally but ensuring that it doesn’t overflow its superview? That’s also simple.

subview.constrainCenter(.x)
subview.constrainEdges(.horizontal, relatedBy: .greaterThanOrEqual)

(That last line is clever enough to invert the relationship and created the .trailingAnchor constraint with the .lessThanOrEqual relation.)

UIColor extensions for WCAG 2.0 contrast ratio calculations

Dark and light pink color swaths with text reading #82007D, Contrast 7.23, #FFD6F9

I think this is my favorite part of this library. WCAG 2.0 specifies contrast requirements for foreground and background colors that appear together to meet AA or AAA levels of compliance (I like this handy checker). This ratio works by converting the color into LCh color space (great article on LCh) and comparing the Luminance values. There are various design tools to check these contrast ratios, but what if we could do it programmatically as part of unit tests and across both light and dark modes and regular and increased contrast modes?

XCTAssert(
    foregroundColor.isSufficientContrast(
        to: backgroundColor,
        context: .normalText,
        level: .AA
    )
)

As a bonus we expanded the calculations to work when the foreground color is not opaque (this is the case for Apple’s system colors such as UIColor.secondaryLabel). If you need the exact contrast ration value, there’s a method for that as well.

let ratio: CGFloat = foregroundColor.contrastRatio(to: backgroundColor)

Protocols to aid loading string, color, and image assets

These are a series of protocols that make it simple and testable to load localized strings, colors, images, and even SF Symbols using enums. Of course it’s trivial to load a color from an asset catalog using UIColor(named: "someName"), but from my viewpoint this has some drawbacks:
1. not easily unit testable
2. fails silently (returns nil)

Y—CoreUI provides a Colorable protocol that any string-based enum automatically conforms to and adds the following methods:

var color: UIColor { get }
func loadColor() -> UIColor?

loadColor() is failable and ideal for unit testing, while color is guaranteed to return a color (by default UIColor.systemPink as a fallback color), but will also log a warning to the console if it falls back. Usage and unit testing is simple:

enum WarningColors: String, Colorable, CaseIterable {
    case warning50
    case warning100
}

func test_warningColors_deliversColors {
    WarningColors.forEach {
        XCTAssertNotNil($0.loadColor())
    }
}

The Localizable, ImageAsset, and SystemImage protocols do the same thing for localized string resources, image assets, and system images (SF Symbols), respectively.

Model objects to declare drop shadows and animations

A large part of our jobs as iOS developers is rendering user interfaces from designs, but this can some times be difficult because the way designs are described in design files can differ from how they are implemented on iOS. For example, Figma and Sketch describe drop shadows as they are defined for the web using the parameters: horizontal offset, vertical offset, blur radius, and spread. On iOS though we have shadowRadius, which when you look in Stack Overflow contains incorrect answers as to how to convert blur radius to shadowRadius. In Y—CoreUI we defined an Elevation object that describes a drop shadow in web/design terminology and knows how to properly apply them to views. This includes correctly calculating spread for rects and round rects. The Elevation object was designed to be automatically generated from Figma tokens using transformation tools such as Style Dictionary.

Another useful model object is Animation which lets us build components with customizable animations without necessarily knowing exactly which type of animation will be chosen. We leverage this for the animations in our snackbar/toast library, Y—Snackbar.

/// Animation curve
public enum Curve: Equatable {
    /// Regular animation curve
    case regular(options: UIView.AnimationOptions)
    /// Spring animation
    case spring(damping: CGFloat, velocity: CGFloat, options: UIView.AnimationOptions = [])
}

That wraps up a cursory overview of YML’s core iOS UI library. In my next article I will describe its companion library for rendering design system typography, Y—Matter Type.

Open Source work

As part of my role as iOS Chapter Lead at YML, I lead a team that builds reusable code that can be shared across projects to accelerate development, reduce bugs through the use of well-tested code, and inject accessibility features into projects with little additional overhead.
Animated GIF of two tags reading
Over the past year or so we have started releasing this code as a series of open source Swift packages on GitHub. To date we have released 14 Swift packages! The easiest way to view them is actually on the Swift Package Index.

These packages are already part of several shipping products. You might have used them to order your food, buy your groceries, sell your car, or schedule a doctor’s appointment.

I’m incredibly proud of this work and our little team. Every package in this collection is:

  • distributed as a Swift package
  • Apache 2.0 licensed
  • linted to identical standards (with SwiftLint, naturally)
  • highly unit tested (90–100% code coverage)
  • public apis 100% documented
  • includes an auto-generated documentation website hosted via GitHub Pages. (Example here) (courtesy of Jazzy)
  • protected through GitHub Actions to pass strict linting and unit tests

Over the next week I plan to write a series of posts that go into more detail on some of the projects. Many of them include some pretty cool accessibility features that I am proud to have shipped and to have included in apps that reach tens of millions of users.

Open Source Diaries

MPFlipViewController: a page-flipping container controller


I put a project called MPFlipViewController up on GitHub. It’s a page-flipping container view controller that allows the user to flip through a series of view controllers as if they were pages in a book. It is based on the MPFlipTransition class I already have on GitHub, but instead of being just a transition between 2 views, it is a full-fledged container view controller that supports panning and swiping between pages (child view controllers). It follows the Containment API introduced in iOS 5, so it behaves as a proper container view controller similar to the system container controllers (e.g. UINavigationController, UITabBarController and UIPageViewController).

Requirements

Xcode 4.3
iOS 5
ARC

API

The API is based on the API for UIPageViewController (since they fulfill an almost identical role). There is a data source protocol that you implement to specify the previous and next pages, and a delegate protocol that you implement in order to receive feedback on whether or not a page-turn operation completed and also to optionally specify the new orientation in the event that device orientation changes (i.e. user rotates the device).

Use

To create a flip controller use the initWithOrientation: method and pass in the desired orientation (horizontal or vertical):

- (id)initWithOrientation:(MPFlipViewControllerOrientation)orientation;


To set the content use the setViewController:direction:animated:completion: method where direction indicates whether the animation should be a page flip forward or backward.

- (void)setViewController:(UIViewController *)viewController 
                direction:(MPFlipViewControllerDirection)direction 
                 animated:(BOOL)animated 
               completion:(void (^)(BOOL finished))completion;

To enable touch gestures (panning and swiping between pages) implement the MPFlipViewControllerDataSource delegate to provide the previous and next pages (if any). Return nil for either method to indicate the user is already on the first or last page.

- (UIViewController *)flipViewController:
          (MPFlipViewController *)flipViewController
      viewControllerBeforeViewController:
          (UIViewController *)viewController;

- (UIViewController *)flipViewController:
          (MPFlipViewController *)flipViewController
       viewControllerAfterViewController:
          (UIViewController *)viewController;

To be notified of whether a page turn animation completed or not, set the MPFlipViewControllerDelegate and implement the optional flipViewController:didFinishAnimating:previousViewController:transitionCompleted: method. This method is only called if the page turn was gesture-driven (i.e. in response to a pan or swipe), and not programmatic (i.e. in response to a call to setViewController:direction:animated:completion:).

- (void)flipViewController:(MPFlipViewController *)flipViewController 
        didFinishAnimating:(BOOL)finished 
    previousViewController:(UIViewController *)previousViewController 
       transitionCompleted:(BOOL)completed;

To change the orientation of the flip controller when device orientation changes, set the MPFlipViewControllerDelegate and implement the optional flipViewController:orientationForInterfaceOrientation: method and return the desired orientation.

- (MPFlipViewControllerOrientation)flipViewController:
                       (MPFlipViewController *)flipViewController 
                   orientationForInterfaceOrientation:
                       (UIInterfaceOrientation)orientation;

Demo Project

The GitHub project includes a sample project that demonstrates the use of the control and its API.

MPFlipTransition – add flip transitions to your app


I’ve added flip transitions to my MPFoldTransition project on GitHub. It provides a class you can use to add a page-flipping transition to your application in just a single line of code (in most cases).

Update: For a touch gesture-enabled container controller with page-flipping (not just a transition), see MPFlipViewController.

Features

There are 3 style bits that can be combined to create 8 different animations.

Direction


Controls whether the page flips from right to left (Forward) or left to right (Backward).

Orientation


Sets whether the page flip is horizontal or vertical.

Perspective


Determines whether the page flips towards the user (Normal) or away from the user (Reverse).

Present a modal view controller

There are extension methods to UIViewController to present or dismiss modal view controllers using flip transitions:

- (void)presentViewController:(UIViewController *)viewControllerToPresent
                    flipStyle:(MPFlipStyle)style 
                   completion:(void (^)(BOOL finished))completion;

- (void)dismissViewControllerWithFlipStyle:(MPFlipStyle)style 
                                completion:(void (^)(BOOL finished))completion;

From your UIViewController subclass you would call this to present your modal view controller:

[self presentViewController:modalViewController
                  flipStyle:MPFlipStyleDefault 
                 completion:nil];

And then call this to dismiss it:

[self dismissViewControllerWithFlipStyle:MPFlipStyleDirectionBackward 
                              completion:nil];

Tip: dismiss your modal controller using a style with the opposite direction bit (Forward or Backward), so that you get the reverse animation.

Push a view controller onto a navigation stack

There are extension methods to UINavigationController to push or pop a view controller using flip transitions:

- (void)pushViewController:(UIViewController *)viewController
                 flipStyle:(MPFlipStyle)style;

- (UIViewController *)popViewControllerWithFlipStyle:
    (MPFlipStyle)style;

From your UIViewController subclass you would call this to push a new view controller onto the stack:

[self.navigationController pushViewController:detailViewController
                                    flipStyle:MPFlipStyleDefault];

And then call this to pop it back off:

[self.navigationController popViewControllerWithFlipStyle:MPFlipStyleDirectionBackward];

Tip: pop your view controller using a style with the opposite direction bit (Forward or Backward) from the style used to push it onto the stack, so that you get the reverse animation.

Transition between any 2 views or controllers

MPFlipTransition has class methods for generic view and view controller transitions:

+ (void)transitionFromViewController:(UIViewController *)fromController
                    toViewController:(UIViewController *)toController
                            duration:(NSTimeInterval)duration
                               style:(MPFlipStyle)style
                          completion:(void (^)(BOOL finished))completion;

+ (void)transitionFromView:(UIView *)fromView
                   toView:(UIView *)toView
                 duration:(NSTimeInterval)duration
                    style:(MPFlipStyle)style
         transitionAction:(MPTransitionAction)action
               completion:(void (^)(BOOL finished))completion;

If you really need to get under the hood (e.g. to adjust the timing curve, shadow effects, or skew), you can initialize your own instance of MPFlipTransition, set the properties as desired, and then call the perform: method to execute the transition.

Storyboard support

You can even incorporate modal or navigation stack flip segues without writing a single line of code! Simply use 1 of the 3 custom UIStoryboardSegue subclasses that are included. These cover modal presentation and push/pop to a navigation stack. Just create a segue between 2 controllers in your storyboard, select Custom as the segue type, then enter MPFlipModalSegue, MPFlipNavPushSegue, or MPFlipNavPopSegue as the Segue Class.

Demo project

The GitHub project includes a sample project that demonstrates the use of all the different API’s as well as all of the transition styles.

iOS version

iOS 5-only because I wrote it with ARC and included storyboard support.

MPFoldTransition – add fold transitions to your app


I’ve put a project I call MPFoldTransition up on GitHub. It provides a class you can use to add a fold transition to your application in just a single line of code (in most cases).
What is a fold transition? It’s an animation such as shown above (or as popularized by the Clear todo list app) where an object folds in upon itself until it disappears and the content surrounding it moves in to fill the gap. Except in this case, I’m doing it for an entire UIView or UIViewController and not just a single UITableViewCell. (For an in-depth analysis of what goes into making a folding animation, read my post here.)

Update: The GitHub project now includes flip transitions as well!

Update 2: The folding effect has been improved with gradient shadows.

Update 3: For a touch gesture-enabled container controller with page-flipping (not just a transition), see MPFlipViewController.

Features

There are 3 style bits that can be combined to create 8 different animations.

Direction


Controls whether the current view collapses inward (Fold) or if the next view expands outward (Unfold).

Mode


Determines whether the next view slides in flat (Normal) or rotates in at right angles to the collapsing halves of the previous view (Cubic).

Orientation


Sets whether the fold is vertical or horizontal.

Present a modal view controller

There are extension methods to UIViewController to present or dismiss modal view controllers using fold transitions:

- (void)presentViewController:(UIViewController *)viewControllerToPresent
                    foldStyle:(MPFoldStyle)style 
                   completion:(void (^)(BOOL finished))completion;

- (void)dismissViewControllerWithFoldStyle:(MPFoldStyle)style 
                                completion:(void (^)(BOOL finished))completion;

From your UIViewController subclass you would call this to present your modal view controller:

[self presentViewController:modalViewController
                  foldStyle:MPFoldStyleDefault 
                 completion:nil];

And then call this to dismiss it:

[self dismissViewControllerWithFoldStyle:MPFoldStyleUnfold 
                              completion:nil];

Tip: dismiss your modal controller using a style with the opposite fold bit (Fold or Unfold), so that you get the reverse animation.

Push a view controller onto a navigation stack

There are extension methods to UINavigationController to push or pop a view controller using fold transitions:

- (void)pushViewController:(UIViewController *)viewController
                 foldStyle:(MPFoldStyle)style;

- (UIViewController *)popViewControllerWithFoldStyle:
    (MPFoldStyle)style;

From your UIViewController subclass you would call this to push a new view controller onto the stack:

[self.navigationController pushViewController:detailViewController
                                    foldStyle:MPFoldStyleDefault];

And then call this to pop it back off:

[self.navigationController popViewControllerWithFoldStyle:MPFoldStyleUnfold];

Tip: pop your view controller using a style with the opposite fold bit (Fold or Unfold) from the style used to push it onto the stack, so that you get the reverse animation.

Transition between any 2 views or controllers

MPFoldTransition has class methods for generic view and view controller transitions:

+ (void)transitionFromViewController:(UIViewController *)fromController
                    toViewController:(UIViewController *)toController
                            duration:(NSTimeInterval)duration
                               style:(MPFoldStyle)style
                          completion:(void (^)(BOOL finished))completion;

+ (void)transitionFromView:(UIView *)fromView
                   toView:(UIView *)toView
                 duration:(NSTimeInterval)duration
                    style:(MPFoldStyle)style
         transitionAction:(MPTransitionAction)action
               completion:(void (^)(BOOL finished))completion;

If you really need to get under the hood (e.g. to adjust the timing curve), you can initialize your own instance of MPFoldTransition, set the properties as desired, and then call the perform method to execute the transition.

Storyboard support

You can even incorporate modal or navigation stack fold segues without writing a single line of code! Simply use 1 of the 3 custom UIStoryboardSegue subclasses that are included. These cover modal presentation and push/pop to a navigation stack. Just create a segue between 2 controllers in your storyboard, select Custom as the segue type, then enter MPFoldModalSegue, MPFoldNavPushSegue, or MPFoldNavPopSegue as the Segue Class.

Demo project

The GitHub project includes a sample project that demonstrates the use of all the different API’s as well as all of the transition styles.

iOS version

iOS 5-only for now because I wrote it with ARC and included storyboard support.