Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI ScrollView with TabView that has another ScrollView

Tags:

swiftui

Okay, so I'm having a problem with the TabView height. This is a basic version of my code.

struct MainView: View {
  var body: some View {
    ScrollView {
        VStack {
            ProfileHStack { ... }
            BioVStack { ... }
            ButtonHStack { ... }
            IconHStack { ... }
            
            TabView {
                GridLayout()
                //Other screens...
            }
            .tabViewStyle(PageTabViewStyle())
        }
    }
  }
}

My grid layout view looks like this.

struct GridLayout: View {
  let columns = [GridItem(.flexible(), spacing: 1), GridItem(.flexible(), spacing: 1), GridItem(.flexible(), spacing: 1)]

  var body: some View {
    ScrollView  {
        LazyVGrid(columns: columns, spacing: 1) {
            ForEach(0..<30) { n in
                Image("placeholder-image")
                    .resizable()
                    .scaledToFill()
                    .frame(maxWidth: UIScreen.main.bounds.width/3, minHeight: UIScreen.main.bounds.width/3)
                    .clipped()
            }
        }
    }
  }
}

Here a screen shot what my full code looks like.

enter image description here

As you can see the tabview height is getting messed up. It only works with a fixed height. Im trying to make the tabview height as big as the content in my grid layout. How would I be able to achieve this.

Edit:

This is how my full code looks to achieve the screenshot, so you can copy and paste it in Xcode and see if someone can get it to work.

struct ProfileScreen: View {
@State var selectedButton: ProfileButton = .post
@State var tabHeight: CGFloat = 0

@State var didTapProfileTab = false
let width = UIScreen.main.bounds.width / 4
let columns = [GridItem(.flexible(), spacing: 1), GridItem(.flexible(), spacing: 1), GridItem(.flexible(), spacing: 1)]
var body: some View {
    NavigationView {
        ScrollView {
            //MARK: User Image & Followers
            HStack(spacing: 16) {
                UserProfileImageView(imageName: "gal-gadot", width: UIScreen.main.bounds.width/4, lineWidth: 8)
                Spacer()
                UserProfileInfo(numberOfPosts: "1,570", numberOfFollows: "67.1M", numberOfFollowing: "1,047")
            }
            .padding(.horizontal)
            
            //MARK: User Name, Profession & Bio
            VStack(alignment: .leading) {
                Spacer()
                    .frame(maxWidth: .infinity)
                Text("Gal Gadot")
                    .font(.headline)
                    .fontWeight(.semibold)
                
                Text("Actress")
                    .font(.subheadline)
                    .foregroundColor(.secondary)
                
                Text("🌈 meet GOODLES - Mac & Cheese that is just gooder!")
                    .lineLimit(4)
            }
            .padding(.horizontal)
            
            //MARK: Follow & Message Button
            HStack {
                Button {
                    
                } label: {
                    Text("Follow")
                        .fontWeight(.semibold)
                }
                .frame(maxWidth: .infinity, minHeight: 44)
                .background(K.AssetColor.purple)
                .tint(.white)
                .cornerRadius(8)
                
                Button {
                    
                } label: {
                    Text("Message")
                        .fontWeight(.semibold)
                }
                .frame(maxWidth: .infinity, minHeight: 44)
                .foregroundColor(.primary)
                .cornerRadius(8)
                .overlay(RoundedRectangle(cornerSize: CGSize(width: 8, height: 8)).stroke(.secondary))
            }
            .padding(.horizontal)
            .padding(.bottom)
            
            //MARK: Buttons
            HStack(spacing: 0) {
                ImageButton(systemName: "squareshape.split.3x3", assignedButton: .post, selectedButton: $selectedButton) {
                    selectedButton = .post
                }
                
                ImageButton(systemName: "film", assignedButton: .reels, selectedButton: $selectedButton) {
                    selectedButton = .reels
                }
                
                ImageButton(systemName: "video", assignedButton: .video, selectedButton: $selectedButton) {
                    selectedButton = .video
                }
                
                ImageButton(systemName: "person.2", assignedButton: .tag, selectedButton: $selectedButton) {
                    selectedButton = .tag
                }
            }
            .padding(.bottom, -6)
            
            TabView {
                GridLayout()
            }
            .tabViewStyle(PageTabViewStyle())
        }
        .navigationTitle("gal_gadot")
        .navigationBarTitleDisplayMode(.inline)
        .toolbar {
            ToolbarItem(placement: .navigationBarLeading) {
                Image(systemName: "chevron.left")
                    .font(.system(size: 24))
            }
            ToolbarItem(placement: .navigationBarTrailing) {
                Image(systemName: "ellipsis")
                    .padding(.trailing, 8)
                
            }
            
        }
    }
}
}

The ProfileButton looks like this.

enum ProfileButton {
case post,reels,video,tag
}

UserProfileImage

