Animate SwiftUI View when .sheet is shown - swiftui

I have a SheetAnimationView from which I want to show a sheet called SheetContentView.
When the sheet appears, I want to show a transition animation of its content (start the animation when the content appears) but am unable to make it work. All 3 views in VStack should ideally end their animation at the same time.
SheetContentView:
struct SheetContentView: View {
#Binding var showSheet: Bool
var body: some View {
VStack(spacing: 8) {
Text("A great content of my new sheet")
Label("still not done", systemImage: "guitars")
Text("I'm done now")
}
.transition(.asymmetric(insertion: .scale, removal: .opacity)) // <--- I want this to work
.animation(Animation.easeInOut(duration: 2)) // <--- for 2 seconds
}
}
SheetAnimationView:
struct SheetAnimationView: View {
#State var showSheet: Bool = false
var body: some View {
Button("show my sheet with animated content (hopefully)") {
showSheet = true
}
.sheet(isPresented: $showSheet) {
SheetContentView(showSheet: $showSheet)
}
}
}

Transition works when view is appeared in view hierarchy (not on screen), so to solve this we need another container and state.
Here is a fixed variant. Tested with Xcode 12.1 / iOS 14.1
struct SheetContentView: View {
#Binding var showSheet: Bool
#State private var isShown = false
var body: some View {
VStack { // container to animate transition !!
if isShown {
VStack(spacing: 8) {
Text("A great content of my new sheet")
Label("still not done", systemImage: "guitars")
Text("I'm done now")
}
.transition(.asymmetric(insertion: .scale, removal: .opacity))
}
}
.animation(Animation.easeInOut(duration: 2))
.onAppear {
isShown = true // << activate !!
}
}
}

Related

SwiftUI: Touches not working after returning from background

Got a strange bug/error. Touches stops working at the top after closing and open the app.
To reproduce:
Click the blue bar to trigger "onTapGesture"
Swipe up to go back to springboard
Open the app
Drag down to close the modal
Click the blue bar (Will not work)
Interesting, if I remove the "Color.red.ignoresSafeArea()" It works as expected. In iOS 15, it also works as expected.
Is this a bug in SwiftUI?
Any suggestion for a workaround?
public struct TestView: View {
#State private var showModal = false
public var body: some View {
ZStack {
Color.red.ignoresSafeArea()
VStack(spacing: 0) {
Color.blue
.frame(height: 20)
.onTapGesture {
showModal = true
}
Color.white
}
}
.sheet(isPresented: $showModal, content: {
Text("HELLO")
})
}
}
I see the same happening on iPhone 14 Pro, iOS 16.2, Xcode 14.2
A workaround could be to dismiss the sheet when the app goes into the background:
struct TestView: View {
#State private var showModal = false
#Environment(\.scenePhase) var scenePhase
public var body: some View {
ZStack {
Color.red.ignoresSafeArea()
VStack(spacing: 0) {
Color.blue
.frame(height: 20)
.onTapGesture {
showModal = true
}
Color.white
}
}
.sheet(isPresented: $showModal, content: {
Text("HELLO")
})
.onChange(of: scenePhase) { scenePhase in
if scenePhase == .background {
showModal = false
}
}
}
}

SwiftUI ForEach animation overrides "local" animation

