SwiftUI: NavigationLink doesn't show animation if setting after background task - swiftui

I am trying to use the new async/await concurrency code with a SwiftUI view to do some basic asynchronous data loading after a button click, to show a spinner view, and on completion, transition to a new page. I use an enum to mark the state of the view, and use that with NavigationLink to move forward to a new view. This works, and shows the 'loading' text view for a second before 'pushing' to the next view, but there is no animation when the new view is pushed. This is the code I am using:
import SwiftUI
enum ContinueActionState: Int {
case ready = 0
case showProgressView = 1
case pushToNextPage = 2
case readingError = 3
case error = 4
}
struct ContentView: View {
#State var actionState: ContinueActionState? = .ready
var body: some View {
NavigationView {
VStack {
Text("Test the Button!")
if (actionState == .showProgressView) {
ProgressView("Loading Data")
} else if (actionState == .error || actionState == .readingError) {
Text("Error in loading something")
}
else {
NavigationLink(destination: DetailPageView(), tag: .pushToNextPage, selection: $actionState) {
Button(action: {
Task {
print("on buttonClick isMain =\(Thread.isMainThread)")
self.actionState = .showProgressView
await self.startProcessingData()
//self.actionState = .pushToNextPage // animation works if only this is used
}
}) {
Text("Continue")
}
.tint(.blue)
.buttonStyle(.borderedProminent)
.buttonBorderShape(.roundedRectangle(radius: 5))
.controlSize(.large)
}
}
}.navigationTitle("Test Async")
}
}
func startProcessingData() async {
Task.detached {
print("startProcessingData isMain =\(Thread.isMainThread)")
try await Task.sleep(nanoseconds: 1_000_000_000)
//await MainActor.run {
self.actionState = .pushToNextPage
//}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct DetailPageView: View {
var body: some View {
Text("Detail page")
}
}
IF I just forego the async call and just set to .pushToNextPage state immediately on button click, the animation works fine.
Is there any way to get this to work with a smooth animation, after processing stuff in a background queue task is complete?

if you move the NavigationLink out of the if statements it works fine for me:
NavigationView {
VStack {
Text("Test the Button!")
NavigationLink(destination: DetailPageView(), tag: .pushToNextPage, selection: $actionState) { Text("") } // here
if (actionState == .showProgressView) {
ProgressView("Loading Data")
} else if (actionState == .error || actionState == .readingError) {
Text("Error in loading something")
}
else {
Button(action: {
Task {
print("on buttonClick isMain =\(Thread.isMainThread)")
self.actionState = .showProgressView
await self.startProcessingData()
}
}) {
Text("Continue")
}
.tint(.blue)
.buttonStyle(.borderedProminent)
.buttonBorderShape(.roundedRectangle(radius: 5))
.controlSize(.large)
}
}.navigationTitle("Test Async")
}

Related

Undesired interplay between tapable, movable items and scrolling in SwiftUI List

I'm working on a SwiftUI list that shows tapable and long-pressable full-width items, which are movable, and allow for detail navigation.
I've noticed that .onLongPressGesture isn't detected when the list allows for moving of items, because the List switches to drag-moving the long-pressed item instead.
import SwiftUI
import PlaygroundSupport
struct ContentView: View {
let data = Array(0..<20)
var body: some View {
NavigationStack {
List {
ForEach(data, id:\.self) { item in
NavigationLink(destination: EmptyView(), label: {
Rectangle().fill(.mint)
.onTapGesture { print("tapped", item) }
.onLongPressGesture{ print("longPressed", item)}
})
}.onMove(perform: moveItems)
}
}
}
func moveItems(from source: IndexSet, to destination: Int) { }
}
PlaygroundPage.current.setLiveView(ContentView())
I've experimented further and found that using simultaneous gesture via simultaneousGesture() fixes the missing notification on long presses, but instead removes scrolling ability from the List.
import SwiftUI
import PlaygroundSupport
struct ContentViewSimultaneous: View {
let data = Array(0..<20)
var body: some View {
NavigationStack {
List {
ForEach(data, id:\.self) { item in
NavigationLink(destination: EmptyView(), label: {
Rectangle().fill(.blue)
.simultaneousGesture(TapGesture().onEnded { print("tapped", item) })
.simultaneousGesture(LongPressGesture().onEnded { _ in
print("longPressed", item) })
})
}.onMove(perform: moveItems)
}
}
}
func moveItems(from source: IndexSet, to destination: Int) { }
}
PlaygroundPage.current.setLiveView(ContentViewSimultaneous())
I'm now looking for a way to make this work and would appreciate any insights! I'm new to SwiftUI and might miss something important.
I think I was able to get this working as you describe. It works with no issues on iOS 15, but there seems to be an animation bug in iOS 16 that causes the rearrange icon not to animate in for some/all List rows. Once you drag an item in edit mode, the icon will display.
struct ContentView: View {
#State private var editMode: EditMode = .inactive
#State var disableMove: Bool = true
var body: some View {
let data = Array(0..<20)
NavigationView {
List {
ForEach(data, id:\.self) { item in
NavigationLink(destination: EmptyView(), label: {
Rectangle().fill(.mint)
.onTapGesture { print("tapped", item) }
.onLongPressGesture{ print("longPressed", item)}
})
}
.onMove(perform: disableMove ? nil : moveItems)
}
.toolbar {
ToolbarItem {
Button {
withAnimation {
self.disableMove.toggle()
}
} label: {
Text(editMode == .active ? "Done" : "Edit")
}
}
}
.environment(\.editMode, $editMode)
}
.onChange(of: disableMove) { disableMove in
withAnimation {
self.editMode = disableMove ? .inactive : .active
}
}
.navigationViewStyle(.stack)
}
func moveItems(from source: IndexSet, to destination: Int) { }
}
Not sure if this helps
enum Status {
case notPressed
case pressed
case longPressed
}
struct ContentView: View {
#State private var status = Status.notPressed
var body: some View {
Rectangle()
.foregroundColor(color)
.simultaneousGesture(LongPressGesture().onEnded { _ in
print("longPressed")
status = .longPressed
})
.simultaneousGesture(TapGesture().onEnded { _ in
print("pressed")
status = .pressed
})
}
var color: Color {
switch status {
case .notPressed:
return .mint
case .pressed:
return .yellow
case .longPressed:
return .orange
}
}
}

SwiftUI no hide animation

I have noticed that when I'm coloring the background I will not get animations when removing Views.
If I remove Color(.orange).edgesIgnoringSafeArea(.all) then hide animation will work, otherwise Modal will disappear abruptly. Any solutions?
struct ContentView: View {
#State var show = false
func toggle() {
withAnimation {
show = true
}
}
var body: some View {
ZStack {
Color(.orange).edgesIgnoringSafeArea(.all)
Button(action: toggle) {
Text("Modal")
}
if show {
Modal(show: $show)
}
}
}
}
struct Modal: View {
#Binding var show: Bool
func toggle() {
withAnimation {
show = false
}
}
var body: some View {
ZStack {
Color(.systemGray4).edgesIgnoringSafeArea(.all)
Button(action: toggle) {
Text("Close")
}
}
}
}
You need to make animatable container holding removed view (and this makes possible to keep animation in one place). Here is possible solution.
Tested with Xcode 12 / iOS 14
struct ContentView: View {
#State var show = false
func toggle() {
show = true // animation not requried
}
var body: some View {
ZStack {
Color(.orange).edgesIgnoringSafeArea(.all)
Button(action: toggle) {
Text("Modal")
}
VStack { // << major changes
if show {
Modal(show: $show)
}
}.animation(.default) // << !!
}
}
}
struct Modal: View {
#Binding var show: Bool
func toggle() {
show = false // animation not requried
}
var body: some View {
ZStack {
Color(.systemGray4).edgesIgnoringSafeArea(.all)
Button(action: toggle) {
Text("Close")
}
}
}
}

SwiftUI - Animate transition

I have a sign out button on a modal sheet that takes the user back to the login screen. To accomplish this I first dismiss the sheet and then, using asyncAfter(deadline:) I set an environment variable that causes the login page to appear. Everything works fine, but once the sheet is dismissed, the transition from the view under the sheet to the login page is pretty jarring. Mostly because there isn't one. The top view just disappears, revealing the login view. I know I can create custom transitions, but I can't figure out where to attach it. Say, for example, I want to fade out the view underneath the sheet. (Although, I'm open to any kind of transition!)
This is the struct that directs the traffic:
struct ConductorView: View {
#EnvironmentObject var tower: Tower
let onboardingCompleted = UserDefaults.standard.bool(forKey: "FirstVisit")
var body: some View {
VStack {
if tower.currentPage == .onboarding {
Onboarding1View()
} else if tower.currentPage == .login {
LoginView()
} else if tower.currentPage == .idle {
LoginView()
}
}.onAppear{
if self.onboardingCompleted {
self.tower.currentPage = .login
} else {
self.tower.currentPage = .onboarding
}
}
}
}
And this is the sign out button on the sheet:
Button(action: {
self.presentationMode.wrappedValue.dismiss()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self.tower.currentPage = .login
}
}) {
Text("Sign Out")
}
Here is a simplified demo on your replicated code (and I made some longer delay to make it mode visible). Of course you will need to tune it for your needs by changing type of transition or animation, etc. Tested with Xcode 12 / iOS 14
class Tower: ObservableObject {
enum PageType {
case onboarding, login, idle
}
#Published var currentPage: PageType = .onboarding
}
struct ConductorView: View {
#EnvironmentObject var tower: Tower
let onboardingCompleted = false
var body: some View {
VStack {
if tower.currentPage == .onboarding {
Onboarding1View()
} else if tower.currentPage == .login {
Text("LoginView")
.transition(.move(edge: .trailing)) // << here !!
} else if tower.currentPage == .idle {
Text("IdleView")
}
}
.animation(.default, value: tower.currentPage) // << here !!
.onAppear{
if self.onboardingCompleted {
self.tower.currentPage = .login
} else {
self.tower.currentPage = .onboarding
}
}
}
}
struct Onboarding1View: View {
#EnvironmentObject var tower: Tower
#Environment(\.presentationMode) var presentationMode
#State private var isPresented = true
var body: some View {
Text("Login")
.sheet(isPresented: $isPresented) {
Button(action: {
self.presentationMode.wrappedValue.dismiss()
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.tower.currentPage = .login
}
}) {
Text("Sign Out")
}
}
}
}

How to display some text when a button is clicked in Swift UI?

I have a function that displays "correct" when the user puts in the right answer.
func displayCorrectOrIncorrect() -> some View {
Group {
if correctAnswer == quiz.answer {
VStack {
Text("Correct")
}
} else {
VStack {
Text("Incorrect, correct answer is \(correctAnswer)")
}
}
}
}
This is how I call it:
Button(action: {
self.displayCorrectOrIncorrect()
self.updateUI()
})
The text is not showing up. How can I fix this?
You are attempting to return a view inside the action closure of your button. This closure merely specifies the action to be done, and does not render any views.
I would suggest going through Apple's SwiftUI tutorial.
Here is an example of what you asked...
struct ContentView: View {
#State var correctAnswer: Bool = false
var body: some View {
VStack {
Group {
if correctAnswer {
Text("Correct!")
} else {
Text("Incorrect!")
}
}
Button("Button") {
correctAnswer.toggle()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

How to go to another view with button click

I have a button in my code and I have a file called LogindView.swift
I cannot get the code to open another view file when clicking on the button.
Can anybody give me an example on how to do it.
In my button action I have tried to write LogindView() but i just gives me a warning.
"Result of 'LogindView' initializer is unused"
Button(action: {
// Do action
LogindView()
}, label: {
//** Label text
Text("Logind")
.font(.headline)
.padding(.all)
.foregroundColor(Color.white)
})
.background(Color.blue)
You essentially have 3 options to transition between views depending on your needs.
First, you can use a NavigationView. This will provide a back button and will allow the user to go back. Note that there are some bugs currently when you don't put the NavigationLink inside of a List as per https://stackoverflow.com/a/57122621/3179416
import SwiftUI
struct MasterView: View {
var body: some View {
NavigationView {
List {
NavigationLink(destination: LoginView()) {
Text("Login")
}
}
.navigationBarTitle(Text("Master"))
}
}
}
struct LoginView: View {
var body: some View {
Text("Login View")
}
}
Second, you can present a modal using .sheet. This will present a modal that appears on top of the current view but it can be dismissed by the user by dragging it down.
import SwiftUI
struct MasterView: View {
#State var isModal: Bool = false
var body: some View {
Button("Login") {
self.isModal = true
}.sheet(isPresented: $isModal, content: {
LoginView()
})
}
}
struct LoginView: View {
var body: some View {
Text("Login View")
}
}
Third, you can just use an if statement to change the current view to your Login View like so
import SwiftUI
struct MasterView: View {
#State var showLoginView: Bool = false
var body: some View {
VStack {
if showLoginView {
LoginView()
} else {
Button("Login") {
self.showLoginView = true
}
}
}
}
}
struct LoginView: View {
var body: some View {
Text("Login View")
}
}
If you would like to animate this, so that the transition doesn't appear so abruptly, you can also do this:
import SwiftUI
struct MasterView: View {
#State var showLoginView: Bool = false
var body: some View {
VStack {
if showLoginView {
LoginView()
.animation(.spring())
.transition(.slide)
} else {
Button("Login") {
withAnimation {
self.showLoginView = true
}
}.animation(.none)
}
}
}
}
struct LoginView: View {
var body: some View {
Text("Login View")
}
}
You can use navigation link instead button
var body: some View {
VStack {
Text("Title")
.font(.headline)
Image("myimage").clipShape(Circle())
Text("mytext").font(.title)
NavigationLink(destination: AnotherView()) {
Image(systemName: "person.circle").imageScale(.large)
}
}
}