I have TextField inside a List inside a NavigationView, declared like this:
NavigationView {
List {
Section("Item Name") {
TextField("", text: $itemName)
.textInputAutocapitalization(.sentences)
.disableAutocorrection(false)
.textFieldStyle(.plain)
}
}
}
In the .onAppear method of the NavigationView, I set the textfield's text variable, which is declared with #State, like this:
#State var itemName: String = ""
and:
.onAppear {
itemName = "Hello, World!"
}
The text is set properly, but the textfield somehow doesn’t refresh. I can tell because I can access the textfield's text property and get the updated value, and when the user taps the text field to edit it, the text suddenly appears.
It seems to me to be a problem with the textfield updating its view when the variable changes. Do I have to call a SwiftUI equivalent of UIKit's layoutSubviews? Or am I declaring my itemName variable wrong?
Minimal Reproducible Example (MRE):
struct ItemView: View {
#State var itemName: String = ""
var body: some View {
NavigationView {
List {
Section("Item Name") {
TextField("", text: $itemName)
.textInputAutocapitalization(.sentences)
.disableAutocorrection(false)
.textFieldStyle(.plain)
}
}
.navigationTitle("Item")
}
.onAppear {
itemName = "Hello, World!"
}
}
}
Any help would be appreciated.
It worked fine in Preview, but I took the next step and tested it in the Simulator, and it failed. That lead me to realize what the issue was. We see this a great deal with animations that are started from .onAppear(). The view is set up and .onAppear() called before the view is actually on screen, causing a failure to update from the .onAppear(). Think of it almost as a race condition. The view has to be set up and on screen before the update can be called, and it needs an extra cycle to do this. As a result the fix is simple:
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now()){
itemName = "Hello, World!"
}
}
Wrap it in a DispatchQueue.main.asyncAfter(deadline:). This gives it time to avoid the issue.
I also was able to reproduce it. I agree with Yrb, it seems like the state mutation in onAppear happens during the TextField's update and therefore the text field isn't picking up the new value from the state.
Another way I was able to fix it is to move the state mutation from the NavigationView's .onAppear modifier to the TextField's .onAppear modifier. If you do that, you won't need DispatchQueue.main.async because SwiftUI will correctly synchronize the onAppear block's execution with the text field's update.
Related
The following code is simplified and isolated. It is intended to have a Text() view, which shows the number of times the button has been clicked, and a Button() to increment the text view.
The issue: Clicking the button does not actually change the Text() view, and it continues to display "1"
struct Temp: View {
#State var h = struct1()
var body: some View {
VStack {
Button(action: {
h.num += 1
}, label: {
Text("CLICK ME")
})
Text(String(h.num))
}
}
}
struct struct1 {
#State var num = 1
}
What's funny is that the same code works in swift playgrounds (obviously with the Text() changed to print()). So, I'm wondering if this is an XCode specific bug? If not, why is this happening?
Remove #State from the variable in struct1
SwiftUI wrappers are only for SwiftUI Views with the exception of #Published inside an ObservableObject.
I have not found this in any documentation explicitly but the wrappers conform to DynamicProperty and of you look at the documentation for that it says that
The view gives values to these properties prior to recomputing the view’s body.
So it is implied that if the wrapped variable is not in a struct that is also a SwiftUI View it will not get an updated value because it does not have a body.
https://developer.apple.com/documentation/swiftui/dynamicproperty
The bug is that it works in Playgrounds but Playground seems to have a few of these things. Likely because of the way it complies and runs.
I searched a lot about this error but it seems there's no solution...
UITableView was told to layout its visible cells and other contents without being in the
view hierarchy (the table view or one of its superviews has not been added to a window).
This may cause bugs by forcing views inside the table view to load and perform layout without
accurate information (e.g. table view bounds, trait collection, layout margins, safe area
insets, etc), and will also cause unnecessary performance overhead due to extra layout passes.
Make a symbolic breakpoint at UITableViewAlertForLayoutOutsideViewHierarchy to catch this in
the debugger and see what caused this to occur, so you can avoid this action altogether if
possible, or defer it until the table view has been added to a window
This is my actual code:
struct ContentView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(fetchRequest: FavoriteBooks.getAllFavoriteBooks()) var favoriteBooks:FetchedResults<FavoriteBooks>
#ObservedObject var bookData = BookDataLoader()
var body: some View {
NavigationView {
List {
Section {
NavigationLink(destination: FavoriteView()) {
Text("Go to favorites")
}
}
Section {
ForEach(0 ..< bookData.booksData.count) { num in
HStack {
Text("\(self.bookData.booksData[num].titolo)")
Button(action: {
**let favoriteBooks = FavoriteBooks(context: self.managedObjectContext)
favoriteBooks.titolo = self.bookData.booksData[num].titolo**
}) {
Image(systemName: "heart")
}
}
}
}
}
}
}
}
struct FavoriteView: View {
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(fetchRequest: FavoriteBooks.getAllFavoriteBooks()) var favoriteBooks:FetchedResults<FavoriteBooks>
var body: some View {
List {
**ForEach (self.favoriteBooks) { book in
Text("\(book.titolo!))")**
}
}
}
}
I just selected on bold what makes this error and I don't know how to avoid it because if I launch the app it doesn't crash but I cannot do anything.
Thanks in advance
You have a couple issues here. The first is, when you use ForEach, if your content is supposed to be able to change (which, with FetchRequest it is...) then either FavoriteBooks needs to be Identifiable or you need to pass in your id. You actually do this twice in the code:
ForEach(0 ..< bookData.booksData.count) { num in
// SwiftUI thinks this content never changes because it doesn't know how to resolve those changes. you didn't tell it
}
should be:
ForEach(0 ..< bookData.booksData.count, id: \.self) { ... }
Notice now you are telling it what the id is. If the count of bookData.booksData changes, now SwiftUI can resolve those changes. But really, why do you need the index in this case specifically? Why not just:
ForEach(bookData.booksData) { book in ... }
If you make this object type conform to Identifiable, you now have the book already.
Now on to another problem, your button action. Why are you re-executing the CoreData query here? You have the Set of the objects you want. This is another reason to just use ForEach(bookData.booksData), you don't have to resolve the index here. But in general, you should never need to re-execute your core data query to find a specific object. what this actually does is then trigger another update on your entire view hierarchy, which is likely why you get the error you're getting. you aren't supposed to be doing this.
For the application I am designing, I have a prominent header that we want to appear on our Homepage. I want it to disappear when I navigate into a child view, a dynamic view embedded within a custom SwiftUI element I created entitled a CategoryRow.
I've got it working, sort of, where the header disappears upon navigation into NavigationLink that I have embedded within the CategoryRow. Only problem is, the response time is a bit slow, and it seems as if the onDisappear event within the NavigationLink only fires sometimes. Here's a gif that demonstrates the exact behavior I am facing:
Here's some of the relevant code I have within my parent view, which I have entitled FeedView:
#State private var showHeader = true // showHeader will be a state variable passed between the parent and child in order to show/hide the header
var body: some View {
VStack(spacing: 0) {
if showHeader { // update and show/hide the header embedded in the HStack on state change
HStack(spacing: 0) {
// header
}
}
NavigationView {
List(categories.keys.sorted(), id: \String.self) {
// we pass the state variable over to the CategoryRow class to be used in appear/disappear
key in CategoryRow(nameOfCategory: "\(key)".uppercased(), posts: self.posts[key]!, showHeader: self.$showHeader)
}
}
(Child View; a CategoryRow element):
#Binding var showHeader: Bool // bind the passed in showHeader variable so we can pass it back to the parent when updates need to happen
// ...
NavigationLink(destination: ExpandMediaView(post: post)
.onAppear { self.showHeader = false } // after testing, this onAppear ALWAYS manages to fire
.onDisappear { self.showHeader = true } // why does this only fire sometimes?
)
The code only hits the .onDisappear portion of the NavigationLink sometimes, which means sometimes the header reappears on the parent class, and sometimes it doesn't. On top of this, the show/hiding of it is about a millisecond behind the page navigation. Has anyone worked through an issue like this before?
I'm attempting to use SwiftUI and CoreData to build a macOS application. This application's main window has a NavigationView, with list items bound to a fetch request, and selecting any of these items populates the detail view. The navigation view goes kind of like this:
NavigationView {
VStack(spacing: 0) {
List(fetchRequest) { DetailRow(model: $0) }
.listStyle(SidebarListStyle())
HStack {
Button(action: add) { Text("+") }
Button(action: remove) { Text("-") }
}
}
Text("Select a model object")
}.navigationViewStyle(DoubleColumnNavigationViewStyle())
DetailRow is a NavigationLink that also defines the detail view:
NavigationLink(destination: ModelDetail(model: model)) {
Text(model.name)
}
I believe that the contents of ModelDetail isn't very important; either way, I'm fairly flexible with it.
In the navigation view, the "-" button, which calls the remove method, should delete the currently-selected model object and return to the default, empty detail view. Unfortunately, I'm struggling to come up with the right way to do this. I believe that I need the following interactions to happen:
subview communicates to navigation view which model object is currently selected
user clicks "-" button, navigation view's remove method deletes currently selected object
subview notices that its model object is being deleted
→ subview calls PresentationMode.dismiss()
Step 3 is the one I'm struggling with. Everything is working out alright so far without using view-model classes on top of the Core Data classes, but I feel stuck trying to figure out how to get the subview to call dismiss(). This needs to happen from the detail view, because it gets the PresentationMode from the environment, and the NavigationView changes it.
While I can get a Binding to the model's isDeleted property through #ObservedObject, I don't know how I can actually react to that change; Binding appears to use publishers under the hood, but they don't expose a publisher that I could hook up to with onPublish, for instance.
KVO over isDeleted might be possible, but listening from a value type isn't great; there's no good place to remove the observer, which could become problematic were the app to run for too long.
What's the guidance for this type of problem?
Heres my solution.
This is my NoteDetailView. It allows deletion from this view, or the "master" view in the Navigation hierarchy. This solution works on Mac, iPad, and iPhone.
I added an optional dateDeleted to my Entity. When a record is deleted, I simply add a value of Date() to this attribute and save the context. In my FetchRequests, I simply predicate for dateDeleted = nil. I'm going to add a trash can and stuff to my app later so people can view or permanently empty their trash.
Then I use a state variable and a notification to clear my View. You can change the code up for the functionality you want:
struct NoteDetailView: View {
var note: Note
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#Environment(\.managedObjectContext) var managedObjectContext
#State var noteBody: String = ""
#State var showEditNoteView: Bool = false
#State var showEmptyView: Bool = false
init(note: Note) {
self.note = note
self._noteBody = State(initialValue: note.body)
}
var body: some View {
VStack {
if (!showEmptyView) {
Text("NOT DELETED")
}
else {
EmptyView()
}
}
.navigationBarTitle(!showEmptyView ? note.title : "")
.navigationBarItems(trailing:
HStack {
if (!showEmptyView) {
Button(action: {
self.showEditNoteView.toggle()
}, label: {
NavBarImage(image: "pencil")
})
.sheet(isPresented: $showEditNoteView, content: {
EditNoteView(note: self.note).environment(\.managedObjectContext, self.managedObjectContext)
})
}
}
)
.onReceive(NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave)) { _ in
if (self.note.dateDeleted != nil) {
self.showEmptyView = true
self.presentationMode.wrappedValue.dismiss()
}
}
}
}
I want to update a text label after it is being pressed, but I am getting this error while my app runs (there is no compile error): [SwiftUI] Modifying state during view update, this will cause undefined behaviour.
This is my code:
import SwiftUI
var randomNum = Int.random(in: 0 ..< 230)
struct Flashcard : View {
#State var cardText = String()
var body: some View {
randomNum = Int.random(in: 0 ..< 230)
cardText = myArray[randomNum].kana
let stack = VStack {
Text(cardText)
.color(.red)
.bold()
.font(.title)
.tapAction {
self.flipCard()
}
}
return stack
}
func flipCard() {
cardText = myArray[randomNum].romaji
}
}
If you're running into this issue inside a function that isn't returning a View (and therefore can't use onAppear or gestures), another workaround is to wrap the update in an async update:
func updateUIView(_ uiView: ARView, context: Context) {
if fooVariable { do a thing }
DispatchQueue.main.async { fooVariable = nil }
}
I can't speak to whether this is best practices, however.
Edit: I work at Apple now; this is an acceptable method. An alternative is using a view model that conforms to ObservableObject.
struct ContentView: View {
#State var cardText: String = "Hello"
var body: some View {
self.cardText = "Goodbye" // <<< This mutation is no good.
return Text(cardText)
.onTapGesture {
self.cardText = "World"
}
}
}
Here I'm modifying a #State variable within the body of the body view. The problem is that mutations to #State variables cause the view to update, which in turn call the body method on the view. So, already in the middle of a call to body, another call to body initiated. This could go on and on.
On the other hand, a #State variable can be mutated in the onTapGesture block, because it's asynchronous and won't get called until after the update is finished.
For example, let's say I want to change the text every time a user taps the text view. I could have a #State variable isFlipped and when the text view is tapped, the code in the gesture's block toggles the isFlipped variable. Since it's a special #State variable, that change will drive the view to update. Since we're no longer in the middle of a view update, we won't get the "Modifying state during view update" warning.
struct ContentView: View {
#State var isFlipped = false
var body: some View {
return Text(isFlipped ? "World" : "Hello")
.onTapGesture {
self.isFlipped.toggle() // <<< This mutation is ok.
}
}
}
For your FlashcardView, you might want to define the card outside of the view itself and pass it into the view as a parameter to initialization.
struct Card {
let kana: String
let romaji: String
}
let myCard = Card(
kana: "Hello",
romaji: "World"
)
struct FlashcardView: View {
let card: Card
#State var isFlipped = false
var body: some View {
return Text(isFlipped ? card.romaji : card.kana)
.onTapGesture {
self.isFlipped.toggle()
}
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
return FlashcardView(card: myCard)
}
}
#endif
However, if you want the view to change when card itself changes (not that you necessarily should do that as a logical next step), then this code is insufficient. You'll need to import Combine and reconfigure the card variable and the Card type itself, in addition to figuring out how and where the mutation going to happen. And that's a different question.
Long story short: modify #State variables within gesture blocks. If you want to modify them outside of the view itself, then you need something else besides a #State annotation. #State is for local/private use only.
(I'm using Xcode 11 beta 5)
On every redraw (in case a state variable changes) var body: some View gets reevaluated. Doing so in your case changes another state variable, which would without mitigation end in a loop since on every reevaluation another state variable change gets made.
How SwiftUI handles this is neither guaranteed to be stable, nor safe. That is why SwiftUI warns you that next time it may crash due to this.
Be it due to an implementation change, suddenly triggering an edge condition, or bad luck when something async changes text while it is being read from the same variable, giving you a garbage string/crash.
In most cases you will probably be fine, but that is less so guaranteed than usual.