Assign value to card in scrollview before it loads

Hello everyone,

I’m working on a tennis calculator app that lets you change the theme based on the tournament you select. to change the theme, the user has 4 cards displayed within a sheet and a button to select them. To swipe between the cards, I’m using a scrollview with scrollTargetLayout and scrollTargetBehaviour(.viewaligned). The problem is that when the sheet loads the cards appear off-centre. Can anyone help me fix it?

//
//  ThemeSelector.swift
//  CalculatorApp
//
//  Created by Pablo Esquivel on 2023-11-01.
//

import SwiftUI




struct ThemeSelector: View {
//    @Binding var tournament: String
    @Binding var  isVisible: Bool
    @Binding var theme: Tournament?
    @State var id: Tournament?
    let tournaments: [Tournament] = [.roland, .wimbledon, .us, .ao]
    
//    init(isVisible: Binding<Bool>, theme: Binding<Tournament?>) {
//           _isVisible = isVisible
//           _theme = theme
//           _id = State(initialValue: theme.wrappedValue)
//       }
    
    var body: some View {
        
            
                ZStack{
                    Color("\(id?.rawValue ?? "roland")-background")
                        .ignoresSafeArea()
                    VStack{
                        HStack{
                            Spacer()
                            Button(action: {
                                isVisible = false
                            }, label: {
                                Image(systemName: "x.circle")
                                    .foregroundStyle(Color("\(id?.rawValue ?? "roland")-main"))
                                    .font(.system(size: 25))
                            })
                        }.padding(.trailing, 20.0)
                            .padding(.vertical, 10)
                        
                        
                        Text(id?.name ?? "Roland Garros")
                            .font(.system(size: 40))
                            .fontWeight(.medium)
                            .padding(.bottom, 5)
                        Text(id?.city ?? "Paris, France") //city
                            .font(.system(size: 16))
                            .padding(.bottom, 15)
                        
                        ScrollView(.horizontal, showsIndicators: false) {
                            HStack{
                                
                                ForEach (tournaments, id: \.self){ tournament in
                                    Image("\(tournament.rawValue)-court")
                                        .resizable()
                                        .aspectRatio(contentMode: .fit)
                                        .containerRelativeFrame(.horizontal)
                                        
                                        .scrollTransition (topLeading: .interactive, bottomTrailing: .interactive, axis: .horizontal) { effect, phase in effect
                                                .scaleEffect(1 - (abs(phase.value)/5))
                                        }
                                }
                                
                            }
                            .scrollTargetLayout()
                            
                        }
                        .safeAreaPadding()
                        .scrollTargetBehavior(.viewAligned)
                        .scrollPosition(id: $id, anchor: .center)
                        
                        HStack{
                            ForEach(tournaments.indices, id: \.self) { index in
                                // Create a dot for each tournament
                                let tournament = tournaments[index]

                                let dotColor = getDotColor(for: tournament)

                                Circle()
                                    .fill(dotColor)
                                    .frame(width: 5, height: 5)
                                    .padding(.horizontal, 1)
                            }
                        }
                        
                        Button(action: {
                            theme = id
                        }, label: {
                            Text(id?.rawValue == theme?.rawValue ? "Chosen" : "Choose")
                                .foregroundStyle(Color("\(id?.rawValue ?? "roland")-background"))
                        }).frame(width: 116, height: 42)
                            .background(Color("\(id?.rawValue ?? "roland")-side")).opacity(id?.rawValue == theme?.rawValue ? 0.7 : 1)
                            .cornerRadius(20)
                            .padding(.top, 20)

                    }
                }
                .foregroundStyle(Color("\(id?.rawValue ?? "roland")-display"))
                .onAppear {
                    id = theme
                    
                }
    }
    
    
    func getDotColor(for tournament: Tournament) -> Color {
        let isSelected = tournament == id
        let selectedColor = isSelected ? (tournament.isDarkBackground ? Color.black : Color.white) : Color.gray
        return isSelected ? selectedColor : selectedColor.opacity(0.5)
    }
    
    
}

#Preview {
    ThemeSelector(isVisible: Binding.constant(true), theme: Binding.constant(.roland))
    
}

@pabloe

Welcome to the community.

Can you provide a screenshot of the way it looks?

I’d like to help but testing your code is difficult without having access to other definitions used in the code. Can you provide access to a cut down version of the project for the purposes of fixing this View?

Thank you Chris!

This is a snapshot of the parent view:

struct CalculatorHome: View {
@State var tournament: Tournament? = .ao

var body: some View {
Button(action: {
                        isVisible = true
                    }, label: {
                        Image(systemName: "tennisball")
                            .foregroundStyle(Color("\(tournament ?? .ao)-main"))
                            .font(.system(size: 25))
                    })
                    .sheet(isPresented: $isVisible, content: {
                        ThemeSelector(isVisible: $isVisible, theme: $tournament)
                    })
    }
}

