struct ContentView: View {
@State var store = Store(initialState: Content.State()) {
Content()
}
var body: some View {
WithViewStore(store, observe: { $0 }) { viewStore in
VStack(spacing: 0) {
ScrollViewReader(content: { proxy in
ScrollView {
LazyVStack {
Color.clear.frame(height: 0)
.id("Top")
ForEach(viewStore.items, id: \.self) { int in
Text(String(int))
.frame(height: 80)
}
}
}
.onReceive(store.publisher.shouldScrollToTop, perform: { value in
guard value else { return }
withAnimation {
proxy.scrollTo("Top")
}
})
})
Button(action: {
store.send(.didTapBottomButton)
}, label: {
Text("Button")
.frame(maxWidth: .infinity)
.frame(height: 80)
.background(Color.gray.opacity(0.6))
.foregroundStyle(Color.white)
.font(.title.bold())
})
}
.overlay(content: {
if viewStore.items.isEmpty {
ProgressView()
.scaleEffect(1.5)
}
})
.onAppear { viewStore.send(.onAppear) }
}
}
}
@Reducer
struct Content {
struct State: Equatable {
var items: [Int] = []
var shouldScrollToTop = false
}
enum Action {
case onAppear,
fetchItems,
didTapBottomButton,
didScrollToTop
}
@Dependency(\.continuousClock) var clock
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case .onAppear:
return .run { send in
try await clock.sleep(for: .seconds(3))
await send(.fetchItems)
}
case .fetchItems:
state.items = Array(0...50)
return .none
case .didTapBottomButton:
state.shouldScrollToTop = true
return .run { send in
await send(.didScrollToTop)
}
case .didScrollToTop:
state.shouldScrollToTop = false
return .none
}
}
}
}