0

Is it possible to make paged TabView that wraps its content? I don't know the height of the content (it is an Image resized to fit the width of the screen) so I can't use frame modifier.

My code looks like this:

ZStack(alignment: .topTrailing) {
    TabView {
        ForEach(data) { entry in
            VStack {
                entry.image
                    .resizable()
                    .scaledToFit()
                    .frame(maxWidth: .infinity)

                Text(entry.description)
            }
        }
    }
    .tabViewStyle(.page(indexDisplayMode: .never))

    Color.red
        .frame(width: 20, height: 20)
}

The problem is that the TabView is as big as the screen and PageIndicator is placed in the top right corner of the screen instead of top right corner of the image. Tanks for any suggestions.

EDIT:

I've added code that is reproducible. PageIndicator was replaced by red rectangle.

struct Test: View {
    struct Entry: Identifiable {
        let image: Image
        let description: String

        var id: String { description }
    }

    let data = [
        Entry(image: Image(systemName: "scribble"), description: "image 1"),
        Entry(image: Image(systemName: "trash"), description: "image 2")
    ]

    var body: some View {
        ZStack(alignment: .topTrailing) {
            TabView {
                ForEach(data) { entry in
                    VStack {
                        entry.image
                            .resizable()
                            .scaledToFit()
                            .frame(maxWidth: .infinity)

                        Text(entry.description)
                    }
                }
            }
            .tabViewStyle(.page(indexDisplayMode: .never))

            Color.red
                .frame(width: 20, height: 20)
        }
    }
}
Ella Gogo
  • 1,051
  • 1
  • 11
  • 17

1 Answers1

0

Your PageIndicator is not placed on the image, because you didn't place it there. You are placing it in a layer on top of a VStack that happens to contain text and an image that can be shorter than the screen. If you want the PageIndicator on the image, then you need to do that specifically. You didn't provide a Minimal, Reproducible Example, but does something like this work:

TabView {
    ForEach(data) { entry in
        VStack {
            entry.image
                .resizable()
                .scaledToFit()
                .overlay(
                    PageIndicator()
                )
                .frame(maxWidth: .infinity)
                .overlay(
                    PageIndicator()
                )

            Text(entry.description)
        }
    }
}
.tabViewStyle(.page(indexDisplayMode: .never))

The .overlay() needs to go before the .frame() so it stays on the image, instead of overlaying the .frame().

Edit:

Based off of the MRE and the comment, here is alternate solution that aligns the PageIndicator to the top of the image, but does not scroll with the image. Please note that this is not a perfect MRE as the image heights are different, but this solution actually accounts for that as well. Lastly, I added a yellow background on the image to show that things are aligned properly.

struct Test: View {

    struct Entry: Identifiable {
        let image: Image
        let description: String
        
        var id: String { description }
    }
    
    let data = [
        Entry(image: Image(systemName: "scribble"), description: "image 1"),
        Entry(image: Image(systemName: "trash"), description: "image 2")
    ]
    
    @State private var imageTop: CGFloat = 50
    
    var body: some View {
        ZStack(alignment: .topTrailing) {
            TabView {
                ForEach(data) { entry in
                    VStack {
                        entry.image
                            .resizable()
                            .scaledToFit()
                            .frame(maxWidth: .infinity)
                            .background(Color.yellow)
                            .background(
                                GeometryReader { imageProxy in
                                    Color.clear.preference(
                                        key: ImageTopInGlobal.self,
                                        // This returns the top of the image relative to the TabView()
                                        value: imageProxy.frame(in: .named("TabView")).minY)
                                }
                            )

                        Text(entry.description)
                    }
                }
            }
            // This gives a reference to another container for comparison
            .coordinateSpace(name: "TabView")
            .tabViewStyle(.page(indexDisplayMode: .never))
            VStack {
                Spacer()
                    .frame(height: imageTop)
                Color.red
                    .frame(width: 20, height: 20)
            }
        }
        // You either have to ignore the safe area or account for it with regard to the TabView(). This was simpler.
        .edgesIgnoringSafeArea(.top)
        .onPreferenceChange(ImageTopInGlobal.self) {
            imageTop = $0
        }
    }
}

private extension Test {
    struct ImageTopInGlobal: PreferenceKey {
        static let defaultValue: CGFloat = 0
        
        static func reduce(value: inout CGFloat,
                           nextValue: () -> CGFloat) {
            value = nextValue()
        }
    }
}

Final edit:

In response to the last comment, my answer is a qualified no. I don't think there is any way to make a TabView hug its contents. (I take the original question using the term wrap to mean hug, as the TabView always "wraps" its contents.) If you try to use a preference key, the TabView collapses. There would have to be a minHeight or height set to prevent this which defeats the purpose of the hugging attempt.

Yrb
  • 8,103
  • 2
  • 14
  • 44
  • Hi, no this does not work. I need the PageIndicator to be above the TabView, it can't scroll with the content. I will provide reproducible example. – Ella Gogo Nov 20 '21 at 14:52
  • I've added the example. If you run it, you can see that the red rectangle does not move when user swipes right or left - that's what I need. My problem is that I need to move the rectangle down so that it aligns with the top right corners of the images (images all have the same aspect ratio). And that's the thing I can't achieve. – Ella Gogo Nov 20 '21 at 15:13
  • Added to the answer. – Yrb Nov 20 '21 at 17:08
  • Perfect :) I suppose that the answer to my original question - Is it possible to make paged TabView that wraps its content? - is no. Can you add this to your answer so I can accept this? – Ella Gogo Nov 21 '21 at 09:17