1

I'm working on a SwiftUI project where I have a list of referrals displayed using List and ForEach. The list is part of the MyReferralsList view, and I want to optimize the UI rendering to avoid redrawing the entire view when the data property changes.

I have already tried implementing the Equatable protocol for all the relevant data types and also for the view itself. I also tried using the id parameter with the ForEach, but it didn't work as expected.

The ScrollingViewForBtns is fetching the buttons title from an API but i do not call that API from here. I am just changing the myReferralsListViewModel.data from this view. The data array is a published object and the data is only being used by the ReferralListSubView.

Here's what I've tried so far:

Conforming to Equatable protocol for all the relevant data types and view. Using the id parameter with ForEach to uniquely identify each element and also used id for the subview that contains the ForEach loop. Despite these efforts, every time I fetch new data and update the myReferralsListViewModel.data, the entire MyReferralsList view is redrawn, including the ScrollingViewForBtns and referralsListView.

Here's the simplified code for the MyReferralsList view:

struct MyReferralsList: View, Equatable {
    
    
    @ObservedObject var myReferralsListViewModel: MyReferralsListVM
    @State private var selectedStatus: Int = 0
    @State private var dataChanged: Bool = false
    private var organizationId: Int = 0
    
    init(organizationId: Int, statusId: Int? = 0){
        self.organizationId = organizationId
        self.myReferralsListViewModel = MyReferralsListVM(referralsService:ReferralsService(networking: Networking.shared), organizationId: organizationId)
    }
    
    
    
    var body: some View {
        
        ZStack(alignment: .top) {
            Color.white
            
            VStack(alignment: .leading, spacing: 16.0) {
                /// Buttons to filter the referrals by stauts
                Group{
                    ScrollingViewForBtns(selectedButtonIndex: $selectedStatus,statusBtnTapped: { id in
                        
                        /**
                         - if selected index is 0 means "All" referrals are selected so we fetch all referrals passing only the organizationID
                         - Else we pass the id for the selected status and fetch the referrals for the selected Status
                         */
                        if selectedStatus == 0 {
                            
                            myReferralsListViewModel.fetchData(id: organizationId)
                        }else{
                            myReferralsListViewModel.fetchReferralForStatus(statusId: id)
                        }
                    })
                }
                ReferralListSubView(data: myReferralsListViewModel.data).id(dataChanged)
                Spacer()
            }
            .padding(.vertical, 14)
            .padding(.horizontal, 15)
        }
        .foregroundColor(Color("textGray"))
        .onChange(of: myReferralsListViewModel.data) { _ in
            dataChanged.toggle()
        }
        
        
    }
    
    static func == (lhs: MyReferralsList, rhs: MyReferralsList) -> Bool {
        
        print("Comparing MyReferralsList instances...")
        print("lhs.selectedStatus: \(lhs.selectedStatus), rhs.selectedStatus: \(rhs.selectedStatus)")
        print("lhs.organizationId: \(lhs.organizationId), rhs.organizationId: \(rhs.organizationId)")
        
        // Compare data property
        print("lhs.myReferralsListViewModel.data: \(lhs.myReferralsListViewModel.data)")
        print("rhs.myReferralsListViewModel.data: \(rhs.myReferralsListViewModel.data)")
        let dataEqual = lhs.myReferralsListViewModel.data == rhs.myReferralsListViewModel.data
        print("Data is equal: \(dataEqual)")
        
        return lhs.selectedStatus == rhs.selectedStatus &&
        lhs.organizationId == rhs.organizationId &&
        lhs.myReferralsListViewModel.data == rhs.myReferralsListViewModel.data
    }
 }

code for ReferralListSubView:

struct ReferralListSubView: View {
    let data: [Referral]
    var body: some View {
     
        
        List{
            ForEach(data, id:\.id) { referral in
                
            NavigationLink(destination: ReferralDetailView(referral: referral)) {
                   ReferralsCells(referral: referral)
                
                    
                    
            }
                .frame(height: 81.0)
                .padding(.horizontal)
                .background(Color.white)
                .cornerRadius(6)
                .shadow(color: Color.black.opacity(0.2), radius: 2, x: 0, y: 2)
                .listRowBackground(Color.clear)
            }
            
               
        }
       
        .listRowSeparator(.hidden)
        .listStyle(PlainListStyle()) // Set the list style to PlainListStyle()
        .background(Color.white) //
        
        
        
        
    }
}

