iosswiftswiftui

Why SwiftUI redraws the body of my custom View since nothing related to it changes?


Here is an example:

struct DemoApp: View {
    @State var viewModel = DemoAppViewModel()
    var body: some View {
        VStack {
            DemoMonthView(date: viewModel.monthDate)
            DemoDayView(date: viewModel.dayDate) // FIRST
                .onTapGesture {
                    viewModel.dayDate = viewModel.dayDate.addingTimeInterval(86000)
                }
            DemoDayView(date: viewModel.monthDate) // SECOND
                .onTapGesture {
                    viewModel.monthDate = viewModel.monthDate.addingTimeInterval(1400000)
                }
        }
    }
}

@Observable
class DemoAppViewModel {
    var dayDate: Date = Date()
    var monthDate: Date = Date()
}

struct DemoMonthView: View {
    var date: Date
    @FetchRequest private var days: FetchedResults<Day> //you need to replace Day here with any Entity that will allow to reproduce the issue
    init(date: Date) {
        self.date = date
        _days = FetchRequest(
            sortDescriptors: [SortDescriptor(\.date, order: .reverse)],
            predicate: NSPredicate(value: true)
        )
        print("DemoMonthView init is called") //should be called, but without body redraws

        // heavy calculations for given month
    }
    
    var body: some View {
        if #available(iOS 17.1, *) {
            print("DemoMonthView body is called") //should not be called❓
        }
        return VStack {
            Text(date.formatted(date: .long, time: .omitted)).font(.title.bold())
        }
    }
}

struct DemoDayView: View {
    var date: Date
    
    var body: some View {
        Text(date.formatted(date: .long, time: .omitted))
    }
}

#Preview {
    DemoApp()
}

Simply, when you tap FIRST button it should not redraw DemoMonthView, but it does. Why? I really need to avoid that by tapping every time FIRST button. SECOND button redraws DemoMonthView view correctly, what I understand. But why the FIRST?

When I comment it out days and _days association in init, then everything is fine, it DOES NOT redraws...

But that situation is just a shortened problem of my real, more complicated app. There is a fetchRequest with heavy calculations which should not be called so frequently like tap on the button, like here in example, when tapping that button does not change anything related to DemoMonthView.

If it is the reason due to the lack of my knowledge, what should I know to avoid that?

Why it matters here? Because I need to update that DemoMonthView ONLY when monthDate changes, not each time when dayDate changes.


Solution

  • In general, you can conform the view to Equatable to prevent unwanted body calls. If you only want body to be called when date changes, so you can write

    extension DemoMonthView: Equatable {
        static func ==(lhs: DemoMonthView, rhs: DemoMonthView) -> Bool {
            lhs.date == rhs.date
        }
    }
    

    It used to be the case that equatable() also needs to be used, but now it seems this is no longer needed.