SwiftUI: Layout & Interfaces

Nov 18 2021 Swift 5.5, iOS 15, Xcode 13

Part 2: Aligning Views

13. Custom Alignment

Episode complete

Play next episode

Next
Save for later
About this episode
See versions

See forum comments
Cinema mode Mark complete Download course materials
Previous episode: 12. Alignment Guides Next episode: 14. Challenge: Custom Alignment Guides

This course was originally recorded in 2019. It has been reviewed and all content and materials updated as of October 2021.

By now, you should be feeling pretty familiar with all of the built-in Alignments that SwiftUI offers to you. Three HorizontalAlignments and five VerticalAlignments.

And in the last last episode, you learned how to tailor them to suit your needs, using alignment guide modifiers. But that’s not the limit of how far you can go. SwiftUI also offers you the flexibility of creating your own named and reusable Alignments.

Why? Well, when you want to line up guides of particular views, but those views are inside of different stacks, custom alignments are the simplest solution.

For example! Here, we’ve got a VStack. And within in, three HSTacks.

ScaledImage is just a View I made to avoid having to type out the same modifiers for all five Images.

By default, as you know, the VStack is using center HorizontalAlignment. But let’s say, we want to visually match up a View’s leading edge, from the top HStack, with the centers of the other HStacks.

You might try adding an alignmentGuide to do that. First, matching the HorizontalAlignment from the outer stack…

        Text("Learn SwiftUI layout!")
          .alignmentGuide(HorizontalAlignment.center) { dimension in
            /*@START_MENU_TOKEN@*/ /*@PLACEHOLDER=Value@*/dimension[.top]/*@END_MENU_TOKEN@*/
          }

…and then, defining a horizontal guide.

        Text("Learn SwiftUI layout!")
          .alignmentGuide(HorizontalAlignment.center) { $0[.leading] }
      }

But, that doesn’t work. Maybe let’s add an explicit alignment value for the VStack…?

VStack(alignment) {

No. Doesn’t help.

But while no Alignment provided by SwiftUI will work, that’s not a showstopper!

Because. What SwiftUI does provide, is the AlignmentID protocol. It’s got one requirement: a type method to get a default alignment guide, based on a ViewDimensions.

This method is exactly like the trailing closures you wrote in the last episode. Only now, you’ll be defining what happens when you don’t use an alignment guide modifier. It’s the default value for a custom alignment.

Any type can adopt the AlignmentID protocol. But most commonly, you’ll see it used with enumerations that don’t have any cases. Because it’s the simplest type that you can make, in Swift.

For naming our AlignmentID type, we can go with the idea that we still want to base things on “Center” alignment. But, we have to do something custom.

}

enum CustomCenter: AlignmentID {

}

struct ScaledImage: View {

With the protocol adopted, we can add the method stub.

enum CustomCenter: AlignmentID {
  static func defaultValue(in context: ViewDimensions) -> CGFloat {
    <#code#>
  }
}

By default, put the guide at the view’s horizontal center.

context[HorizontalAlignment.center]

So now you’ve got an Alignment ID, but that’s only a component of a full Alignment instance.

In order to actually create an Alignment, you need to use this initializer. Both HorizontalAlignment and VerticalAlignment have it.

“AlignmentID.Type” is what’s called a “metatype”. Among other uses, it’s the way to use types, themselves, as method arguments. To get a type’s metatype, you write “dot self” after its name.

So, to make a HorizontalAlignment that corresponds with your new CustomCenter AlignmentID type, this is how you do it.

But you’re typically not going to see that initializer used within views, when they incorporate custom alignments. Here’s what you can do, to match the clean alignment syntax you’ve seen so far in this course.

First, you put your AlignmentID within an extension for the Alignment that it’s designed for. In your case, HorizontalAlignment.

extension HorizontalAlignment {
  enum CustomCenter: AlignmentID {
    static func defaultValue(in context: ViewDimensions) -> CGFloat {
      context[HorizontalAlignment.center]
    }
  }
}

Then, you create a static constant with the same name as the type, only lowercase, because it’s going to be an instance.

  }

  static let customCenter =
}

For the value, instantiate a HorizontalAlignment using CustomCenter’s metatype.

static let customCenter = HorizontalAlignment(CustomCenter.self)

And because you’re working in a HorizontalAlignment extension, you can use Self with a capital “S”, instead, if you want. Same meaning.

static let customCenter = Self(CustomCenter.self)

And now, finally, you can use your custom HorizontalAlignment anywhere you could use a built-in one. Like, on this VStack.

VStack(alignment: .customCenter) {

And of course nothing changes, because “Custom Center” uses the same default guide as …“Non-Custom Center”. Bust just to illustrate that you’re affecting all three HStacks, let’s shift the default alignment guide to be way off to their left sides.

    static func defaultValue(in context: ViewDimensions) -> CGFloat {
//      context[HorizontalAlignment.center]
      -300
    }

And they’ve all shifted right. …But we don’t actually want that.

context[HorizontalAlignment.center]

What we’re actually going for is aligning this Text View…

…based on its leading edge, instead of being aligned based on its parent HStack getting centered.

You actually learned what you need to do now, in the last episode. Just match up the alignments!

.alignmentGuide(.customCenter) { $0[.leading] }

…Even though customCenter is being defined on an indirect parent, with custom alignments, this works! You can see that “Learn Swift Layout”’s leading edge has been aligned with the centers of the other HStacks.

For the next Text View down, do the same thing, but with its trailing edge. First, match the customCenter guide…

        Text("Help others learn it too!")
          .alignmentGuide(.customCenter), computeValue: <#T##(ViewDimensions) -> CGFloat#>)
        ScaledImage("Hearts")

…and then, use the ViewDimensions’ trailing value.

.alignmentGuide(.customCenter) { $0[.trailing] }

And now, from top to bottom, your HStacks are using three different types of guides. The top two are based on individual nested views.

And the bottom is using the simpler, standard case, of the center of the entire HSTack.

One last note before we move on to a custom alignment guide challenge:

Although SwiftUI does have built-in AlignmentIDs —one for each of those 8 built-in alignments— you can’t use them directly. And that’s okay. Generally, just having access to Alignments that are based on those IDs, is enough.

If you find that’s true for your own AlignmentIDs, for those of you who know about restricting your types to have private access, feel free to do that. You’ll match what Apple’s done.