Skip to content
memo.d.foundation / topics / swift-custom-view-recipesConnect

Research notes/Product engineering

Building custom iOS views in Swift

Three step-by-step recipes for custom views in Swift, a passcode input, a draggable bottom menu and circular text in SwiftUI, each built from gestures, input handling or geometry.

Use with AI

Each recipe below builds one custom view from a small set of primitives. The passcode view takes keyboard input and maps it to dots. The bottom menu follows a drag gesture and snaps between heights. The circular text measures each character and rotates it around a circle. The first two use UIKit, the third uses SwiftUI. Each recipe links to its full source code.

1Passcode view

The passcode view can becomeFirstResponder and resignFirstResponder to show and hide the virtual keyboard when needed. That makes it behave much like a TextField.

Finished passcode view with PIN dots

Define the passcode view:

Passcode view class definition

1.1Show the keyboard

By default, the user must touch a TextField or TextView to show the keyboard. A custom view becomes a keyboard input view when it overrides canBecomeFirstResponder and conforms to the UIKeyInput protocol.

Overriding canBecomeFirstResponder

Conforming to UIKeyInput

Now the view can show the keyboard. Test it by calling passcode.becomeFirstResponder().

Calling becomeFirstResponder in a test

Keyboard shown by the passcode view

To make the view easy to use, add a tap gesture that calls becomeFirstResponder(). Now a tap shows the keyboard.

Tap gesture that shows the keyboard

1.2Handle the input

The input logic centers on three methods:

The three UIKeyInput methods

Append to or delete from the code string as needed:

Appending and deleting characters in the code string

1.3Map the code to PIN dots

With the logic finished, the next challenge is mapping the code to a PIN UI when the code changes. Add a UIStackView to the passcode view. The stack distributes "dot views" as the PIN.

Adding a UIStackView to the passcode view

Stack view layout for the PIN dots

Create the PIN view:

PIN view definition

Create two helper methods, one for an empty pin and one for a normal pin:

Helper methods for empty and filled pins

Map the user's input code to an array of pin views and distribute them in the stack:

Mapping the code to an array of pin views

Add a helper method that removes all arranged subviews from the stack:

Helper that removes all arranged subviews

Call the stack update when the code changes:

Updating the stack when the code changes

1.4Test and finish

Create a view controller to test the stack:

View controller used to test the passcode view

Make PasscodeView conform to UITextInputTraits so you can set the keyboard to the number pad:

Conforming to UITextInputTraits for a number pad

Result:

Passcode view result with number pad

Add a callback for when the user finishes the input:

Completion callback code

Using the completion callback

Full source code: Passcode on GitHub. The project was written in Xcode 11 beta. Copy all the Passxxx and Pin files into your Xcode project to test.

2Draggable bottom menu

This recipe recreates the bottom menu of Apple's Find My app in a few lines of code. The device list menu has three states: collapsed, half expanded and expanded.

The three bottom menu states: collapsed, half expanded and expanded

The view needs to do two things:

  1. Update its own height to follow the user's finger while dragging (pan gesture).
  2. Resize its height automatically, based on the direction of the drag and the position compared to the "half position".

2.1Set up the view

Create an Xcode project and add a new UIView named BottomMenuView.

Add a PanView as a subview of BottomMenuView. Constrain its leading, trailing and top edges to the superview and set the height constraint to 44.0. The PanView tracks the user's finger with a UIPanGestureRecognizer.

PanView constraints inside BottomMenuView

Add a bottom constraint for the PanView, and make outlets for the PanView and the bottom constraint. Xcode may warn about conflicting constraints, which is fine.

Outlets for the PanView and its bottom constraint

2.2Write the code

Define some variables to set the menu heights:

Variables for the menu heights

Add a pan gesture to the panView and handle the pan event:

Adding and handling the pan gesture

Update the menu height based on the drag direction and position:

Updating the menu height by drag direction and position

Set up the UI and the initial menu height in awakeFromNib:

Setting up the UI in awakeFromNib

Finally, test the menu by adding it to a view controller:

Adding the menu to a view controller

Bottom menu running in the test project

Full source code: BottomMenu on GitHub.

3Circular text in SwiftUI

This recipe lays text along a circle in SwiftUI.

Finished circular text

Open Xcode, create a new SwiftUI project, and add a new SwiftUI file named CircularText. The first two attributes of the control are the string and the radius of the circle.

The string and radius attributes

3.1Split the string

To make circular text, split the string into an array of characters. Each character gets its own Text control, and each control is rotated to match the circle. Define a computed property that splits the string into an array of characters:

Computed property that splits the string into characters

Use enumerated to get the offset, which is needed later.

3.2Compute the angle

To calculate the rotation angle you need two things: the perimeter of the circle and the width of each character.

Perimeter of the circle

The perimeter is used later through radius.perimeter.

The width of each character is harder. The character must sit in some control, and the control must report its size. Text is the best candidate. Embed the Text and a Spacer in a VStack. This gives a custom control as tall as the parent view, with the text at the top, which makes rotation easy because you only need to rotate around the center of the control.

VStack with Text and Spacer for each character

Now there is a list of Text views and the next step is getting each width. SwiftUI lets you read a control's size with GeometryReader, and you send it outside the view with a PreferenceKey.

GeometryReader and PreferenceKey code

Place the custom Sizeable view as the background of the Text. A Sizeable view is actually a Color, so it fills its parent (the background of the Text) and reports exactly the size of the Text. To store the sizes, add a dictionary named textSizes. The key is the position of the character.

The textSizes dictionary

Update the VStacks to save the sizes when the preference changes:

Saving sizes when the preference changes

With the perimeter and each character's width, calculate the angle of each character by its position:

Angle calculation for each character

This function calculates what percentage of the perimeter the character's width takes, then derives the angle from that percentage.

3.3Rotate and adjust

With the angle in hand, apply the rotation and look at the result:

Characters rotated around the circle

The spacing between characters looks wrong. It only fits horizontal text, and the rotation pushes the characters close together. The built-in kerning modifier adjusts the spacing.

Adjusting spacing with the kerning modifier

To center the text, take the angle of the last character, divide it by 2, and rotate the whole text anticlockwise by that amount.

Centered circular text

Full source code: SwiftUIGeometryPractice on GitHub.