There is the tournament enum defined in another file:

enum Tournament: String, CaseIterable {
    case roland = "roland"
    case wimbledon = "wimbledon"
    case us = "us"
    case ao = "ao"

    var name: String {
        switch self {
        case .roland:
            return "Roland Garros"
        case .wimbledon:
            return "Wimbledon"
        case .us:
            return "US Open"
        case .ao:
            return "Australian Open"
        }
    }

    var city: String {
        switch self {
        case .roland:
            return "Paris, France"
        case .wimbledon:
            return "London, United Kingdom"
        case .us:
            return "New York City, USA"
        case .ao:
            return "Melbourne, Australia"
        }
    }
    
    var isDarkBackground: Bool {
        switch self {
        case .roland, .wimbledon:
            return true
        case .us, .ao:
            return false
        }
    }
}

Finally, the photos:
The cards appear fine at first but when the tournament is changed, something weird happens and they get displayed off centre. My theory is that the values are not getting initialized correctly and that’s what is messing it up, because when you swipe to see the other tournaments all of them get positioned correctly. (I tried using an init, which fixed the problem, but the the image of the court didn’t update accordingly.)

The first image is how the tournament card looks the first time the sheet is presented and the others are once the theme is changed and the sheet is brought up again.

Hi @pabloe

The code you’ve provided compiles but since I don’t have the court images and the associated color definitions I don’t see anything on the screen.

Can you compress and upload the Assets.xcassets folder as a zip file to either DropBox or Google Drive and create a share link and post that in a reply.

If you choose to use Google Drive then when you create the Share link, ensure that the “General Access” option is set to “Anyone with the Link” rather than “Restricted”.

Unfortunately I don’t have time to make up colours or download images in order to replicate what you have already spent a lot of time setting up.

Cheers

Thank you Chis! Really appreciate the help. Here you go:

Let me know what else I can give you to make debugging it easier and less time consuming.

Cheers

Hey @pabloe

With the code that you’ve provided, this is what I am seeing and it looks fine to me. It must be something to do with a parent view that is affecting the layout. Here is a video of how it looks in the project that I set up with your code.

By the way, that works very nicely. I really like the way each card size changes slightly as it moves in and out of the view.

@pabloe

Wait, when I go back to the ThemeSelector after choosing a theme, the initial position is now off centre. So that’s what you are referring to.

Hmmmmmm…

Maybe lookup ScrollViewReader which might be the answer that you need. I have not used them but Paul Hudson has this article that might help you come up with a solution.

So weird right? I think it has to do with how the value is initialized when it first loads, because once you start swiping it works perfectly. I’ll ask in stackOverflow, maybe someone else has had the same problem…

Hey Chris,

I just wanted to let you know that I figured it out. Thank you for trying to help me! In case you were curious, the problem was the safeAreaPadding()

Thanks for letting me know. Well done for figuring it out. Did you find something on Stackoverflow or did you just tinker with it?

I just tinkered with it!

One question, since you’re already familiar with the code…
At the top of my code I assign the value .roland to the variable tournament to set the color theme.

    @State var tournament: Tournament? = .roland

I notice that when the user closes that app and reopens it, it restarts with the tournament .roland, idependently of if another tournament was selected which makes sense, since the code is restarted. I’m wondering if you know of a way in which I can somehow change the starting value of tournament so that if the user closes the app and opens it again, it opens with the last tournament they selected?

Yes, what you could do is make use of @AppStorage which you would configure like this:

struct CalculatorHome: View {
    @State var isVisible = false
    @AppStorage("Tournament") var tournament: Tournament = .wimbledon
//    @State var tournament: Tournament? = .wimbledon

    var body: some View {
        Button(action: {
            isVisible = true
        }, label: {
            Image(systemName: "tennisball")
                .foregroundStyle(Color("\(tournament)-main"))
                .font(.system(size: 25))
        })
        .sheet(isPresented: $isVisible, content: {
            ThemeSelector(isVisible: $isVisible, theme: Binding($tournament))
        })
    }
}

AppStorage allows you to save the state of something to an area allocated to every App. Bear in mind that you should never use it for storing anything sensitive like passwords or API keys etc.

You can’t make it Optional like you had it before when it was just a State declaration and in the .sheet code you have to link it by explicitly telling SwiftUI to interpret it as a Binding hence the code:

ThemeSelector(isVisible: $isVisible, theme: Binding($tournament))

If you close the App it will remember what court you chose last.

See how you go with that.

Thanks for the help Chris! I published the app to the app store finally. You can find it under “tennis calculator”. For some reason, with the app storage modification you gave me, the tournament theme doesn’t update automatically all the time. It waits for the user to press a button for it to change.