SwiftUI: FocusState animations stop working when using NavigationViews - swift

I am trying to implement an animation relating to a TextField. A Cancel button slide in when the textfield is clicked. However, it only works correctly when it's in a standalone view. When I try to nest the view within a NavigationLink, the animation stops working. Here's the code:
struct TestView: View {
#FocusState private var isEditing: Bool
var body: some View {
VStack {
Button("click me", action: { isEditing.toggle() })
HStack {
TextField("Search", text: .constant("test"))
.focused($isEditing)
.padding(8)
.padding(.leading, 25)
.padding(.trailing, 22)
.background(Color.gray)
.cornerRadius(10)
.padding(.horizontal)
if isEditing {
Button {} label: {
ZStack {
Text("Cancel")
.foregroundColor(.primary)
.padding(.trailing)
}
}
.transition(.move(edge: .trailing))
}
}
.animation(.spring(), value: isEditing)
.navigationBarHidden(true)
}
}
}
Correct animation:
https://imgur.com/iqGr7fx
However, when I have a second view with a NavigationLink containing the previous view:
struct TestView2: View {
#State var test: Bool = false
var body: some View {
NavigationView {
VStack {
NavigationLink(isActive: $test, destination: { TestView() }, label: {})
Button("click me", action: { test.toggle() })
}
.navigationBarHidden(true)
}
}
}
The animation looks like this:
https://imgur.com/a/LK9pxf2
Is this a bug related to SwiftUI? Or am I not supposed to use FocusState for animations? If so, how can I change the code to have the animation work in both versions?

Fixed the animations by changing the FocusState variable to a State variable and then removing the .focused modifier and changing it to .onTapGesture, setting the isEditing variable to true

Related

Entering new view after button press (Swift)

struct EnterOptions: View {
var body: some View {
VStack{
Button(action: {
//Action here
} ){
Text("SIGN UP").frame(maxWidth: 300)
.frame(maxHeight: .greatestFiniteMagnitude)
.font(.largeTitle)
.fontWeight(.heavy)
}
I create a button where I want the user to decide to sign up or login. Just the sign up is shown.
struct makeM: View {
#StateObject var viewModel: makeM.ViewModel = .init()
#StateObject var home: makeM.LoggedIn = .init()
var body: some View {
VStack{
TextField("Username", text: $viewModel.username)
TextField("Password", text: $viewModel.password)
TextField("Email", text: $viewModel.email)
Button("Sign Up", action: {
Task{await viewModel.signUp() }} )
Button("Sign In", action: {
Task{ await viewModel.signIn() }
})
//if(viewModel.si == true){
Button("Sign Out", action: {
Task{ await viewModel.signOutLocally() }
})
//}
}
.padding(.horizontal, 20)
}
I want the user to see this screen after clicking the button. I've tried using "makeM()" or putting MakeM() in a WindowGroup() but not sure how I can switch screens for the user using the right syntax. Having trouble in the "//Action Here" part in the first code snippet. Am I going about this the wrong way? Not trying to use storyboard
You should use NavigationLink if you want to show another screen, but make sure that whole view is wrapped by NavigationStack or NavigationView.
For example:
struct EnterOptions: View {
var body: some View {
NavigationStack {
VStack{
NavigationLink {
// Destination
} label: {
Text("SIGN UP").frame(maxWidth: 300)
.frame(maxHeight: .greatestFiniteMagnitude)
.font(.largeTitle)
.fontWeight(.heavy)
}
}

Dismissing a SwiftUI sheet with a lot of navigation views

I have a button that opens up a Profile & Settings view in a sheet that has additional navigation views in it.
I am aware how to dismiss the sheet, however this method seems to not work with additional navigation views, as when I'm deeper into the navigation and I tap "Done" to dismiss the sheet, it only returns me back to the previous navigation view until I go back to the main Profile & Settings view.
The view with the button:
import SwiftUI
struct TodayView: View {
#State private var showSheet = false
var body: some View {
NavigationView {
ScrollView {
VStack(alignment: .leading) {
TodayTabDateComponent()
.padding(.top, -10)
ForEach(0 ..< 32) { item in
VStack(alignment: .leading) {
Text("Title")
Text("Description")
}
.padding(.vertical)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal)
}
.navigationTitle("Today")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
showSheet = true
}, label: {
Image(systemName: "person.circle.fill")
.foregroundColor(.primary)
})
.sheet(isPresented: $showSheet) {
ProfileAndSettingsView()
}
}
}
}
}
}
struct TodayView_Previews: PreviewProvider {
static var previews: some View {
TodayView()
}
}
The Profile & Settings view:
import SwiftUI
struct ProfileAndSettingsView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
VStack {
List {
Section {
NavigationLink {
UserProfileView()
} label: {
HStack(alignment: .center) {
Image("avatar")
.resizable()
.aspectRatio(contentMode: .fit)
.clipShape(Circle())
.frame(width: 60, height: 60)
VStack(alignment: .leading) {
Text("Name Surname")
.font(.title2)
.fontWeight(.bold)
Text("Profile Settings, Feed Preferences\n& Linked Accounts")
.font(.caption)
}
}
}
.padding(.vertical, 6)
} }
.listStyle(.insetGrouped)
.navigationTitle("Profile & Settings")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Text("Done")
}
}
}
}
}
}
}
struct ProfileAndSettingsView_Previews: PreviewProvider {
static var previews: some View {
ProfileAndSettingsView()
}
}
I have looked into the issue but couldn't find any working solutions.
Is your issue here that you're applying the .sheet to the Button inside the Toolbar? I think you need to apply it to the NavigationView itself?
import SwiftUI
struct TodayView: View {
#State private var showSheet = false
var body: some View {
NavigationView {
....
}
.navigationTitle("Today")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
showSheet = true
}, label: {
Image(systemName: "person.circle.fill")
.foregroundColor(.primary)
})
}
}
.sheet(isPresented: $showSheet) {
ProfileAndSettingsView()
}
}
}
}
If you're targeting iOS15 or higher don't use presentationMode use #Environment(\.isPresented) private var isPresented instead this will perform the action that you want.
presentationMode was deprecated and replaced by isPresented and dismiss
I believe that presentationMode performs a similar action as dismiss does which according to Apple Docs (on the dismiss)
If you do this, the sheet fails to dismiss because the action applies to the environment where you declared it, which is that of the detail view, rather than the sheet. In fact, if you’ve presented the detail view in a NavigationView, the dismissal pops the detail view the navigation stack.
The dismiss action has no effect on a view that isn’t currently presented. If you need to query whether SwiftUI is currently presenting a view, read the isPresented environment value.
If you're targeting a lower iOS version you can create your own key like so
struct SheetOpen: EnvironmentKey {
static var defaultValue: Binding<Bool> = .constant(false)
}
extension EnvironmentValues {
var sheetOpen: Binding<Bool> {
get { self[SheetOpen.self] }
set { self[SheetOpen.self] = newValue }
}
}
Where you have your sheet defined you do this
.sheet(isPresented: $showSheet) {
ProfileAndSettingsView()
.environment(\.sheetOpen, $showSheet)
}
Then you can use it like any other environment variable
#Environment(\.sheetOpen) var sheetOpen
To dismiss it you simply do this sheetOpen.wrappedValue.toggle()

