A better way to use ScrollViews/UIButtons (beginner question)

Hi guys - I am very new to Swift and I am trying to recreate a shopping app programmatically. As shopping apps go, there's a category for everything, like All, Bestsellers, Best Deal etc. And each such category has its own scrollable image gallery to go with it, whether a horizontal or a vertical one.

Building a single screen with a headline, categories (UIButtons) a ScrollView featuring an image gallery for one of the categories is straightforward enough. It's when I try to add more image galleries for other categories that things fall apart. ScrollView HStacks just start piling up on a screen and not conform to their intended UIButtons.

Clearly, another approach is needed. Do I create a Model/Data file for all image galleries and make each Button upload the relevant one on-screen? Or do I create a ReusableCell layout and make it display different image galleries when a UIButton is pressed?

I've been running around this beginner issue in circles, any pointing into the right direction will be appreciated.

It's difficult to point you in the right direction from this description alone — if you have any minimal example code demonstrating the issue you're running into that would help!

Hi Matt,

The issue I'm running into is that I can't add a separate image carousel for another item category. It's the same image carousel for all categories, and if I try to add another struct with images, it just piles underneath the first one. There must be a solution to tie an image carousel to a category, but I keep missing it.

Here's the code:

import SwiftUI

struct HomeScreen: View {
@State private var selectedIndex: Int = 0
private let categories = ["All", "Recommended", "Popular", "Just In", "Best Buy"]

    var body: some View {
    NavigationView {
        ZStack {
            Color.white
                .ignoresSafeArea()
            
            ScrollView (showsIndicators: false) {
                VStack (alignment: .leading) {
                    
                        TagLineView()
                        .padding()
                        
                      
                    ScrollView (.horizontal, showsIndicators: false) {
                        HStack {
                            ForEach(0 ..< categories.count) { i in
                                Button(action: {selectedIndex = i}) {
                                    CategoryView(isActive: selectedIndex == i, text: categories[i])
                                }
                            }
                        }
                        .padding()
                    }
                    
                    
                    ScrollView (.horizontal, showsIndicators: false) {
                        HStack (spacing: 0) {
                            ForEach(0 ..< 4) { i in
                                NavigationLink(
                                    destination: ItemScreen(),
                                    label: {
                                        ItemView(image: Image("item_\(i+1)"), size: 200)
                                    })
                                    .navigationBarHidden(true)
                                    .foregroundColor(.black)
                            }
                            .padding(.leading)
                        }
                    }
                    .padding(.bottom)
                    
                    
                    
                    
                }
            }
            
        }
    }
    
    
}

}

struct HomeScreen_Previews: PreviewProvider {
static var previews: some View {
HomeScreen()
}
}

struct TagLineView: View {
var body: some View {
Text("Headline")
.font.title3
.foregroundColor(Color.black)

}

}

struct CategoryView: View {
let isActive: Bool
let text: String
var body: some View {
VStack (alignment: .leading, spacing: 0) {
Text(text)
.font(.system(size: 18))
.fontWeight(.medium)
.foregroundColor(isActive ? Color.black : Color.black.opacity(0.5))
if (isActive) { Color.black
.frame(width: 25, height: 2)
.clipShape(Capsule())
}
}
.padding(.trailing)
}
}

struct ItemView: View {
let image: Image
let size: CGFloat

var body: some View {
    ZStack {
        image
            .resizable()
            .aspectRatio(contentMode: .fill)
          
        
        Text(“Placeholder").font(.title3).fontWeight(.bold).foregroundColor(Color.white)

        HStack (spacing: 2) {
            
            Spacer()
            Text("Placeholder text description")
                .font(.body)
                .fontWeight(.medium)
                .foregroundColor(Color.white)
                .multilineTextAlignment(.center)
              OpenButton()

} }
.frame()
.padding()
.background(Color.white)
.cornerRadius(20.0)

}

}

Looking at your code, it seems right now you essentially have:

ScrollView {
    VStack {
        //  a header

        ScrollView {
            HStack {
                //  a button for each category, where
                //  tapping a category sets that category as selected
                //  (using the @State selectedIndex property)
            }
        }

        ScrollView {
            HStack {
                //  4 navigation links to the same ItemScreen
            }
        }
    }
}

If I'm understanding you correctly and you want

//  4 navigation links to the same ItemScreen

to instead be

//  4 navigation links to ItemScreens for the current category

Data modeling suggestions aside, could you share why referencing the value of selectedIndex to build out your 4 navigation links isn't accomplishing what you want?

Data modeling suggestions aside, could you share why referencing the value of selectedIndex to build out your 4 navigation links isn't accomplishing what you want?

Hi Matt,

Thank you so much for pointing out that multiple navigation links can do the job. By referencing a selectedIndex inside a NavigationLink struct, may I ask how it works? A selectedIndex is referenced in a destination block, or its own block of code like ForEach struct?

So a simple, bruteforce approach could be something like this — referencing the product index and selected category to decide what the destination view should be. I'm using a Group here because it's a quick and easy way (for demonstration purposes) to return views based on conditional logic. (Group and View.body can do this because they're annotated with @ViewBuilder)

ForEach(0..<4) {
    productIndex in

    NavigationLink(
        destination: Group {
            if selectedCategory == 0 {
                if productIndex == 0 {
                    //  Item screen for product 0 in category 0
                } else if productIndex == 1 {
                    //  Item screen for product 1 in category 0
                }
            } else if selectedCategory == 1 {
                //  and so on
            }
        },
        label: {
            ItemView(image: Image("item_\(i + 1)"), size: 200)
        }
    )
}

But it's easy to make a mistake while writing out what products should appear when, and would require a lot of work to maintain anytime you changed a category or product. One possible way to model the category-product relationship might be like this (note that this isn't functional, but an example:)

//  Models:

struct Category {

    let name: String

    let products: Product

}

struct Product {

    let name: String

    let description: String

}

//  Somewhere in your view:

let categories = [
    Category(
        name: "Electronics",
        products: [ 
            Product(name: "Phone", description: "It's a phone! Wow!")
        ]
    ),
    Category(
        name: "Beauty",
        products: [ 
            Product(name: "Lotion", description: "It's lotion! Amazing!")
        ]
    )
]

@State var selectedCategoryIndex = 0

var body: some View {
    //  Show a button for each category

    ForEach(categories.indices) {
        categoryIndex in

        let isSelected = categoryIndex == selectedCategoryIndex

        //  Display category button, styled based on isSelected
    }

    //  Display link to each product in selected category

    let selectedCategory = categories[selectedCategoryIndex]

    ForEach(selectedCategory.products.indices) {
        productIndex in

        let product = selectedCategory.products[productIndex]

        NavigationLink(
            destination: ProductView(product: product)
            label: Text(product.name)
        )
    }
}

Hi Matt,

Thank you so much for explaining the code behind navigation links/selected index reference!

1 Like

Hello,

This forum is only about the Swift language. Apple has requested that questions/comments about their proprietary frameworks should be asked on the Apple developer forums .

It's great that a member of the community helped you regardless, but in future please direct SwiftUI-related questions to the Apple developer forums.

Thank you.