code for ScrollingViewForBtns:

struct ScrollingViewForBtns: View {
    @ObservedObject var referralsStatusVM = ReferralsStatusViewModel(referralService: ReferralsService(networking: Networking.shared))
    @Binding  var selectedButtonIndex: Int
    
  
    
    var statusBtnTapped : (Int) -> Void
    var body: some View {
        ScrollView(.horizontal, showsIndicators: false){
            HStack(spacing: 16){
                
                ForEach(referralsStatusVM.buttons.indices, id:\.self) { index in
                    StatusButton(btnText: referralsStatusVM.buttons[index].name, btnAction: {
                        
                        selectedButtonIndex = index // This will help to change selected button color
                        statusBtnTapped(referralsStatusVM.buttons[index].id) // Passing the selected status id to the parent view
                        // this handles the API from parent view
                    }, isSelected: Binding(
                        get: { selectedButtonIndex == index },
                        set: { _ in }))
                }
            }
            .padding(.vertical,10)
            .padding(.horizontal,2)
         
        }
    }
}

I just need to update the ReferralListSubView when the data array changes.

Could someone please help me identify what I might be missing or suggest alternative approaches to optimize the rendering and prevent the entire view from being redrawn every time data changes?

Thank you in advance for any assistance!

Umair Khan
  • 11
  • 2
  • You may need to factor-out parts of the big view into separate ```View``` structs and possibly fragment your model too. If you only want parts of the view to be rebuilt when the the data changes, then only these parts should be observing changes to the data. Unfortunately, there are too many missing parts in the code you have provided to be able to provide a working example (missing parts include ```ReferralsStatusViewModel```, ```ReferralsService```, ```Networking```, ```StatusButton```, ```Referral```, ```ReferralDetailView```, ```ReferralsCells```, ```MyReferralsListVM```). – Benzy Neez Jul 20 '23 at 12:01
  • You View does not need `Equatable` it actually works against SwiftUI's underlying updating system. Also anytime an `ObservableObject` triggers a reader it will redraw everything, this is by design, it is very inefficient but it is just how it works. Watch "Demystify SwiftUI". You are also misusing `ObservedObject` use `StateObject` instead, this might help you improve performance a little. `ObservedObject` does not have the ability to manage lifecycle, – lorem ipsum Jul 20 '23 at 13:05
  • @BenzyNeez I gave all the code that could impact this its just if I do not update the data array everuthing including the Status buttons will work fine. So I assume the issue is some where either in this class or with the observed object. – Umair Khan Jul 20 '23 at 13:16
  • @loremipsum I followed this answer https://stackoverflow.com/questions/60482098/swiftui-how-to-prevent-view-to-reload-whole-body thats why I used Equatable – Umair Khan Jul 20 '23 at 13:18
  • That post is from a very very long time ago, it isn't applicable. Watch Demystify SwiftUI it is counter productive to override Equatable when dealing with SwiftUI. SwiftUI was a little baby back then and very inefficient, there have been leaps since then – lorem ipsum Jul 20 '23 at 13:19
  • Please trim your code to make it easier to find your problem. Follow these guidelines to create a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example). – Community Jul 20 '23 at 13:20
  • @loremipsum just finished watching it. It was a great knowledge still trying hard for to apply some changes that can help me solve this issue. – Umair Khan Jul 20 '23 at 15:50

1 Answers1

0

So here is the fix it might help some one in the future.

In ScrollingViewForBtns I was using @ObservedObject and when ever I tap on a button from ScrollingViewForBtns the view is recreated because it changes the color for the selected button and because of that when ever I was selecting a new button the view got recreated and @ObservedObject was trigerring.

To solve this I used the @StateObject because @StateObject will ensure that the view model is only created once during the lifetime of the view.

Umair Khan
  • 11
  • 2