I’m trying to understand how SwiftUI decides whether or not to re-render a view;
In this example I have a PageView
which shows horizontal scrollable pages through a DragGesture
for every week, very similar to UIPageViewController
.
The view accepts an @escaping
closure that will render each page based on the returned value, in this case I’m using a week object that represent a week of a month.
store
object is a StateObject
that contains a single @Published var state: ViewState
and it dispatches events, very similar to a Redux implementation.
The issue is that if I write the view in this way, during the scroll the body of PageContentView
is called constantly making the app glitching:
@ViewBuilder
private var pagedView: some View {
PageView(
selectedItem: store.binding(
get: .selectedWeek,
dispatch: { .onWeekChange($0) }
),
dataSource: .init(
previousItem: store.state.previousWeek,
nextItem: store.state.nextWeek
)
) { week in
let viewState = store.state.viewState(for: week)
PageContentView(
week: week,
state: viewState,
scrollTarget: store.binding(get: .scrollTarget, dispatch: { .onScrollTargetChange($0) }),
onRefresh: {
store.dispatch(event: .onWeekRefresh(week, $0))
},
didTapShift: { shift in
store.dispatch(event: .onShiftTap(shift))
},
didTapCreateShift: {
store.dispatch(event: .onCreateShiftTap)
},
didTapClearFilters: {
store.dispatch(event: .onClearFiltersTap)
}
)
.animation(.spring, value: viewState)
}
.overlay(alignment: .top) {
todayButton
}
.animation(nil, value: store.state.snackBarModels)
}
But, if I extract values at top like this, it doesn’t unnecessarily call view’s body, making everything super smooth
@ViewBuilder
private var pagedView: some View {
let scrollTarget = store.binding(get: .scrollTarget, dispatch: { .onScrollTargetChange($0) })
let store = store
PageView(
selectedItem: store.binding(
get: .selectedWeek,
dispatch: { .onWeekChange($0) }
),
dataSource: .init(
previousItem: store.state.previousWeek,
nextItem: store.state.nextWeek
)
) { week in
let viewState = store.state.viewState(for: week)
PageContentView(
week: week,
state: viewState,
scrollTarget: scrollTarget,
onRefresh: {
store.dispatch(event: .onWeekRefresh(week, $0))
},
didTapShift: { shift in
store.dispatch(event: .onShiftTap(shift))
},
didTapCreateShift: {
store.dispatch(event: .onCreateShiftTap)
},
didTapClearFilters: {
store.dispatch(event: .onClearFiltersTap)
}
)
.animation(.spring, value: viewState)
}
.overlay(alignment: .top) {
todayButton
}
.animation(nil, value: store.state.snackBarModels)
}
As you can see now scrollTarget
binding is created once and then passed to the closure, hence SwiftUI in some way decides the object didn’t change (and it makes sense actually, as in the example before I was creating a binding at each call of the closure).
The thing I can’t understand is why if I just make a local let store = store
, which is a class type and then pass down to the closure of PageView
, it doesn’t call body on each drag anymore. What am I missing?