Group Group Group Group Group Group Group Group Group

SwiftUI don't update scrollview content via API

Hi,
I have been working one thing for whole day, and figured out something odd.

I am fetching my data via API. then wanna show in a horizontal scroll view. Using ObservableObject and ObservedObject.

Problem: View doesn’t update when fetching has been done. I need to click the view and it refreshes with my content.

Odd part of it, If I use only List instead of scrollview then there isn’t any problem. It refreshes perfectly.

Not sure this is a bug or I am missing something.

Code below:

class HomeViewModel: ObservableObject {
    private var subscriptions = Set<AnyCancellable>()
    @Published private var companies = [Company]()
    var allCompanies: [Company] {
        return companies
    }

    func start() {

        ApolloNetwork().send(GetAllCompaniesQuery())
            .receive(on: DispatchQueue.main)
            .sink(receiveCompletion: { _ in }) { result in
                self.companies = result.companies.map {
                    Company(name: $0.name!, graphQLId: $0.id!)
                }
            }
            .store(in: &subscriptions)
    }
}

View

   struct ContentView: View {

                            @ObservedObject var viewModel: HomeViewModel

                            init(viewModel: HomeViewModel) {
                                self.viewModel = viewModel
                            }

                            var body: some View {
                                NavigationView {
        // 1 - This part not refreshing unless it is interacted with view then it refreshes.
                                    List{
                                        ScrollView(.horizontal) {
                                            HStack {
                                                ForEach(viewModel.allCompanies){ company in
                                                    NavigationLink(destination: CompanyProfileView()) {
                                                        CompanyView(company: company)
                                                    }
                                                }
                                            }
                                        }
                                    }.navigationBarTitle(Text("Home"))
            /*
                                 // 2- Comment part 1 and uncomment this part. WORKS WELL   
                                 List(viewModel.allCompanies) { company in
                                        NavigationLink(destination: CompanyProfileView()) {
                                            CompanyView(company: company)
                                        }
                                   }
            */
                                }
                            }
                        } 

ScrollView Shots
First picture, fetching done but didn’t refresh, did tap on the screen then refreshed.

List only shot fetch done refreshed immediately.

Thanks
Selcuk

@selcukyucel Do you still have issues with this?

I have a same issue. And spend a lot of time trying to solve it without success. It’s strange but error related to only .horizontal ScrollView, but not for .vertical. I also tries different ways to publish changes also without success.
If I simple remove ScrollView then everything works perfectly. But where is no any possibility to make horizontal collection.

Here is my Model

class RecentTrainingsViewModel: ObservableObject {
    var objectWillChange: AnyPublisher<[Training], Never> = Empty().eraseToAnyPublisher()
    @Published private(set) var recentTrainings = [Training]()
    
    let manager: DataManager
    
    init(factory: DataFactory = FirebaseDataFactory(), collection: DataCollection = .recentTrainings) {
        manager = DocumentManager(store: factory.makeStore(collection: collection))
        
        objectWillChange = $recentTrainings.handleEvents(
            receiveSubscription: { _ in
                self.getTrainings()
            }
        ).eraseToAnyPublisher()
    }
    
    func getTrainings() {
        manager.load { (result: Result<[Training], Error>) in
            switch result {
            case let .success(trainings):
                self.recentTrainings = trainings
                logger.debug(trainings)
                
            case let .failure(error):
                self.recentTrainings = []
                logger.warning("Error loading recent trainings  \(error.localizedDescription)")
            }
        }
    }
}

my View

struct Trainings: View {
    @ObservedObject var recentTrainingsViewModel = RecentTrainingsViewModel()

    var body: some View {
        VStack {
            SectionTitle(title: "trainings.recent")
            List {
                ScrollView(.horizontal, showsIndicators: false) {
                    HStack(alignment: .center, spacing: 15) {
                        ForEach(recentTrainingsViewModel.recentTrainings) {
                            TrainingCardVertical(
                                cardSize: CGSize(width: 150, height: 150),
                                titleStyle: .title,
                                training: $0
                            )
                        }
                    }
                    .padding(.leading, 20)
                }
                .frame(height: 260)
            }

            Spacer()
        }
    }
}

I’ve found another interesting moment. If I provide recentTrainings synchronously by creating them thru struct initialiser everything also works.

For example

struct Trainings: View {
    @ObservedObject var recentTrainingsViewModel = RecentTrainingsViewModel()

    var recentTrainings = [
        DummyTrainings.dummy,
        DummyTrainings.dummy,
        DummyTrainings.dummy
    ]

    var body: some View {
        VStack {
            SectionTitle(title: "trainings.recent")
            List {
                ScrollView(.horizontal, showsIndicators: false) {
                    HStack(alignment: .center, spacing: 15) {
                        // ----- using local dummy 
                        ForEach(recentTrainings) {
                            TrainingCardVertical(
                                cardSize: CGSize(width: 150, height: 150),
                                titleStyle: .title,
                                training: $0
                            )
                        }
                    }
                    .padding(.leading, 20)
                }
                .frame(height: 260)
            }

            Spacer()
        }
    }
}

// -- Hereis local dummy struct
struct DummyTrainings {
    static let dummy = Training(
        id: "DB3D7BD2-347A-4F94-B2BA-2FF267149C45",
        name: "Test training name",
        description: "Test training description",
        duration: 1800,
        difficulty: Difficulty(rawValue: 1) ?? .easy,
        bodyPart: "Legs",
        category: "Test training category",
        equipment: "ball, mat",
        exercises: [
            Exercise(
                id: "9C4550A3-AC90-4528-B138-2A392E2A73EB",
                name: "Test exercise name",
                description: "Test exercise description",
                bodyPart: "Test bodypart",
                imageURL: "htts://test.server/exercises/image"
            )
        ],
        imageURL: "https://firebasestorage.googleapis.com/v0/b/smfit-dev.appspot.com/o/trainingImages%2FFitness-girl-sportswear-dumbbells-gym_2880x1800.jpg?alt=media&token=7917e32a-9223-4555-9588-11fbf6ea10bc"
    )
}

@selcukyucel if you find any solution please share. Thanks!

Looks like I found a hack.
ScrollView can’t be initialised with empty array. You have to provide initial value or enclose ScrollView into if !model.value.isEmpty {} statement.

This code is working fine

var body: some View {
        VStack {
            SectionTitle(title: "trainings.recent")

            if !viewModel.recentTrainings.isEmpty { // ---- here is a hack
                ScrollView(.horizontal, showsIndicators: false) {
                    HStack(alignment: .center, spacing: 15) {
                        ForEach(viewModel.recentTrainings) {
                            TrainingCardVertical(
                                cardSize: CGSize(width: 150, height: 150),
                                titleStyle: .title,
                                training: $0
                            )
                        }
                    }
                    .padding(.leading, 20)
                }
                .frame(height: 260)
            }

            Spacer()
        }
    }