Search code examples
swiftuivstackhstack

How to dynamically change from HStack to VStack when using LabeledContent?


I am setting up the frame width and wondering if it is possible to have this display as a VStack

value:

title

instead of the default of a HStack when someone increases to a larger font size.

value: title

struct TextRowView: View {
    let title: String
    let value: String
    let width: CGFloat
    
    var body: some View {
        LabeledContent {
            Text(value)
                .frame(width: width, alignment: .leading)
        } label: {
            Text(title)
                .bold()
        }
    }
}

Solution

  • I'd write a custom LabeledContentStyle.

    If you just want to use a HStack when a VStack "doesn't fit", you can use ViewThatFits:

    struct DynamicStackStyle: LabeledContentStyle {
        func makeBody(configuration: Configuration) -> some View {
            ViewThatFits(in: .horizontal) {
                HStack {
                    configuration.label
                    configuration.content
                }
                VStack {
                    configuration.label
                    configuration.content
                }
            }
        }
    }
    
    extension LabeledContentStyle where Self == DynamicStackStyle {
        static var dynamicStack: DynamicStackStyle { .init() }
    }
    
    LabeledContent {
        ...
    } label: {
        ...
    }
    .labeledContentStyle(.dynamicStack)
    

    If you want to specifically check for dynamic type size (I'm assuming that's what you mean by "increases to a larger font size"), you can do that too.

    struct DynamicStackStyle: LabeledContentStyle {
        @Environment(\.dynamicTypeSize) var size
        
        func makeBody(configuration: Configuration) -> some View {
            if size < .xLarge {
                HStack {
                    configuration.label
                    configuration.content
                }
            } else {
                VStack {
                    configuration.label
                    configuration.content
                }
            }
            
        }
    }
    

    Note that the default labeled content style appears differently in list rows and other things. You cannot recreate this behaviour with your own labeled content style. If the default behaviour is desirable, you should not use a custom labeled content style. Just wrap the LabeledContent directly with a ViewThatFits or if size < .xLarge { ... } else { ... }.

    ViewThatFits(in: .horizontal) {
        LabeledContent {
            Text(value)
                .frame(width: width, alignment: .leading)
        } label: {
            Text(title)
                .bold()
        }
        VStack {
            Text(value)
                .frame(width: width, alignment: .leading)                Text(title)
                .bold()
        }
    }