Files
swift-composable-architectu…/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-Animations.swift
Thomas Grapperon 85050a28a9 Fix WithViewStore issues with Views using escaping closures (#1015)
* Use a new instance of `ViewStore` in `WithViewStore`'s `body`

* Use explicit `self` for style coherence

* Remove `viewCancellable` additional reference in `newInstance()`

* Revert "Remove `viewCancellable` additional reference in `newInstance()`"

This reverts commit cb6a22ae4f.

* Change `viewCancellable` capture list

* Update `GeometryReader`'s workarounds
2022-03-11 15:23:42 -05:00

187 lines
5.5 KiB
Swift

import Combine
import ComposableArchitecture
import SwiftUI
private let readMe = """
This screen demonstrates how changes to application state can drive animations. Because the \
`Store` processes actions sent to it synchronously you can typically perform animations \
in the Composable Architecture just as you would in regular SwiftUI.
To animate the changes made to state when an action is sent to the store you can pass along an \
explicit animation, as well, or you can call `viewStore.send` in a `withAnimation` block.
To animate changes made to state through a binding, use the `.animation` method on `Binding`.
To animate asynchronous changes made to state via effects, use the `.animation` method provided \
by the CombineSchedulers library to receive asynchronous actions in an animated fashion.
Try it out by tapping or dragging anywhere on the screen to move the dot, and by flipping the \
toggle at the bottom of the screen.
"""
extension Effect where Failure == Never {
public static func keyFrames<S>(
values: [(output: Output, duration: S.SchedulerTimeType.Stride)],
scheduler: S
) -> Effect where S: Scheduler {
.concatenate(
values
.enumerated()
.map { index, animationState in
index == 0
? Effect(value: animationState.output)
: Just(animationState.output)
.delay(for: values[index - 1].duration, scheduler: scheduler)
.eraseToEffect()
}
)
}
}
struct AnimationsState: Equatable {
var alert: AlertState<AnimationsAction>? = nil
var circleCenter = CGPoint(x: 50, y: 50)
var circleColor = Color.white
var isCircleScaled = false
}
enum AnimationsAction: Equatable {
case circleScaleToggleChanged(Bool)
case dismissAlert
case rainbowButtonTapped
case resetButtonTapped
case resetConfirmationButtonTapped
case setColor(Color)
case tapped(CGPoint)
}
struct AnimationsEnvironment {
var mainQueue: AnySchedulerOf<DispatchQueue>
}
let animationsReducer = Reducer<AnimationsState, AnimationsAction, AnimationsEnvironment> {
state, action, environment in
switch action {
case let .circleScaleToggleChanged(isScaled):
state.isCircleScaled = isScaled
return .none
case .dismissAlert:
state.alert = nil
return .none
case .rainbowButtonTapped:
return .keyFrames(
values: [Color.red, .blue, .green, .orange, .pink, .purple, .yellow, .white]
.map { (output: .setColor($0), duration: 1) },
scheduler: environment.mainQueue.animation(.linear)
)
case .resetButtonTapped:
state.alert = .init(
title: .init("Reset state?"),
primaryButton: .destructive(
.init("Reset"),
action: .send(.resetConfirmationButtonTapped, animation: .default)
),
secondaryButton: .cancel(.init("Cancel"))
)
return .none
case .resetConfirmationButtonTapped:
state = .init()
return .none
case let .setColor(color):
state.circleColor = color
return .none
case let .tapped(point):
state.circleCenter = point
return .none
}
}
struct AnimationsView: View {
@Environment(\.colorScheme) var colorScheme
let store: Store<AnimationsState, AnimationsAction>
var body: some View {
WithViewStore(self.store) { viewStore in
GeometryReader { proxy in
VStack(alignment: .leading) {
ZStack(alignment: .center) {
Text(template: readMe, .body)
.padding()
Circle()
.fill(viewStore.circleColor)
.blendMode(.difference)
.frame(width: 50, height: 50)
.scaleEffect(viewStore.isCircleScaled ? 2 : 1)
.offset(
x: viewStore.circleCenter.x - proxy.size.width / 2,
y: viewStore.circleCenter.y - proxy.size.height / 2
)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(self.colorScheme == .dark ? Color.black : .white)
.simultaneousGesture(
DragGesture(minimumDistance: 0).onChanged { gesture in
viewStore.send(
.tapped(gesture.location),
animation: .interactiveSpring(response: 0.25, dampingFraction: 0.1)
)
}
)
Toggle(
"Big mode",
isOn:
viewStore
.binding(get: \.isCircleScaled, send: AnimationsAction.circleScaleToggleChanged)
.animation(.interactiveSpring(response: 0.25, dampingFraction: 0.1))
)
.padding()
Button("Rainbow") { viewStore.send(.rainbowButtonTapped, animation: .linear) }
.padding([.horizontal, .bottom])
Button("Reset") { viewStore.send(.resetButtonTapped) }
.padding([.horizontal, .bottom])
}
.alert(self.store.scope(state: \.alert), dismiss: .dismissAlert)
}
}
}
}
struct AnimationsView_Previews: PreviewProvider {
static var previews: some View {
Group {
NavigationView {
AnimationsView(
store: Store(
initialState: .init(),
reducer: animationsReducer,
environment: AnimationsEnvironment(
mainQueue: .main
)
)
)
}
NavigationView {
AnimationsView(
store: Store(
initialState: .init(),
reducer: animationsReducer,
environment: AnimationsEnvironment(
mainQueue: .main
)
)
)
}
.environment(\.colorScheme, .dark)
}
}
}