SwiftUI basic animation - ios

Let's say I have a number and I want to animate the transition each time it changes its value. So the value disappears to the left and the new increased value appears from the right. Can this easily be made using .transition(.slide)?
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
VStack(spacing: 64) {
Text(viewModel.count.description)
.font(.largeTitle)
Button("Increment", action: viewModel.increment)
}
}
}
final class ViewModel: ObservableObject {
#Published var count = 0
func increment() {
count += 1
}
}

The .slide transition by default inserts from left and removes to right. We can use asymmetric move transition to achieve your goal.
Here is a raw demo of possible approach. Tested with Xcode 13 / iOS 15.
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
VStack(spacing: 64) {
// transition works on view insertion/remove that is
// why we conditionally switch views
if 1 == viewModel.count % 2 {
NumberView(number: viewModel.count)
} else {
NumberView(number: viewModel.count)
}
Button("Increment", action: viewModel.increment)
}
.animation(.default, value: viewModel.count)
}
}
// By default on transition view is removed to the edge from
// which it appears, that is why we need asymmetric transition
struct NumberView: View {
let number: Int
var body: some View {
Text("\(number)")
.font(.largeTitle)
.frame(maxWidth: .infinity)
.transition(.asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .leading)))
}
}

Related

SwiftUI: Init ObservableObject based on Environment

In this example, the blue rectangle should be initially visible on devices with .regular size class and hidden on devices with .compact size class.
I'm using an ObservableObject called Settings and the #Published variable isVisible to manage visibilty of the rectangle. My problem is that I don't know how I can init Settings with the correct horizontalSizeClass from my ContentView. Right now I am using .onAppear to change the value of isVisible but this triggers .onReceive. On compact devices this causes the rectangle to be visible and fading out when the view is presented instead of being invisible right away.
How can I init Settings based on Environment values like horizontalSizeClass so that isVisible is correct from the start?
struct ContentView: View {
#Environment(\.horizontalSizeClass) var horizontalSizeClass
#StateObject var settings = Settings()
#State var opacity: CGFloat = 1
var body: some View {
VStack {
Button("Toggle Visibility") {
settings.isVisible.toggle()
}
.onReceive(settings.$isVisible) { _ in
withAnimation(.linear(duration: 2.0)) {
opacity = settings.isVisible ? 1 : 0
}
}
Rectangle()
.frame(width: 100, height: 100)
.foregroundColor(.blue)
.opacity(opacity)
}
.onAppear {
settings.isVisible = horizontalSizeClass == .regular // too late
}
}
}
class Settings: ObservableObject {
#Published var isVisible: Bool = true // can't get size class here
}
The rectangle should not be visible on start:
We need just perform dependency injection (environment is known is parent so easily can be injected into child), and it is simple to do with internal view, like
struct ContentView: View {
#Environment(\.horizontalSizeClass) var horizontalSizeClass
struct MainView: View {
// later knonw injection
#EnvironmentObject var settings: Settings
var body: some View {
VStack {
Button("Toggle Visibility") {
settings.isVisible.toggle()
}
Rectangle()
.frame(width: 100, height: 100)
.foregroundColor(.blue)
.opacity(settings.isVisible ? 1 : 0) // << direct dependency !!
}
.animation(.linear(duration: 2.0), value: settings.isVisible) // << explicit animation
}
}
var body: some View {
MainView() // << internal view
.environmentObject(
Settings(isVisible: horizontalSizeClass == .regular) // << initial injecttion !!
)
}
}
Tested with Xcode 13.4 / iOS 15.5
Test code is here
withAnimation should be done on the Button action changing the state, e.g.
import SwiftUI
struct RectangleTestView: View {
#Environment(\.horizontalSizeClass) var horizontalSizeClass
#State var settings = Settings()
var body: some View {
VStack {
Button("Toggle Visibility") {
withAnimation(.linear(duration: 2.0)) {
settings.isVisible.toggle()
}
}
Rectangle()
.frame(width: 100, height: 100)
.foregroundColor(.blue)
.opacity(settings.opacity)
}
.onAppear {
settings.isVisible = horizontalSizeClass == .regular
}
}
}
struct Settings {
var isVisible: Bool = true
var opacity: CGFloat {
isVisible ? 1 : 0
}
}
FYI we don't really use onReceive anymore since they added onChange. Also its best to keep view data in structs not move it to expensive objects. "Views are very cheap, we encourage you to make them your primary encapsulation mechanism" Data Essentials in SwiftUI WWDC 2020 at 20:50.