I have a view with an infinite animation. These views are added to a VStack, as follows:
struct PanningImage: View {
let systemName: String
#State private var zoomPadding: CGFloat = 0
var body: some View {
VStack {
Spacer()
Image(systemName: self.systemName)
.resizable()
.aspectRatio(contentMode: .fill)
.padding(.leading, -100 * self.zoomPadding)
.frame(maxWidth: .infinity, maxHeight: 200)
.clipped()
.padding()
.border(Color.gray)
.onAppear {
let animation = Animation.linear.speed(0.5).repeatForever()
withAnimation(animation) {
self.zoomPadding = abs(sin(zoomPadding + 10))
}
}
Spacer()
}
.padding()
}
}
struct ContentView: View {
#State private var imageNames: [String] = []
var body: some View {
NavigationView {
ScrollView {
VStack {
ForEach(self.imageNames, id: \.self) { imageName in
PanningImage(systemName: imageName)
}
// Please uncomment to see the problem
// .animation(.default)
// .transition(.move(edge: .top))
}
}
.toolbar(content: {
Button("Add") {
self.imageNames.append("photo")
}
})
}
}
}
Observe how adding a row to the VStack can be animated, by uncommenting the lines in ContentView.
The problem is that if an insertion into the list is animated, the "local" infinite animation no longer works correctly. My guess is that the ForEach animation is applied to each child view, and somehow these animations influence each other. How can I make both animations work?
The issue is using the deprecated form of .animation(). Be careful ignoring deprecation warnings. While often they are deprecated in favor of a new API that works better, etc. This is a case where the old version was and is, broken. And what you are seeing is as a result of this. The fix is simple, either use withAnimation() or .animation(_:value:) instead, just as the warning states. An example of this is:
struct ContentView: View {
#State private var imageNames: [String] = []
#State var isAnimating = false // You need another #State var
var body: some View {
NavigationView {
ScrollView {
VStack {
ForEach(self.imageNames, id: \.self) { imageName in
PanningImage(systemName: imageName)
}
// Please uncomment to see the problem
.animation(.default, value: isAnimating) // Use isAnimating
.transition(.move(edge: .top))
}
}
.toolbar(content: {
Button("Add") {
imageNames.append("photo")
isAnimating = true // change isAnimating here
}
})
}
}
}
The old form of .animation() had some very strange side effects. This was one.

NavigationLink in a Section doesn't behave like in a normalView with a simultaneous action

I have created a simple View with a NavigationLink in a Section an when the user presses on it, the value of the variable should change and should navigate the next View simultaneously. But it doesn't work like it should. If I press the "Text", the Value changes, but no navigation. If I press the "empty Space" it navigates to the next View, but the value doesn't change.
If I out the NavigationLink in a "normal" View, it does work like it should.
Is there a way to get this working without SubViews?
#State private var newValue = -1
var body: some View {
NavigationView {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("\(newValue)")
List {
Section ("Navigationlink") {
NavigationLink(destination: EmptyView()) {
Text("to Emptyview")
}.simultaneousGesture(TapGesture().onEnded{
newValue = 100
})
}
}
}
}
}
}
struct EmptyView: View {
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
}
}
}
You need a #State for Navigation to take place, this is needed as a source of truth needs to change(including navigation) in SwiftUI for any View change to happen , you change the #State for newValue so it changes, but you need to do same for NavigationView, also try NavigationStack in place of NavigationView in future , try below code , good luck
struct ContentView: View {
#State private var newValue = -1
#State private var changeView = false
var body: some View {
NavigationView {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("\(newValue)")
List {
Section ("Navigationlink") {
NavigationLink(destination: EmptyView(), isActive: $changeView) {
Text("to Emptyview")
}.simultaneousGesture(TapGesture().onEnded{
newValue = 100
changeView = true
})
}
}
}
}
}
}

Swiftui NavigationLink not executing

I'm trying to but a NavigationLink in a LazyVGrid and I'm having a bit of trouble with the navigation. I would like that when the color is clicked that I could segue to the next page. Here is some code:
Here is how the start page looks here
Here is how the image card is set up:
ZStack{
Color(colorData.image)
.cornerRadius(15)
.aspectRatio(contentMode: .fit)
.padding(8)
.matchedGeometryEffect(id: colorData.image, in: animation)
}
Here is how the LazyVgrid is set up:
ScrollView(.vertical, showsIndicators: false, content: {
VStack{
LazyVGrid(columns: Array(repeating: GridItem(.flexible(),spacing: 15), count: 2),spacing: 15){
ForEach(bags){color in
ColorView(colorData: color,animation: animation)
.onTapGesture {
withAnimation(.easeIn){
print("pressed")
selectedColor = color
}
}
}
}
.padding()
.padding(.top,10)
}
})
}
Here is how I navigate:
if selectedColor != nil {
NavigationLink(destination: DetailView()) {}
}
You can do as mentioned in #workingdog's answer, or slightly modify the code like so:
struct ContentView: View {
#State var isActive: Bool = false
#State var selectedColor: Color?
var body: some View {
NavigationView {
NavigationLink(destination: DetailsVieww(selectedColor: selectedColor), isActive: $isActive) {
EmptyView()
}
VStack {
ColorView(colorData: color, animation: animation)
.onTapGesture {
selectedColor = color
isActive = true
}
}
}
}
}
typically with navigation you would do something like this:
struct ContentView: View {
#State var selection: Int?
#State var selectedColor: Color?
var body: some View {
NavigationView { // <--- required somewhere in the hierarchy of views
// ....
NavigationLink(destination: DetailsVieww(selectedColor: selectedColor),
tag: 1,
selection: $selection) {
ColorView(colorData: color, animation: animation)
.onTapGesture {
withAnimation(.easeIn) {
selectedColor = color
selection = 1 // <--- will trigger just the tag=1 NavigationLink
}
}
}
// ....
}
}
}
struct DetailsVieww: View {
#State var selectedColor: Color?
var body: some View {
Text("selected color view \(selectedColor?.description ?? "")")
}
}

