9

Given an HStack like the following:

        HStack{
            Text("View1")

            Text("Centre")

            Text("View2")

            Text("View3")
        }

How can I force the 'Centre' view to be in the centre?

Confused Vorlon
  • 9,659
  • 3
  • 46
  • 49

3 Answers3

18

Here is possible simple approach. Tested with Xcode 11.4 / iOS 13.4

demo

struct DemoHStackOneInCenter: View {
    var body: some View {
        HStack{
            Spacer().overlay(Text("View1"))

            Text("Centre")

            Spacer().overlay(
                HStack {
                    Text("View2")
                    Text("View3")
                }
            )
        }
    }
}

The solution with additional alignments for left/right side views was provided in Position view relative to a another centered view

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • 1
    that's an interesting approach. It does rather mess up the spacing between the views, but you can fix that with more spacers in the left/right HStacks and explicit spacing. – Confused Vorlon Jun 15 '20 at 14:42
8

the answer takes a handful of steps

  1. wrap the HStack in a VStack. The VStack gets to control the horizontal alignment of it's children
  2. Apply a custom alignment guide to the VStack
  3. Create a subview of the VStack which takes the full width. Pin the custom alignment guide to the centre of this view. (This pins the alignment guide to the centre of the VStack)
  4. align the centre of the 'Centre' view to the alignment guide

For the view which has to fill the VStack, I use a Geometry Reader. This automatically expands to take the size of the parent without otherwise disturbing the layout.

import SwiftUI


//Custom Alignment Guide
extension HorizontalAlignment {
    enum SubCenter: AlignmentID {
        static func defaultValue(in d: ViewDimensions) -> CGFloat {
            d[HorizontalAlignment.center]
        }
    }
    
    static let subCentre = HorizontalAlignment(SubCenter.self)
}

struct CentreSubviewOfHStack: View {
    var body: some View {
        //VStack Alignment set to the custom alignment
        VStack(alignment: .subCentre) {
            HStack{
                Text("View1")
                
                //Centre view aligned
                Text("Centre")
                .alignmentGuide(.subCentre) { d in d.width/2 }
                
                Text("View2")
                
                Text("View3")
            }
            
            //Geometry reader automatically fills the parent
            //this is aligned with the custom guide
            GeometryReader { geometry in
                EmptyView()
            }
            .alignmentGuide(.subCentre) { d in d.width/2 }
        }
    }
}

struct CentreSubviewOfHStack_Previews: PreviewProvider {
    static var previews: some View {
        CentreSubviewOfHStack()
            .previewLayout(CGSize.init(x: 250, y: 100))
    }
}

Edit: Note - this answer assumes that you can set a fixed height and width of the containing VStack. That stops the GeometryReader from 'pushing' too far out

In a different situation, I replaced the GeometryReader with a rectangle:

            //rectangle fills the width, then provides a centre for things to align to
            Rectangle()
                .frame(height:0)
                .frame(idealWidth:.infinity)
                .alignmentGuide(.colonCentre) { d in d.width/2 }

Note - this will still expand to maximum width unless constrained!

Centre is in the centre

Confused Vorlon
  • 9,659
  • 3
  • 46
  • 49
  • This didn't work for me, although I may have done something wrong. The empty view ended up translating off the screen to maintain its alignment with the centre text view. Once I wrapped the Centre text view in a ZStack I achieved the desired behaviour. Using an EmptyView to have something to align with was clever imo! – pakobongbong Dec 27 '21 at 22:30
2

Asperis answer is already pretty interesting and inspired me for following approach:

Instead of using Spacers with overlays, you could use containers left and right next to the to-be-centered element with their width set to .infinity to stretch them out just like Spacers would.

HStack {
    // Fills the left side
    VStack {
      Rectangle()
        .foregroundColor(Color.red)
        .frame(width: 120, height: 200)
    }.frame(maxWidth: .infinity)
    
    // Centered
    Rectangle()
      .foregroundColor(Color.red)
      .frame(width: 50, height: 150)
    
    // Fills the right side
    VStack {
      HStack {
        Rectangle()
          .foregroundColor(Color.red)
          .frame(width: 25, height: 100)
        Rectangle()
          .foregroundColor(Color.red)
          .frame(width: 25, height: 100)
      }
    }.frame(maxWidth: .infinity)
  }.border(Color.green, width: 3)

I've put it in a ZStack to overlay a centered Text for demonstration:

enter image description here

Using containers has the advantage, that the height would also translates to the parent to size it up if the left/right section is higher than the centered one (demonstrated in screenshot).

Dominik Seemayr
  • 830
  • 2
  • 12
  • 30
  • 1
    Asperi's method left the overlay with 0 height for me (which pushed the content up to sit half invisible above the frame), and I was struggling to fix that without breaking the spacing completely. This ended up being a much easier solution. It also is nice that it isn't really going against SwiftUI or taking advantage of bugs or unreliable implementation details, you're just specifying elements with size constraints within an HStack. – Austin Aug 01 '22 at 05:31