Frame transition animation works conditionally

I am trying to make child view which contains string array moves back and forth with animation as button on parent view is toggled.
However, child view just show up and disappeared without any animation at all.
struct ParentView: View {
#State isToggle: Bool
var body: some View {
ChildView(isToggle: isToggle)
.onTabGesture {
withAnimation {
isToggle.toggle()
}
}
}
}
struct ChildView: View {
let values = [one, two, three, four]
var isToggle: Bool
var body: some View {
HStack {
ForEach(values) { value in
Text("\(value)")
.frame(width: UIScreen.main.bounds.width / 3)
}
}
.frame(width: UIScreen.main.bounds.width, alignment: isToggle ? .trailing : .leading)
}
I changed code(stored property to viewmodel) as below. and It works as expected.
class ViewModel: ObservableObject {
#Published var values = [one, two, three, four]
}
struct ParentView: View {
#State isToggle: Bool
var body: some View {
ChildView(isToggle: isToggle)
.onTabGesture {
withAnimation {
isToggle.toggle()
}
}
}
}
struct ChildView: View {
#EnvironmentObject private vm: ViewModel
var isToggle: Bool
var body: some View {
HStack {
ForEach(values) { vm.value in
Text("\(value)")
.frame(width: UIScreen.main.bounds.width / 3)
}
}
.frame(width: UIScreen.main.bounds.width, alignment: isToggle ? .trailing : .leading)
}
I thought that toggling state redraws view only when with stored property.
But, child view with viewmodel is still redrawn when toggle state changes.
Data itself not changed at all. Please kindly let me know why this is happening.
there are some minor typos in your first code. If I correct them the code runs, and the animation works:
struct ContentView: View {
#State private var isToggle: Bool = false
var body: some View {
ChildView(isToggle: isToggle)
.onTapGesture {
withAnimation {
isToggle.toggle()
}
}
}
}
struct ChildView: View {
let values = ["one", "two", "three", "four"]
var isToggle: Bool
var body: some View {
HStack {
ForEach(values, id: \.self) { value in
Text("\(value)")
.frame(width: UIScreen.main.bounds.width / 3)
}
}
.frame(width: UIScreen.main.bounds.width, alignment: isToggle ? .trailing : .leading)
}
}

SwiftUI #State variable does not change view

Using HalfASheet (https://github.com/franklynw/HalfASheet).
I have a View called ProjectsView, and in the ZStack in ProjectsView I have ProjectSorting and SortingView(both injected with the EnvironmentObject). I want the Text(🟩) in ProjectSorting to be changed, and the HStack(🟦) in SortingView to have a checkmark, both depending on the value of the sorting variable in SortingValues. Users can change the value of the sorting by pressing the Button in SortingView.
For whatever reason, the Text(🟩) in ProjectSorting does not change at all. And the HStack(🟦) in SortingView only gets the checkmark when its ancestor stack has another Text(🟨) which includes the #State variable from the environment, which I find very weird.
What should I change? Is there any way I can make this work using #EnvironmentObject? I'm a newbie and couldn't really understand other wrappers so I'd like to make this work within #State, #Binding, #EnvirionmentObject.
Thanks in advance.
SortingValues.swift
import Combine
class SortingValues: ObservableObject {
#Published var sorting = "Top Rated"
}
ProjectsView.swift
struct ProjectsView: View {
#Binding var isPresented: Bool
#State var showSortingSheet = false
var body: some View {
ZStack {
NavigationView {
VStack(spacing: 0) {
ProjectsTopView(isPresented: $isPresented)
ProjectSorting(showSortingSheet: $showSortingSheet)
.environmentObject(SortingValues())
ProjectList()
}
.navigationBarHidden(true)
}
SortingView(showSortingSheet: $showSortingSheet)
.environmentObject(SortingValues())
}
}
}
ProjectSorting.swift
import SwiftUI
struct ProjectSorting: View {
#EnvironmentObject var sortingValues: SortingValues
#Binding var showSortingSheet: Bool
#State var sortingValue = ""
var body: some View {
VStack {
HStack {
Text("Projects")
Spacer()
Button {
showSortingSheet.toggle()
} label: {
HStack(spacing: 3) {
Image("sortingArrows")
Text(sortingValue) // < 🟩 this is the Text I want to be changed
}
}
}
// Another HStack goes here
}
.onReceive(sortingValues.$sorting) { sorting in
print("This is ProjectSorting. sorting:", sorting) // < this does not print when I close the half sheet
sortingValue = sorting
}
}
}
SortingView.swift
import SwiftUI
import HalfASheet
struct SortingView: View {
#EnvironmentObject var sortingValues: SortingValues
#Binding var showSortingSheet: Bool
#State var sortingValue = ""
var body: some View {
VStack {
HalfASheet(isPresented: $showSortingSheet) {
let sorting = ["Most Recent", "Most Reviewed", "Top Rated", "Lowest Price", "Highest Price"]
VStack(alignment: .leading) {
ForEach(sorting, id: \.self) { sorting in
VStack(alignment: .leading, spacing: 14) {
Button (action: {
sortingValues.sorting = sorting
}, label: {
HStack { // 🟦
Text(sorting)
Spacer()
if sorting == sortingValue { // < this is where I add the checkmark
Image(systemName: "checkmark")
}
}
.foregroundColor(.primary)
})
if sorting != "Highest Price" {
Divider()
}
}
}
}
}
.height(.fixed(325))
// Text("Inside VStack, outside HalfASheet") // adding this Text DOES NOT make the HStack have a checkmark
Text("Inside VStack, outside HalfASheet: \(sortingValue)") // 🟨 adding this Text DOES make the HStack have a checkmark
}
.onReceive(sortingValues.$sorting) { sorting in
// the two printing lines below print correctly every time I tap the Button
print("This is SortingView. sorting:", sorting)
print("sortingValues.sorting: \(sortingValues.sorting)")
sortingValue = sorting
}
}
}
Your SortingView and ProjectSorting both access an environment object of type SortingValues, but you're passing new, separate instances to each. So the change you make in one place isn't being reflected in the other, because each view is communicating with one of two completely different objects of the same type.
If you want them to interact with the same object instance, you need to declare it at a point that's above both in the object hierarchy and make sure that that single instance is passed into both. For example:
struct ProjectsView: View {
#Binding var isPresented: Bool
#State var showSortingSheet = false
#StateObject var sortingValues = SortingValues()
var body: some View {
ZStack {
NavigationView {
VStack(spacing: 0) {
ProjectsTopView(isPresented: $isPresented)
ProjectSorting(showSortingSheet: $showSortingSheet)
.environmentObject(sortingValue)
ProjectList()
}
.navigationBarHidden(true)
}
SortingView(showSortingSheet: $showSortingSheet)
.environmentObject(sortingValues)
}
}
}
But you can go one step further. Because environment objects and values propagate down the view hierarchy automatically, you can replace two separate .environmentObject calls with one:
struct ProjectsView: View {
#Binding var isPresented: Bool
#State var showSortingSheet = false
#StateObject var sortingValues = SortingValues()
var body: some View {
ZStack {
NavigationView {
VStack(spacing: 0) {
ProjectsTopView(isPresented: $isPresented)
ProjectSorting(showSortingSheet: $showSortingSheet)
ProjectList()
}
.navigationBarHidden(true)
}
SortingView(showSortingSheet: $showSortingSheet)
}
.environmentObject(sortingValues)
}
}
There are probably better ways of dealing with reacting to changes in your observed model rather than duplicating variable values in a local state variable -- but ensuring that all your views are using the same shared environment object should get you on your way.

How to update value in previous screen in SwiftUI?

i am navigating from one screen to another and passing data using #Binding but I am alo trying to update value back to first screen when its updating in second screen.
struct FirstView: View {
#State private var valueToPass : Int = 0
var body: some View {
VStack {
Button(action: {
self.valueToPass += 1
}) {
Text("Increase value \(self.valueToPass)")
}
}
.overlay(
SecondView(valueToGet: $valueToPass)
)
}
}
struct SecondView: View {
#Binding var valueToGet: Int
var body: some View {
VStack {
Text("Show value \(valueToGet)")
.padding(.top, 50)
}
}
}
I want to change value in SecondView without dismissing overlay need updated value that in first view.
I am not sure how should i do same in reverse.
You are quite close.
Just add a buttons in the second view to increment the value
struct SecondView: View {
#Binding var valueToGet: Int
var body: some View {
VStack {
Text("Show value \(valueToGet)")
Button("Increment") {
valueToGet += 1
}
}
}
}

SwiftUI - update target view during a transition

I want to transition between two views in SwiftUI using a horizontal sliding transition. The problem is, that I also want to update the target view once the data is fetched from the network.
Down below is a minimal example of the transition. When the button on the first view is pressed, the transition and the (placeholder) background work is started. For better visibility, the transition is slowed down. In the second view, I have a ProgressView which should be replaced with the actual view (here a Text view) once the data is available.
#main
struct MyApp: App {
#StateObject private var viewModel = ViewModel()
#State private var push = false
private let transition = AnyTransition.asymmetric(insertion: .move(edge: .trailing),
removal: .move(edge: .leading))
private let transitionAnimation = Animation.easeOut(duration: 3)
var body: some Scene {
WindowGroup {
if !push {
VStack(alignment: .center) {
HStack { Spacer() }
Spacer()
Button(action: {
push.toggle()
DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 0.2) {
DispatchQueue.main.sync {
self.viewModel.someText = "Test ### Test ### Test ### Test ### Test ### Test ###"
}
}
}){
Text("Go")
}
Spacer()
}
.background(Color.green)
.transition(transition)
.animation(transitionAnimation)
} else {
SecondView()
.transition(transition)
.animation(transitionAnimation)
.environmentObject(viewModel)
}
}
}
}
final class ViewModel: NSObject, ObservableObject {
#Published var someText: String = ""
}
struct SecondView: View {
#EnvironmentObject var viewModel: ViewModel
var body: some View {
VStack(alignment: .center) {
HStack { Spacer() }
Spacer()
if(viewModel.someText.isEmpty) {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
} else {
Text(viewModel.someText)
}
Spacer()
}.background(Color.red)
}
}
The problem now is that the Text view is not included in the view transition. I would expect that it is moving along with the transition (= the red area), but instead, it just appears at the location where it would be after the transition. The following animation shows this effect.
Is it possible to achieve the animation of the Text view? To be clear: I know that in this case I could just always display the Text view because the string is empty at the beginning. As I stated earlier, this is a massively simplified version of my actual view hierarchy. I don't see a way of leaving out the if-else statement or use the hidden modifier.
Few things have to be done to make it work properly.
First, Text should exist in the views hierarchy even when the someText is empty. You can wrap it and the progress indicator into ZStack and control the text visibility with .opacity instead of the if/else statement.
Second, the animations can be applied conditionally depending on which value has changed. You should apply transitionAnimation only when the push variable is changed:
.animation(transitionAnimation, value: push)
There is a catch though: it won't work from the if/else branches on the same variable, because each of the .animation statements will exist only for one value of the push variable, and the changes in it won't be noticed. To fix that if/else should be wrapped into a Group, and animation should be applied to it.
Here is a full solution:
#main
struct MyApp: App {
#StateObject private var viewModel = ViewModel()
#State private var push = false
private let transition = AnyTransition.asymmetric(insertion: .move(edge: .trailing),
removal: .move(edge: .leading))
private let transitionAnimation = Animation.easeOut(duration: 3)
var body: some Scene {
WindowGroup {
Group {
if !push {
VStack(alignment: .center) {
HStack { Spacer() }
Spacer()
Button(action: {
push.toggle()
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
self.viewModel.someText = "Test ### Test ### Test ### Test ### Test ### Test ###"
}
}){
Text("Go")
}
Spacer()
}
.background(Color.green)
.transition(transition)
} else {
SecondView()
.transition(transition)
.environmentObject(viewModel)
}
}
.animation(transitionAnimation, value: push)
}
}
}
final class ViewModel: NSObject, ObservableObject {
#Published var someText: String = ""
}
struct SecondView: View {
#EnvironmentObject var viewModel: ViewModel
var body: some View {
VStack(alignment: .center) {
HStack { Spacer() }
Spacer()
ZStack {
if(viewModel.someText.isEmpty) {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
}
Text(viewModel.someText)
.opacity(viewModel.someText.isEmpty ? 0.0 : 1.0)
}
Spacer()
}.background(Color.red)
}
}

Resources