Popover displaying inaccurate information inside ForEach

I'm having a problem where I have a ForEach loop inside a NavigationView. When I click the Edit button, and then click the pencil image at the right hand side on each row, I want it to display the text variable we are using from the ForEach loop. But when I click the pencil image for the text other than test123, it still displays the text test123 and I have absolutely no idea why.
Here's a video. Why is this happening?
import SwiftUI
struct TestPopOver: View {
private var stringObjects = ["test123", "helloworld", "reddit"]
#State private var editMode: EditMode = .inactive
#State private var showThemeEditor = false
#ViewBuilder
var body: some View {
NavigationView {
List {
ForEach(self.stringObjects, id: \.self) { text in
NavigationLink( destination: HStack{Text("Test!")}) {
HStack {
Text(text)
Spacer()
if self.editMode.isEditing {
Image(systemName: "pencil.circle").imageScale(.large)
.onTapGesture {
if self.editMode.isEditing {
self.showThemeEditor = true
}
}
}
}
}
.popover(isPresented: $showThemeEditor) {
CustomPopOver(isShowing: $showThemeEditor, text: text)
}
}
}
.navigationBarTitle("Reproduce Editing Bug!")
.navigationBarItems(leading: EditButton())
.environment(\.editMode, $editMode)
}
}
}
struct CustomPopOver: View {
#Binding var isShowing: Bool
var text: String
var body: some View {
VStack(spacing: 0) {
HStack() {
Spacer()
Button("Cancel") {
self.isShowing = false
}.padding()
}
Divider()
List {
Section {
Text(text)
}
}.listStyle(GroupedListStyle())
}
}
}
This is a very common issue (especially since iOS 14) that gets run into a lot with sheet but affects popover as well.
You can avoid it by using popover(item:) rather than isPresented. In this scenario, it'll actually use the latest values, not just the one that was present when then view first renders or when it is first set.
struct EditItem : Identifiable { //this will tell it what sheet to present
var id = UUID()
var str : String
}
struct ContentView: View {
private var stringObjects = ["test123", "helloworld", "reddit"]
#State private var editMode: EditMode = .inactive
#State private var editItem : EditItem? //the currently presented sheet -- nil if no sheet is presented
#ViewBuilder
var body: some View {
NavigationView {
List {
ForEach(self.stringObjects, id: \.self) { text in
NavigationLink( destination: HStack{Text("Test!")}) {
HStack {
Text(text)
Spacer()
if self.editMode.isEditing {
Image(systemName: "pencil.circle").imageScale(.large)
.onTapGesture {
if self.editMode.isEditing {
self.editItem = EditItem(str: text) //set the current item
}
}
}
}
}
.popover(item: $editItem) { item in //item is now a reference to the current item being presented
CustomPopOver(text: item.str)
}
}
}
.navigationBarTitle("Reproduce Editing Bug!")
.navigationBarItems(leading: EditButton())
.environment(\.editMode, $editMode)
}.navigationViewStyle(StackNavigationViewStyle())
}
}
struct CustomPopOver: View {
#Environment(\.presentationMode) private var presentationMode: Binding<PresentationMode>
var text: String
var body: some View {
VStack(spacing: 0) {
HStack() {
Spacer()
Button("Cancel") {
self.presentationMode.wrappedValue.dismiss()
}.padding()
}
Divider()
List {
Section {
Text(text)
}
}.listStyle(GroupedListStyle())
}
}
}
I also opted to use the presentationMode environment property to dismiss the popover, but you could pass the editItem binding and set it to nil as well (#Binding var editItem : EditItem? and editItem = nil). The former is just a little more idiomatic.