Dismiss view and push to navigation stack SwiftUI

I have my entire application wrapped in a NavigationView and am trying to duplicate the transition in the brief video listed below. Based on what I am seeing, it looks like they present a fullScreenCover, and when a link is pressed it dismisses the fullScreenCover and pushes whatever was tapped onto the navigation stack once the dismiss has completed.
Example video
I currently have this...
#Environment(\.dismiss) var dismiss
ScrollView {
VStack {
ForEach(viewModel.searchResults, id: \.self) { bottle in
NavigationLink(destination: BottleDetailView(bottle: bottle)) {
BottleCell(bottle: bottle) <-- tapping this would dismiss fullscreenCover and push this NavigationLink into the NavigationStack of my app
}
}
}
}
I have tried embedding another navigation view in the fullscreenCover but that was not even close to duplicating the transition above. How can I duplicate this?
You can make use of a programmatically controllable NavigationStack. In the fullscreenCover first dismiss, then wait for it to vanish, then set the navigation destination.
Here is a full example:
struct ContentView: View {
#State private var search = ""
#State private var showSheet = false
// programmatically controllable Navigation Stack
#State private var path = [Int]()
var body: some View {
NavigationStack(path: $path) {
VStack {
searchField
.disabled(true)
.onTapGesture {
showSheet = true
}
Spacer()
Text("Other stuff")
Spacer()
}
.padding()
.navigationTitle("Find something")
.fullScreenCover(isPresented: $showSheet) {
fullscreenSheet
}
// this defines the destination(s) for the programatically activated navigation stack
.navigationDestination(for: Int.self) { value in
Text("Detail View for Result \(value)")
}
}
}
var fullscreenSheet: some View {
VStack(alignment: .leading, spacing: 30) {
HStack {
searchField
Button("Cancel") { showSheet = false }
}
// dummy search results
ForEach(1..<6) { result in
Button("Result \(result) >") {
// dismiss sheet
showSheet = false
// wait and trigger navigation
Task {
try await Task.sleep(for: .seconds(0.1))
self.path = [result]
}
}
}
Spacer()
}
.padding()
}
var searchField: some View {
HStack {
Image(systemName: "magnifyingglass")
.foregroundColor(.white)
TextField("", text: $search,
prompt: Text("What are you looking for?")
.foregroundColor(.white)
)
}
.padding()
.background(
Capsule().fill(.gray)
)
}
}

NavigationLink keeps aligning my text elements to center instead of leading SwiftUI

