When trying to navigate back from a view using the environment Dismiss value while also focussing on an empty searchable modifier the view you navigated back to becomes unresponsive. This is due to an empty UIView blocking any interaction with the view as seen in this screenshot:
Empty UIView blocking view after navigating back
This only occurs when the searchbar is focussed and empty when trying to navigate back. When there's a value in the searchbar everything works:
GIF of the bug
Am I doing something wrong here?
Tested on Xcode 14.2 iPhone 14 Pro (iOS 16.0) simulator.
import SwiftUI
struct MainPage: View {
var body: some View {
if #available(iOS 16.0, *) {
NavigationStack {
Text("Main view")
NavigationLink(destination: DetailView()) {
Text("Click me")
}
}
}
}
}
struct DetailView: View {
#Environment(\.dismiss) private var dismiss
#State private var searchText = ""
var body: some View {
VStack {
Text("Detail view")
Button("Go back") {
dismiss()
}
}
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always))
}
}
This bug only seems to happen when using NavigationStack or NavigationView with a .navigationViewStyle(.stack). When using NavigationView without a navigationViewStyle it seems to work fine. Currently I can work around this using the latter but I would prefer to use NavigationStack as NavigationView has become deprecated since iOS 16.0.
Any help is appreciated.
When dismissing a fullScreenCover using a variable inside an ObservableObject (lines commented with 1.-) it shows the "Publishing changes from within view updates is not allowed, this will cause undefined behavior." message in the console, but using a #State variable (lines commented with 2.-) does not show the warning. I do not understand why.
Here is the code:
import SwiftUI
final class DismissWarningVM: ObservableObject {
#Published var showAnotherView = false
}
struct DismissWarningView: View {
#StateObject private var dismissWarningVM = DismissWarningVM()
#State private var showAnotherView = false
var body: some View {
VStack {
HStack {
Spacer()
Button {
// 1.- This line provokes the warning
dismissWarningVM.showAnotherView = true
// 2.- This line DO NOT provokes the warning
//showAnotherView = true
} label: {
Text("Show")
}
}
.padding(.trailing, 20)
Spacer()
Text("Main view")
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.white)
// 1.- This line provokes the warning
.fullScreenCover(isPresented: $dismissWarningVM.showAnotherView) {
// 2.- This line DO NOT provokes the warning
//.fullScreenCover(isPresented: $showAnotherView) {
AnotherView()
}
}
}
struct AnotherView: View {
#Environment(\.dismiss) var dismiss
var body: some View {
VStack(spacing: 30) {
Text("Another view")
Button {
dismiss()
} label: {
Text("Dismiss")
.foregroundColor(.red)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.ignoresSafeArea()
}
}
struct DismissWarningView_Previews: PreviewProvider {
static var previews: some View {
DismissWarningView()
}
}
Fixed. It was a problem of the XCode version. I was trying with 14.0.1 version but after updating to 14.1 version the warning is no longer shown
#StateObject isn't designed for view model objects. In SwiftUI the View struct is the view model already you don't another one. Remember SwiftUI is diffing these structs and creating/updating/removing UIView objects automatically for us. If you use view model objects then you'll have viewModel object -> View struct -> UIView object which is a big mess and will lead to bugs. #StateObject is designed for when you need a reference type in an #State which isn't very often nowadays given we have .task and .task(id:) for asynchronous features.
You can achieve what you need like this:
struct WarningConfig {
var isPresented = false
// mutating func someLogic() {}
}
struct SomeView: View {
#State private var config = WarningConfig()
...
.fullScreenCover(isPresented: $config.isPresented) {
WarningView(config: $config)
I'm pretty new to SwiftUI, so I'm wondering how to navigate to a new view only when an async method returns with a value. Here is my view:
import SwiftUI
struct FollowersView: View {
#State var followers: [Follower]
#State var user: User?
#State private var selection: String? = nil
#StateObject private var viewModel = GitHubUsersViewModel()
let columns = [
GridItem(.flexible(minimum: 150, maximum: 175)),
GridItem(.flexible(minimum: 150, maximum: 175)),
]
var body: some View {
ScrollView {
LazyVGrid(columns: columns, spacing: 20) {
ForEach(followers, id: \.self) { follower in
FollowerCardView(follower: follower)
.onTapGesture {
Task {
self.user = try await self.viewModel.manager.getUser(for: follower.login)
self.selection = NavigationTags.userFound
}
}
}
}
.padding(.horizontal)
}
.frame(maxHeight: .infinity)
}
}
When self.user gets populated, I want to be able to navigate to another view. But, I can't figure out where to put the NavigationLink.
This FollowersView is already embedded in a NavigationView from within it's parent view.
Please advise?
For iOS 15 you can still use NavigationLink as it isn't deprecated until iOS 16 when the new NavigationStack stuff comes in.
Anyway...
struct SomeView: View {
#State var shouldNavigate: Bool = false
var body: some View {
NavigationLink(
isActive: $shouldNavigate,
destination: navDestination
) {
Text("Press me")
.onTapGesture {
let result = await someAsyncFunction()
shouldNavigate = true
}
}
}
#ViewBuilder
var navDestination: some View {
Text("Ta dah!")
}
}
This will navigate to your destination when the async function completes and sets shouldNavigate to true.
There are nmany ways that you could do this. This is just one of them. Good luck
As a side note, you need to make sure your view is part of a NavigationView too.
I have two different views, ContentView and CreateView.
In CreateView, I get user's inputs by textfield, and once user clicks on Save button, the inputs will be stored in AppStorage.
Then, I want to display the saved inputs on ContentView.
Here, I tried to use State & Binding but it didn't work out well.
How would I use the variable, that is created in CreateView, in ContentView?
what property should I use..
Thanks
Here's the updated questions with the code...
struct ContentView: View {
// MARK: - PROPERTY
#ObservedObject var appData: AppData
let createpage = CreatePage(appData: AppData())
var body: some View {
HStack {
NavigationLink("+ create a shortcut", destination: CreatePage(appData: AppData()))
.padding()
Spacer()
} //: HStack - link to creat page
VStack {
Text("\(appData.shortcutTitle) - \(appData.shortcutOption)")
}
}
struct CreatePage: View {
// MARK: - PROPERTY
#AppStorage("title") var currentShortcutTitle: String?
#AppStorage("option") var currentOption: String?
#Environment(\.presentationMode) var presentationMode
#ObservedObject var appData: AppData
var body: some View {
NavigationView{
ScrollView{
Text("Create a ShortCut")
.padding()
HStack {
TextField("what is the title?", text: $appData.titleInput)
.textFieldStyle(RoundedBorderTextFieldStyle())
//.frame(width: 150, height: 60, alignment: .center)
.border(Color.black)
.padding()
} //: HStack - Textfield - title
.padding()
HStack (spacing: 10) {
TextField("options?", text: $appData.optionInput)
.textFieldStyle(RoundedBorderTextFieldStyle())
.frame(width: 80, height: 40, alignment: .leading)
.padding()
} //: HStack - Textfield - option
.padding()
Button(action: {
self.appData.shortcutTitle = self.appData.titleInput
self.appData.shortcutOption = self.appData.optionInput
UserDefaults.standard.set(appData.shortcutTitle, forKey: "title")
UserDefaults.standard.set(appData.shortcutOption, forKey: "option")
presentationMode.wrappedValue.dismiss()
}, label: {
Text("Save")
.padding()
.frame(width: 120, height: 80)
.border(Color.black)
}) //: Button - save
.padding(.top, 150)
} //: Scroll View
} //: Navigation View
} //: Body
class AppData: ObservableObject {
#Published var shortcutTitle : String = "Deafult Shortcut"
#Published var shortcutOption : String = "Default Option"
#Published var titleInput : String = ""
#Published var optionInput : String = ""
}
So the problem here is that
when I put new inputs on CreatePage and tab the save button, the new inputs do not appear on ContentView page.The output keeps showing the default values of title and option, not user inputs.
If user makes a new input and hit the save button, I want to store them in AppStorage, and want the data to be kept on ContentView (didn't make the UI yet). Am I using the AppStorage and UserDefaults in a right direction?
If anyone have insights on these issues.. would love to take your advice or references.
You're creating instances of AppData in multiple places. In order to share data, you have to share one instance of AppData.
I'm presuming that you create AppData in a parent view of ContentView since you have #ObservedObject var appData: AppData defined at the top of the view (without = AppData()). This is probably in your WindowGroup where you also must have a NavigationView.
I removed the next (let createpage = CreatePage(appData: AppData())) because it does nothing. And in the NavigationLink, I passed the same instance of AppData.
struct ContentView: View {
// MARK: - PROPERTY
#StateObject var appData: AppData = AppData() //Don't need to have `= AppData()` if you already create it in a parent view
var body: some View {
// I'm assuming there's a NavigationView in a parent view
VStack { //note that I've wrapped the whole view in a VStack to avoid having two root nodes (which can perform differently in NavigationView depending on the platform)
HStack {
NavigationLink("+ create a shortcut", destination: CreatePage(appData: appData))
.padding()
Spacer()
} //: HStack - link to creat page
VStack {
Text("\(appData.shortcutTitle) - \(appData.shortcutOption)")
}
}
}
}
struct CreatePage: View {
// MARK: - PROPERTY
#AppStorage("title") var currentShortcutTitle: String?
#AppStorage("option") var currentOption: String?
#Environment(\.presentationMode) var presentationMode
#ObservedObject var appData: AppData
var body: some View {
NavigationView{
ScrollView{
Text("Create a ShortCut")
.padding()
HStack {
TextField("what is the title?", text: $appData.titleInput)
.textFieldStyle(RoundedBorderTextFieldStyle())
//.frame(width: 150, height: 60, alignment: .center)
.border(Color.black)
.padding()
} //: HStack - Textfield - title
.padding()
HStack (spacing: 10) {
TextField("options?", text: $appData.optionInput)
.textFieldStyle(RoundedBorderTextFieldStyle())
.frame(width: 80, height: 40, alignment: .leading)
.padding()
} //: HStack - Textfield - option
.padding()
Button(action: {
appData.shortcutTitle = appData.titleInput
appData.shortcutOption = appData.optionInput
presentationMode.wrappedValue.dismiss()
}, label: {
Text("Save")
.padding()
.frame(width: 120, height: 80)
.border(Color.black)
}) //: Button - save
.padding(.top, 150)
} //: Scroll View
} //: Navigation View
} //: Body
}
Regarding #AppStorage and UserDefaults, it's a little hard to tell what your intent is at this point with those. But, you shouldn't need to declare AppStorage and call UserDefaults on the same key -- #AppStorage writes to UserDefaults for you. Read more at https://www.hackingwithswift.com/quick-start/swiftui/what-is-the-appstorage-property-wrapper
you can use a singleton ObservableObject that conforms to NSObject so you can observe everything even older apple objects like progress.
class appData : NSObject , ObservableObject {
static let shared = appData()
#Published var localItems = Array<AVPlayerItem>()
#Published var fractionCompleted : Double = 0
#Published var downloaded : Bool = false
#Published var langdentifier = UserDefaults.standard.value(forKey: "lang") as? String ?? "en" {
didSet {
print("AppState isLoggedIn: \(langIdentifier)")
}
}
var progress : Progress?
override init() {
}
}
then u can use it anywhere in your code like this:
appData.shared.langIdentifier == "en" ? .leading : .trailing
You should be able to simply put AppStorage objects in the ObservableClass and call them from there. There should be no need to put AppStorage in the View and then read from UserDefaults in the class.
class AppData: ObservableObject {
#AppStorage(keyForValue) var valueForKey: ValueType = defaultValue
...
}
Of course, you could make #Published property in the class and define the getter and setter for it so it reads and writes directly to the UserDefaults but at that point, you're just creating more work than simply using AppStorage from the beginning directly in the class.
I came across some issues with my SwiftUI code.
I made a simple example.
Just a Button that opens a Sheet.
struct ContentView: View {
#State private var showSheet = false
var body: some View {
Button(action: {
showSheet.toggle()
}, label: {
Text("Button")
})
.sheet(isPresented: $showSheet) {
SheetView(show: $showSheet, selectedDate: .constant(nil))
}
}
}
The Sheet has an optional Binding.
You can close it via the mark button.
However, as soon as I use an #Environment wrapper, the xmark stops working.
struct SheetView: View {
#Binding var show: Bool
#Environment(\.colorScheme) var colorScheme
#Binding var selectedDate: Date?
var body: some View {
NavigationView {
Text("Hello, \(selectedDate?.description ?? "Welt")!")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
self.show = false
}) {
Image(systemName: "xmark")
.renderingMode(.original)
.accessibilityLabel(Text("Save"))
}
}
}
}
}
}
(This seems to be due to the view constantly refreshing itself?)
In addition: This only happens on my iPhone 12 Pro Max.
In the simulator or an iPhone 11 Pro Max it is working fine.
(Don't have any other devices to test on.)
If I don't use .constant(nil) but give it a $Date, it works just fine.
If I remove the #Environment, it also works.
Am I doing something wrong here?
What is my mistake?