struct UserProfileImageView: View {
let imageName: String
let width: CGFloat
let lineWidth: CGFloat

var body: some View {
    ZStack {
        LinearGradient(colors: [K.AssetColor.purple,K.AssetColor.yellow], startPoint: .bottomLeading, endPoint: .topTrailing)
        Circle()
            .foregroundColor(.white)
            .frame(width: width + lineWidth, height: width + lineWidth)
        Image(imageName)
            .resizable()
            .scaledToFill()
            .clipShape(Circle())
            .frame(width: width, height: width)
    }
    
    .frame(width: width + lineWidth * 2, height: width + lineWidth * 2)
    .clipShape(Circle())
}
}

UserProfileInfo

struct UserProfileInfo: View {
let numberOfPosts: String
let numberOfFollows: String
let numberOfFollowing: String
var body: some View {
    HStack(spacing: 0) {
        Group {
            VStack(alignment: .center) {
                Text(numberOfPosts)
                    .fontWeight(.semibold)
                    .font(.headline)
                Text("Posts")
                    .font(.subheadline)
                    .foregroundColor(.secondary)
            }
            Spacer()
            VStack(alignment: .center) {
                Text(numberOfFollows)
                    .fontWeight(.semibold)
                    .font(.headline)
                Text("Followers")
                    .font(.subheadline)
                    .foregroundColor(.secondary)
            }
            Spacer()
            VStack(alignment: .center) {
                Text(numberOfFollowing)
                    .fontWeight(.semibold)
                    .font(.headline)
                Text("Following")
                    .font(.subheadline)
                    .foregroundColor(.secondary)
            }
        }
    }
}
}

ImageButton

struct ImageButton: View {
let systemName: String
let assignedButton: ProfileButton
@Binding var selectedButton: ProfileButton
let action: () -> Void

var body: some View {
    VStack {
        Button(action: action) {
            Image(systemName: systemName)
                .font(.title2)
                .foregroundColor(selectedButton == assignedButton ? K.AssetColor.purple : K.AssetColor.purple.opacity(0.5))
        }
        Spacer()
        
        Rectangle()
            .frame(minWidth: UIScreen.main.bounds.width / 4, maxHeight: 1)
            .foregroundColor(selectedButton == assignedButton ? K.AssetColor.purple : K.AssetColor.purple.opacity(0.5))
    }
    
}
}

Update:

So now the problem is when I have another screen with a different height.

 TabView {
    //Height 1000
    Screen1(height: $height)
    //Height 300
    Screen2(height: $height)
 }
 .tabViewStyle(PageTabViewStyle())
 .frame(height: height)

So I included this on both screens.

@Binding var height: CGFloat

.background(
        GeometryReader { geo in
            Color.clear
                .preference(
                    key: HeightPreferenceKey.self,
                    value: geo.size.height
                )
        }
        .onPreferenceChange(HeightPreferenceKey.self) { height in
            self.height = height
        }
    )

The onPreferenceChange only gets called once so when I switch from screen 2 to screen 1, screen 1 height is now the height of screen 2. Now I had to add this to both screens for it to work.

@State var screenHeight: CGFloat

Instead of setting the self.height I set the screenHeight in onPreferenceChange. Then I set the self.height to screenHeight in onAppear.

Now the only thing now is that when I just drag a little to screen2 the height of screen1 one changes to screen2. Here is a visual example..

like image 753
Luis Ramirez Avatar asked Oct 31 '25 23:10

Luis Ramirez


1 Answers

Firstly, you should remove the inner ScrollView. It's not a good idea to have a ScrollView in a ScrollView, because it doesn't make sense to be scrolling in something already scrollable.

I solved this by reading the actual height of the LazyVStack with GeometryReader, set that value to a @Binding so it is propagated up, then set the height of the TabView to this value.

Code:

struct ProfileScreen: View {
    /* ... */

    @State private var height: CGFloat = 0

    var body: some View {
        NavigationView {
            ScrollView {
                //MARK: User Image & Followers
                /* ... */

                //MARK: User Name, Profession & Bio
                /* ... */

                //MARK: Follow & Message Button
                /* ... */

                //MARK: Buttons
                /* ... */

                TabView {
                    GridLayout(height: $height)
                }
                .tabViewStyle(PageTabViewStyle())
                .frame(height: height)
            }
            /* ... */
        }
    }
}
struct GridLayout: View {
    let columns = [GridItem(.flexible(), spacing: 1), GridItem(.flexible(), spacing: 1), GridItem(.flexible(), spacing: 1)]

    @Binding var height: CGFloat

    var body: some View {
        LazyVGrid(columns: columns, spacing: 1) {
            /* ... */
        }
        .background(
            GeometryReader { geo in
                Color.clear
                    .preference(
                        key: HeightPreferenceKey.self,
                        value: geo.size.height
                    )
            }
            .onPreferenceChange(HeightPreferenceKey.self) { height in
                self.height = height
            }
        )
    }
}
struct HeightPreferenceKey: PreferenceKey {
    static let defaultValue: CGFloat = 0

    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = nextValue()
    }
}
like image 129
George Avatar answered Nov 04 '25 04:11

George