I have a CustomSearchBar view that looks like this
However, when I wrap it with NavigationLink, the placeholder text will be centered. And user inputs will be centered too.
How do I maintain the leading alignment while using NavigationLink?
My code structure looks like this:
enum Tab {
case social
}
struct MainAppView: View {
#State var selection: Tab = .social
var body: some View {
TabView(selection: $selection) {
ZStack{
CustomButton()
NavigationView { SocialView() }
}.tabItem{Image(systemName: "person.2")}.tag(Tab.social)
// other tabs....
}
struct SocialView: View {
// ...
var body: some View {
GeometryReader{ geometry in
VStack{
NavigationLink(destination: Text("test")) {
CustomSearchBar()
//...
}.navigationBarHidden(true)
.navigationBarTitle(Text(""))
}
}
}
}
struct CustomSearchBar: View {
var body: some View {
VStack{
HStack {
SearchBarSymbols(// some binding arguments)
CustomTextField(// some binding arguments)
CancelButton(// some binding arguments)
}
.padding(.vertical, 8.0)
.padding(.horizontal, 10.0)
.background(Color("SearchBarBackgroundColor"))
.clipShape(Capsule())
}
.padding(.horizontal)
}
}
struct CustomTextField: View {
var body: some View {
TextField("friend name", text: $searchText)
.frame(alignment: .leading)
.onTapGesture {
// some actions
}
.foregroundColor(Color("SearchBarSymbolColor"))
.accentColor(Color("SearchBarSymbolColor"))
.disableAutocorrection(true)
}
}
The issues with your code are:
Your navigation view contains the search field. This means that any new view that gets pushed will cover the search field.
Your search field is inside of the navigation link. There are conflicting interactions here as it effectively turns the field into a button, ie tapping the search field vs tapping the navigation link.
Solution:
Move the navigation view below the text field, so that the new view will appear without covering it. Then change the navigation link so that it is activated via a binding that gets triggered when the search field is editing:
struct SocialView: View {
#State private var text: String = ""
#State private var isActive: Bool = false
var body: some View {
GeometryReader{ geometry in
VStack {
CustomTextField(searchText: $text, isActive: $isActive)
.padding(.vertical, 8.0)
.padding(.horizontal, 10.0)
.background(Color("SearchBarBackgroundColor"))
.clipShape(Capsule())
NavigationView {
NavigationLink(isActive: $isActive, destination: { Text("test") }, label: { EmptyView() })
}
}
}
}
}
struct CustomTextField: View {
#Binding var searchText: String
#Binding var isActive: Bool
var body: some View {
TextField("friend name", text: $searchText) { editing in
self.isActive = editing
} onCommit: {
}
.frame(alignment: .leading)
.disableAutocorrection(true)
}
}

SwiftUI conditional view will not animate/transition

I’m trying to get my views to animate/transition using .transition() on views. I use similar code from here and put .transition() to both conditional views.
struct Base: View {
#State private var isSignedIn = false
var body: some View {
Group {
if(isSignedIn){
Home().transition(.slide)
}else{
AuthSignin(isSignedIn: self.$isSignedIn).transition(.slide)
}
}
}
}
struct AuthSignin: View {
#Binding var isSignedIn: Bool
var body: some View {
VStack {
Button(action: {
self.isSignedIn = true
}) {
Text("Sign In")
.bold()
.frame(minWidth: CGFloat(0), maxWidth: .infinity)
.padding()
.background(Color.blue)
.foregroundColor(Color.white)
.cornerRadius(CGFloat(10))
}.padding()
}
}
}
However, whenever I click on the "Sign In" button (with or without .transition()), the app will freeze for a second and then the Home() view will suddenly appear without any animation/transition. I've also tried to wrap self.isSignedIn = true in withAnimation but it still won't work. Any ideas or is there a better way to do this?
Place your .transition on the container of the views that will switch, not each conditional view. Here's a trivial example from some code I have done (which works).
In the main View that needs to transition conditionally:
import SwiftUI
struct AppWrapperView: View {
#State var showFirstRun:Bool = true
var body: some View {
ZStack {
if (showFirstRun) {
FirstRunView(showFirstRun: $showFirstRun)
} else {
Text("Some other view")
}
}
.transition(.slide)
}
}
Then, somewhere in the view that triggers the change in condition:
import SwiftUI
struct FirstRunView: View {
#Binding var showFirstRun:Bool
var body: some View {
Button(action: {
withAnimation {
self.showFirstRun = false
}
}) {
Text("Done")
}
}
}
I had to put my if..else statement inside ZStack container instead of Group. Seems that Group was the main reason for broken animation in my case. Also, I applied .transition in combination with .animation to container instead of views.
ZStack {
if(isSignedIn){
Home()
} else {
AuthSignin(isSignedIn: self.$isSignedIn)
}
}
.transition(.slide)
.animation(.easeInOut)
Put
WithAnimation before self.isSignedIn = true