Building Custom Views with SwiftUI

Description: Learn how to build custom views and controls in SwiftUI with advanced composition, layout, graphics, and animation. See a demo of a high performance, animatable control and watch it made step by step in code. Gain a deeper understanding of the layout system of SwiftUI.

  • View always has the size of its body
  • The rootView has the dimensions of the device minus the safe area insets.
  • The top layer of any view with a body is always what we call layout neutral. Because its view bounds are defined by the bounds of its body, regardless of what’s above.

Layout Process

Three steps:

  1. The root view offers our view a proposed size (the safe layout area)
  2. The view answers with the size it would like
  3. The root view, know with its child view size knowledge, put the child somewhere (by default at the center of its size, both vertically and horizontally)

In SwiftUI there's no way to force a size on the view's child: the parent has to respect that choice.

In short:

  1. Parent proposes a size for child
  2. Child chooses its own size
  3. Parent places child in parent’s coordinate space
  4. (Bonus) SwiftUI rounds coordinates to the nearest pixel

Views sizes

Since every view controls its own size, when we build a view, we get to decide how and when it resizes.

The bounds of a Text view never stretch beyond the height and width of its displayed lines.

When we need to fight for space, especially in stack views, we can raise the layout priority by setting the view layoutPriority (0 by default).

Custom Alignment

Need of an alignment between views of different stacks?

We can achieve so by creating an extension of VerticalAlignment (or any other alignment), and then use that as our .alignmentGuide in the interested views.

Graphics

  • SwiftUI has some built in shapes (Circle for example) that conform to the Shape protocol and have their own modifiers, since all shapes are effectively rendered as Views, modifiers of shapes can be applied to views and vice versa.
  • We can create custom shapes by conforming to the Shape protocol and implementing the function path(in:) that draws a path representing the desired shape.
  • By implementing the animatableData property in our custom shapes, we can tell SwiftUI how this shape can be interpolated for animations.
  • When a view appears/disappears, the default transition animation is to fade-in/fade-out. We can customize these transitions by creating a custom ViewModifier and specifying how the view should change between the end states of the animation, then including that ViewModifier in an asymmetric transition.
  • SwiftUI ViewModifiers are Views defined as a function of some other view. This can be clearly seen when creating a custom ViewModifier where we will have to implement a body function, which takes a generic Content as a parameter (which is constrained to conform to View) and returns some View.
  • By default, SwiftUI renders all views natively as either a UIView or an NSView (based on the platform it's rendering for), and that's typically what we want for normal controls like Buttons and TextFields. However, if we need better rendering performance for many views, then we can flatten our views inside a drawingGroup, which is a special way of rendering graphics, where our view hierarchy is flatten into a single UIView or NSView, which renders all our views using Metal.

Missing anything? Corrections? Contributions are welcome 😃

Related

Written by

Federico Zanetello

Federico Zanetello

Software engineer with a strong passion for well-written code, thought-out composable architectures, automation, tests, and more.

Ammar AlTahhan

Ammar AlTahhan

Software Engineer