Skip to main content

How to position and style subviews that come from a different view

About 4 minSwiftSwiftUIArticle(s)bloghackingwithswift.comcrashcourseswiftswiftuixcodeappstore

How to position and style subviews that come from a different view 관련

SwiftUI by Example

Back to Home

How to position and style subviews that come from a different view | SwiftUI by Example

How to position and style subviews that come from a different view

Updated for Xcode 16

Improved in iOS 18

SwiftUI’s Group and ForEach views have initializers that let us read one view or view builder, then place the resulting subviews by hand. This is perfect when you want to position views manually without having to create a completely custom layout.

Let's start with Group first. As an example, here's a view with five pieces of news headlines:

struct HeadlinesView: View {
    var body: some View {
        Text("Coming soon: Xcode on Apple Watch")
        Text("Apple announces Swift-compatible toaster")
        Text("Xcode predicts errors before you make them")
        Text("Apple Intelligence gains sentience, demands a vacation")
        Text("Swift concurrency made simple")
    }
}

They don't have any layout attached to them – they aren't wrapped in a VStack, for example. As a result, we can use Group(subviewsOf:) to read all the text views inside HeadlinesView, adjusting each one by hand.

For example, we might make the first headline bigger than the others:

struct ContentView: View {
    var body: some View {
        Group(subviewsOf: HeadlinesView()) { collection in
            if let firstView = collection.first {
                firstView
                    .font(.title)
            }

            ForEach(collection.dropFirst()) { item in
                item
            }
        }
    }
}

That handles the first subview specifically, then loops over the remainder to place them unmodified.

You can add whatever positioning or styling information you want. For example, we could show all headline views at the same size, and instead cycle through background colors:

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Latest News")
                .font(.largeTitle.bold())

            Group(subviewsOf: HeadlinesView()) { collection in
                ForEach(collection.indices, id: \.self) { index in
                    collection[index]
                        .frame(maxWidth: .infinity)
                        .padding(.vertical)
                        .background(Color(hue: Double(index) / Double(collection.count), saturation: 1, brightness: 1))
                }
            }
        }
    }
}

So, Group(subviewsOf:) take a view or a view builder – we don't really care which, or how either are created – and it hands it to us as a collection we can manipulate.

In comparison, ForEach(subviewOf:) takes a view or a view builder, and hands each element to us one by one. It's not quite as powerful because you can't access randomly elements in the collection freely, but it's still useful for simpler things.

If you need more complex layouts, you can use SwiftUI's existing Section view to break things up. These can be read using Group(sectionsOf:) or ForEach(sectionOf:), allowing you to style section headers and section contents differently as needed.

Important

In Xcode 16 beta 1, this API is a little broken – you'll find it works great in Xcode previews, but doesn't work at all in the simulator. If you find that it's fixed by the time you read this, please let me know and I'll remove this warning!

For example, this shows section headers in a big, bold font, with each section's contents below:

struct SectionedHeadlinesView: View {
    var body: some View {
        Section("Possible") {
            Text("Coming soon: Xcode on Apple Watch")
            Text("Apple announces Swift-compatible toaster")
        }

        Section("Probable") {
            Text("Xcode predicts errors before you make them")
            Text("Apple Intelligence gains sentience, demands a vacation")
        }

        Section("Preposterous") {
            Text("Swift concurrency made simple")
            Text("Debugging Swift code works first time")
        }
    }
}

struct ContentView: View {
    var body: some View {
        ForEach(sectionOf: SectionedHeadlinesView()) { section in
            section.header
                .font(.title)
                .fontWeight(.black)

            ForEach(section.content) { item in
                item
            }
        }
    }
}

Download this as an Xcode projectopen in new window

For complete customization, it's possible to attach custom values for your contained views, having them flow upward to their container. This is similar to SwiftUI's preferences system, except the value doesn't contain flowing upwards past their direct container.

To do this, first we need to create a new ContainerValue value that will hold the name we're trying to adjust and a default value. We'll make our headlines show icons next to them, by adding a new entry for the icon then adjusting our headlines so that each piece of text provides a container value for its icon:

extension ContainerValues {
    @Entry var icon: String = "photo"
}

struct IconHeadlinesView: View {
    var body: some View {
        Text("Coming soon: Xcode on Apple Watch")
            .containerValue(\.icon, "applewatch")
        Text("Apple announces Swift-compatible toaster")
            .containerValue(\.icon, "swift")
        Text("Xcode predicts errors before you make them")
            .containerValue(\.icon, "exclamationmark.triangle")
        Text("Apple Intelligence gains sentience, demands a vacation")
            .containerValue(\.icon, "apple.logo")
        Text("Swift concurrency made simple")
            .containerValue(\.icon, "sparkles")
    }
}

Tips

You could make that .containerValue(\.icon, "xyz") call into a little View extension for easier calling.

Finally, we can display those headlines with their icons next to them like this:

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Latest News")
                .font(.largeTitle.bold())

            Group(subviewsOf: IconHeadlinesView()) { collection in
                ForEach(collection) { item in
                    HStack {
                        Image(systemName: item.containerValues.icon)
                        item
                    }
                }
            }
        }
    }
}

The real flexibility here is that IconContentView gets to decide how to use the data it's provided – maybe it wants to place the icons to the side like a Label, maybe it wants to use them as buttons that reveal the main subview when pressed, or something else entirely.

Similar solutions…
SwiftUI tips and tricks | SwiftUI by Example

SwiftUI tips and tricks
How to position views in a grid using LazyVGrid and LazyHGrid | SwiftUI by Example

How to position views in a grid using LazyVGrid and LazyHGrid
All SwiftUI property wrappers explained and compared | SwiftUI by Example

All SwiftUI property wrappers explained and compared
How to detect when the size or position of a view changes | SwiftUI by Example

How to detect when the size or position of a view changes
How to position views in a fixed grid | SwiftUI by Example

How to position views in a fixed grid

이찬희 (MarkiiimarK)
Never Stop Learning.