I'm trying the iOS 16 beta, MacOS 13 beta, and Xcode 14 beta. I can't get TextField to edit a property within a #Binding struct. It works with a #State struct variable, but not binding. Anyone else seeing this problem?
I filed a Feedback with Apple a couple of days ago, but no response yet. I imagine they are swamped the week after WWDC.
MacOS Version 13.0 Beta (22A5266r)
Xcode Version 14.0 beta (14A5228q)
Here's a code example of the problem.
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationStack {
List {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
}
/// Click on Category List
NavigationLink {
CatList()
} label: {
Text("Category List")
}
}
}
}
}
struct CatList: View {
/// Click on bar
#State var catArray: [Categories] = [ Categories(id: UUID(), myName: "foo"), Categories(id: UUID(), myName: "bar")]
var body: some View {
List {
ForEach($catArray) { $eachCat in
NavigationLink {
CatDetails(thisCategory: $eachCat)
} label: {
Text(eachCat.myName)
}
}
}
}
}
struct CatDetails: View {
#Binding var thisCategory: Categories
var body: some View {
Form {
/// Cannot edit bar.
TextField("Name:", text: $thisCategory.myName)
}
}
}
struct Categories: Identifiable, Hashable {
var id: UUID
var myName: String
}
Related
The app has a model that stores the user's current preference for light/dark mode, which the user can change by clicking on a button:
class DataModel: ObservableObject {
#Published var mode: ColorScheme = .light
The ContentView's body tracks the model, and adjusts the colorScheme when the model changes:
struct ContentView: View {
#StateObject private var dataModel = DataModel()
var body: some View {
NavigationStack(path: $path) { ...
}
.environmentObject(dataModel)
.environment(\.colorScheme, dataModel.mode)
As of Xcode Version 14.0 beta 5, this is producing a purple warning: Publishing changes from within view updates is not allowed, this will cause undefined behavior. Is there another way to do this? Or is it a hiccup in the beta release? Thanks!
Update: 2022-09-28
Xcode 14.1 Beta 3 (finally) fixed the "Publishing changes from within view updates is not allowed, this will cause undefined behavior"
See: https://www.donnywals.com/xcode-14-publishing-changes-from-within-view-updates-is-not-allowed-this-will-cause-undefined-behavior/
Full disclosure - I'm not entirely sure why this is happening but these have been the two solutions I have found that seem to work.
Example Code
// -- main view
#main
struct MyApp: App {
#StateObject private var vm = ViewModel()
var body: some Scene {
WindowGroup {
ViewOne()
.environmentObject(vm)
}
}
}
// -- initial view
struct ViewOne: View {
#EnvironmentObject private var vm: ViewModel
var body: some View {
Button {
vm.isPresented.toggle()
} label: {
Text("Open sheet")
}
.sheet(isPresented: $vm.isPresented) {
SheetView()
}
}
}
// -- sheet view
struct SheetView: View {
#EnvironmentObject private var vm: ViewModel
var body: some View {
Button {
vm.isPresented.toggle()
} label: {
Text("Close sheet")
}
}
}
// -- view model
class ViewModel: ObservableObject {
#Published var isPresented: Bool = false
}
Solution 1
Note: from my testing and the example below I still get the error to appear. But if I have a more complex/nested app then the error disappears..
Adding a .buttonStyle() to the button that does the initial toggling.
So within the ContentView on the Button() {} add in a .buttonStyle(.plain) and it will remove the purple error:
struct ViewOne: View {
#EnvironmentObject private var vm: ViewModel
var body: some View {
Button {
vm.isPresented.toggle()
} label: {
Text("Open sheet")
}
.buttonStyle(.plain) // <-- here
.sheet(isPresented: $vm.isPresented) {
SheetView()
}
}
}
^ This is probably more of a hack than solution since it'll output a new view from the modifier and that is probably what is causing it to not output the error on larger views.
Solution 2
This one is credit to Alex Nagy (aka. Rebeloper)
As Alex explains:
.. with SwiftUI 3 and SwiftUI 4 the data handling kind of changed. How SwiftUI handles, more specifically the #Published variable ..
So the solution is to have the boolean trigger to be a #State variable within the view and not as a #Published one inside the ViewModel. But as Alex points out it can make your views messy and if you have a lot of states in it, or not be able to deep link, etc.
However, since this is the way that SwiftUI 4 wants these to operate, we run the code as such:
// -- main view
#main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ViewOne()
}
}
}
// -- initial view
struct ViewOne: View {
#State private var isPresented = false
var body: some View {
Button {
isPresented.toggle()
} label: {
Text("Open sheet")
}
.sheet(isPresented: $isPresented) {
SheetView(isPresented: $isPresented)
// SheetView() <-- if using dismiss() in >= iOS 15
}
}
}
// -- sheet view
struct SheetView: View {
// I'm showing a #Binding here for < iOS 15
// but you can use the dismiss() option if you
// target higher
// #Environment(\.dismiss) private var dismiss
#Binding var isPresented: Bool
var body: some View {
Button {
isPresented.toggle()
// dismiss()
} label: {
Text("Close sheet")
}
}
}
Using the #Published and the #State
Continuing from the video, if you need to still use the #Published variable as it might tie into other areas of your app you can do so with a .onChange and a .onReceive to link the two variables:
struct ViewOne: View {
#EnvironmentObject private var vm: ViewModel
#State private var isPresented = false
var body: some View {
Button {
vm.isPresented.toggle()
} label: {
Text("Open sheet")
}
.sheet(isPresented: $isPresented) {
SheetView(isPresented: $isPresented)
}
.onReceive(vm.$isPresented) { newValue in
isPresented = newValue
}
.onChange(of: isPresented) { newValue in
vm.isPresented = newValue
}
}
}
However, this can become really messy in your code if you have to trigger it for every sheet or fullScreenCover.
Creating a ViewModifier
So to make it easier for you to implement it you can create a ViewModifier which Alex has shown works too:
extension View {
func sync(_ published: Binding<Bool>, with binding: Binding<Bool>) -> some View {
self
.onChange(of: published.wrappedValue) { newValue in
binding.wrappedValue = newValue
}
.onChange(of: binding.wrappedValue) { newValue in
published.wrappedValue = newValue
}
}
}
And in use on the View:
struct ViewOne: View {
#EnvironmentObject private var vm: ViewModel
#State private var isPresented = false
var body: some View {
Button {
vm.isPresented.toggle()
} label: {
Text("Open sheet")
}
.sheet(isPresented: $isPresented) {
SheetView(isPresented: $isPresented)
}
.sync($vm.isPresented, with: $isPresented)
// .onReceive(vm.$isPresented) { newValue in
// isPresented = newValue
// }
// .onChange(of: isPresented) { newValue in
// vm.isPresented = newValue
// }
}
}
^ Anything denoted with this is my assumptions and not real technical understanding - I am not a technical knowledgeable :/
Try running the code that's throwing the purple error asynchronously, for example, by using DispatchQueue.main.async or Task.
DispatchQueue.main.async {
// environment changing code comes here
}
Task {
// environment changing code comes here
}
Improved Solution of Rebel Developer
as a generic function.
Rebeloper solution
It helped me a lot.
1- Create extension for it:
extension View{
func sync<T:Equatable>(_ published:Binding<T>, with binding:Binding<T>)-> some View{
self
.onChange(of: published.wrappedValue) { published in
binding.wrappedValue = published
}
.onChange(of: binding.wrappedValue) { binding in
published.wrappedValue = binding
}
}
}
2- sync() ViewModel #Published var to local #State var
struct ContentView: View {
#EnvironmentObject var viewModel:ViewModel
#State var fullScreenType:FullScreenType?
var body: some View {
//..
}
.sync($viewModel.fullScreenType, with: $fullScreenType)
Currently I am trying to update the count in a SingleDay struct inside a Days class from from the TestScreen view.The SingleDay struct is also in an array in Days. The change in count should be reflected in the UpdatingArrayElements view. So far I am running into this error:
Left side of mutating operator isn't mutable: 'day' is a 'let' constant"
and I have absolutely no idea on how to resolve this issue. I would appreciate any help given that I am still a beginner in iOS development and still trying to get the hang of building more complex iOS apps, thanks!
import SwiftUI
struct SingleDay: Identifiable {
let id: String = UUID().uuidString
let day: Int
var count: Int
}
class Days: ObservableObject {
#Published var daysArray: [SingleDay] = []
init() {
daysArray.append(SingleDay(day: 1, count: 0))
}
}
struct UpdatingArrayElements: View {
#StateObject var days: Days = Days()
var body: some View {
NavigationView {
List {
ForEach(days.daysArray) { day in
HStack{
Text("Day: \(day.day)")
Text("Count: \(day.count)")
}
}
}
.navigationBarItems(trailing:
NavigationLink(destination: TestScreen(dayViewModel: days), label: {
Image(systemName: "arrow.right")
.font(.title)
})
)
}
}
}
struct TestScreen: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var dayViewModel: Days
var body: some View {
ZStack {
Color.green.ignoresSafeArea()
VStack {
ForEach(dayViewModel.daysArray) { day in
Text(String(day.day))
Button(action: {
day.count += 1
}, label: {
Text("Add count")
})
}
}
}
}
}
struct UpdatingArrayElements_Previews: PreviewProvider {
static var previews: some View {
UpdatingArrayElements()
}
}
here you go. now we are not using the identifiable so if you want you can also remove it.
so the real problem was that the day which is of type SingleDay is a struct not a class. so foreach give us let not vars , so in case of class object we can easily update properties because they are reference type, but in case of struct we can not do that, so it means day is another copy. so even if we can update the day of struct it will still not update the daysArray element.
struct TestScreen: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var dayViewModel: Days
var body: some View {
ZStack {
Color.green.ignoresSafeArea()
VStack {
ForEach(dayViewModel.daysArray.indices ,id : \.self) { index in
let day = dayViewModel.daysArray[index]
Text("\(day.count)")
Button(action: {
dayViewModel.daysArray[index].count = day.count + 1
print( day.count)
}, label: {
Text("Add count")
})
}
}
}
}
}
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?
I have two views ListView and DetailView
ListView:
#EnvironmentObject var userData: UserData
var body: some View {
VStack {
ForEach(userData.packs) { pack in
if pack.added {
NavigationLink(destination: DetailView(packIndex: self.userData.packs.firstIndex(where: { $0.id == pack.id })!)) {
MyRowViewDoesntMatter(pack: pack)
}
}
}
}
.padding(.horizontal)
}
DetailView:
#EnvironmentObject var userData: UserData
var packIndex: Int
VStack {
List {
VStack {
.... some Vies ... doesn't matter
.navigationBarItems(trailing:
THE PROBLEM IS HERE (BELOW)
Button(action: {
self.userData.packs[self.packIndex].added.toggle()
}) {
Image(systemName: self.userData.packs[self.packIndex].added ? "plus.circle.fill" : "plus.circle")
}
...
The problem is when I click on button in the navigationBarItems in DetailView. The "added" property of the "#EnvironmentObject var userData: UserData" is updated and the user's screen is going back (to the RowView). I fond out that the problem with EnvironmentObject, because the data is updated and View tries to rerender (?) that is why it pushes me back?
How to fix it? I want to stay at the DetailView screen after clicking the button.
P.S. I need to use EnvironmentObject type because then when I go back I need to see the results.
Thank you very much!
Here is possible approach (by introducing some kind of selection). As NavigationView does not allow to remove link from stack (as identifier of stacked navigation), probably also worth considering separate view model for DetailView to be applied into common container on finish editing.
Tested with Xcode 11.4 / iOS 13.4.
Some replication of your code, used for testing:
struct ListView: View {
#EnvironmentObject var userData: PushBackUserData
#State private var selectedPack: Pack? = nil
var body: some View {
NavigationView {
VStack {
ForEach(Array(userData.packs.enumerated()), id: \.element.id) { i, pack in
NavigationLink("Pack \(pack.id)", destination:
DetailView(pack: self.$selectedPack)
.onAppear {
self.selectedPack = pack
}
.onDisappear {
self.userData.packs[i].added = self.selectedPack?.added ?? false
}
).isHidden(!pack.added)
}
}
.padding(.horizontal)
}
}
}
struct DetailView: View {
#Binding var pack: Pack?
var body: some View {
VStack {
List {
VStack {
Text("Pack \(pack?.id ?? "<none>")")
}
}
.navigationBarItems(trailing:
Button(action: {
self.pack?.added.toggle()
}) {
Image(systemName: pack?.added ?? false ? "plus.circle.fill" : "plus.circle")
}
)
}
}
}
just convenient helper extension
extension View {
func isHidden(_ hidden: Bool) -> some View {
Group {
if hidden { self.hidden() }
else { self }
}
}
}
Currently using:
Xcode 11 Beta 5
Mac OSX Catalina Beta 5
Here is the code:
import SwiftUI
struct SwiftUIView : View {
var body: some View {
NavigationView {
NavigationLink(destination: Product()) {
Text("Click")
}
.navigationBarTitle(Text("Navigation"))
}
}
}
#if DEBUG
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
SwiftUIView()
}
}
#endif
And here is result:
When tapped or clicked on button, it should go to detail view, bit nothing is happening.
Notes:
The Landmark example project by apple, is also not working when tapped on the landmarks on home screen.
This website mentions that "Not sure if it is a bug or by design, in Beta 5 above code won't work"
https://fuckingswiftui.com/#navigationlink
It must be a bug. But as a workaround, when on the top view of a NavigationView, embed NavigationLink inside a VStack. The button will gain its proper style and "clickability".
struct SwiftUIView : View {
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: Product()) {
Text("Click")
}
}.navigationBarTitle(Text("Navigation"))
}
}
}
Works in Xcode(11.2)
struct MasterView: View {
#State var selection: Int? = nil
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DetailsView(), tag: 1, selection: $selection) {
Button("Press") {
self.selection = 1
}
}
}
}
}