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)
Related
In my app, I have a Screen with Toolbar and Main View.
VStack {
ToolbarView()
MainView()
}
Think of it like this:
Toolbar has its own View and ToolbarViewModel where we can select “Tools”
struct ToolbarView: View {
#StateObject private var VM = ToolbarViewModel()
var body: some View {
Text("Toolbar View")
}
}
#MainActor final class ToolbarViewModel: ObservableObject {
var selectedTool: Int = 1
func selectTool() {
//We select a new tool
selectedTool = 2
}
}
Main view has its own View and MainViewModel
struct MainView: View {
#StateObject private var VM = MainViewModel()
var body: some View {
Text("Main View")
}
}
#MainActor final class MainViewModel: ObservableObject {
var selectedTool: Int = 1
}
Now, when I tap a button in the ToolbarView and call a function in ToolbarViewModel to select a new tool, the tool must change in the MainViewModel too.
What would be the correct way of implementing this?
In the screen with the MainView and ToolbarView instances, create #StateObject(s) for both
#StateObject private var mainVM = MainViewModel()
#StateObject private var toolbarVM = ToolbarViewModel()
Then, create Observed objects in both your instances:
ToolbarView:
struct ToolbarView: View {
#ObservedObject var VM: ToolbarViewModel
var body: some View {
Text("Toolbar View")
}
}
MainView:
struct MainView: View {
#ObservedObject var VM: MainViewModel
var body: some View {
Text("Main View")
}
}
Then pass your objects in the screen than you created your instances:
VStack {
ToolbarView(VM: toolbarVM)
MainView(VM: mainVM)
}
Finally, whenever you make a change you can just listen to it like:
VStack {
ToolbarView(VM: toolbarVM)
MainView(VM: mainVM)
}
.onChange(of: toolbarVM.isDrawing) { newValue in {
mainVM.isDrawing = newValue
}
.onChange(of: mainVM.isDrawing) { newValue in {
toolbarVM.isDrawing = newValue
}
We don't use view model objects in SwiftUI. The View data struct is already the view model that SwiftUI uses to create/update/remove UIView objects automatically for us. The property wrappers give the struct reference semantics giving us the best of both worlds. You'll have to learn #State and #Binding and put the shared state in a parent View, then pass it down as a let for read access or #Bindng var for write access, e.g.
#State var tools = Tools()
...
VStack {
ToolbarView(tools: $tools)
MainView(tools: tools)
}
struct Tools {
var selectedTool: Int = 1
mutating func selectTool() {
//We select a new tool
selectedTool = 2
}
}
struct MainView: View {
let tools: Tools
var body: some View {
Text("Main View \(tools.selected)")
}
}
struct ToolbarView: View {
#Binding var tools: Tools
var body: some View {
Text("Toolbar View")
Button("Select") {
tools.selectTool()
}
}
}
i am learning swiftui now and I am newbie for stackoverflow, I find a question,this is my code. I want to change the #State nopubName in sink ,but it's not work,the print is always "Nimar", I don't know why
struct ContentView: View {
#State var nopubName: String = "Nimar"
private var cancellable: AnyCancellable?
var stringSubject = PassthroughSubject<String, Never>()
init() {
cancellable = stringSubject.sink(receiveValue: handleValue(_:))
}
func handleValue(_ value: String) {
print("handleValue: '\(value)'")
self.nopubName = value
print("in sink "+nopubName)
}
var body: some View {
VStack {
Text(self.nopubName)
.font(.title).bold()
.foregroundColor(.red)
Spacer()
Button("sink"){
stringSubject.send("World")
print(nopubName)
}
}
}
}
You should only access a state property from inside the view’s body, or from methods called by it.
https://developer.apple.com/documentation/swiftui/state
You can get that functionality working in an ObservableObject and update an #Published To keep the UI updated
https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app
You don't need to use Combine, If you are within the View, you can change the value of #State variables directly
struct ContentView: View {
#State var nopubName: String = "Nimar"
var body: some View {
VStack {
Text(self.nopubName)
.font(.title).bold()
.foregroundColor(.red)
Spacer()
Button("sink"){
nopubName = "World"
}
}
}
}
I have a MVVM SwiftUI app that will navigate to another view based on the value of a #Published property of a view model:
class ViewModel: ObservableObject {
#Published public var showView = false
func doShowView() {
showView = true
}
}
struct MyView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
NavigationView {
MySubView().environmentObject(viewModel)
}
}
}
struct MySubView: View {
#EnvironmentObject private var viewModel: ViewModel
var body: some View {
VStack {
Button(action: {
viewModel.doShowView()
}) {
Text("Button")
}
NavigationLink(
destination: SomeOtherView(),
isActive: $viewModel.showView,
label: {
EmptyView()
})
}
}
}
The problem is sometimes when I run the app it will work only every other time and sometimes it works perfectly as expected.
The cause seems to be that sometimes when the property is set in the view model (in doShowView()) SwiftUI will immediately render my view with the old value of showView and in the working case the view is rendered on the next event cycle with the updated value.
Is this a feature (due to the fact #Published is calling objectWillChange under the hood and the view is rendering due to that) or a bug?
If it is a feature (and I just happen to get lucky when it works as I want it to) what is the best way to guarantee it renders my view after the new value is set?
Note this is only a simple example, I cannot use a #State variable in the button action since in the real code the doShowView() method may or may not set the showView property in the view model.
The issue here is that SwiftUI creates the SomeOtherView beforehand. Then, this view is not related with the viewModel in any way, so it's not re-created when viewModel.showView changes.
A possible solution is to make SomeOtherView depend on the viewModel - e.g. by explicitly injecting the environmentObject:
struct MySubView: View {
#EnvironmentObject private var viewModel: ViewModel
var body: some View {
VStack {
Button(action: {
viewModel.doShowView()
}) {
Text("Button")
}
NavigationLink(
destination: SomeOtherView().environmentObject(viewModel),
isActive: $viewModel.showView,
label: {
EmptyView()
}
)
}
}
}
I came upon a working solution. I did add a #State variable and set it by explictly watching for changes of showView:
class ViewModel: ObservableObject {
#Published public var showView = false
var disposables = Set<AnyCancellable>()
func doShowView() {
showView = true
}
}
struct MyView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
NavigationView {
MySubView().environmentObject(viewModel)
}
}
}
struct MySubView: View {
#EnvironmentObject private var viewModel: ViewModel
#State var showViewLink = false
var body: some View {
VStack {
Button(action: {
viewModel.doShowView()
}) {
Text("Button")
}
NavigationLink(
destination: SomeOtherView(),
isActive: $showViewLink,
label: {
EmptyView()
})
}
.onAppear {
viewModel.$showView
.sink(receiveValue: { showView in
showViewLink = showView
})
.store(in: &viewModel.disposables)
}
}
}
I am struggle with understanding about why i have to give Popup view dependency named vm while calling this view since it is observable
struct ContentView: View {
#State private var showPopup1 = false
var body: some View {
VStack {
Button(action: { withAnimation { self.showPopup1.toggle()}}){
Text("showPopup1") }
Text("title")
DetailView() /// this line shows error
}
}
}
struct DetailView:View {
#ObservedObject var vm:ViewModel
var body : some View {
Text("value from VM")
}
}
class ViewModel: ObservableObject {
#Published var title:String = ""
}
You have to set your vm property when you init your View. Which is the usual way.
struct ContentView: View {
#State private var showPopup1 = false
var body: some View {
VStack {
Button(action: { withAnimation { self.showPopup1.toggle()}}){
Text("showPopup1") }
Text("title")
DetailView(vm: ViewModel()) // Initiate your ViewModel() and pass it as DetailView() parameter
}
}
}
struct DetailView:View {
var vm: ViewModel
var body : some View {
Text("value from VM")
}
}
class ViewModel: ObservableObject {
#Published var title:String = ""
}
Or you could use #EnvironmentObject. You have to pass an .environmentObject(yourObject) to the view where you want to use yourObject, but again you'll have to initialize it before passing it.
I'm not sure it's the good way to do it btw, as an environmentObject can be accessible to all childs view of the view you declared the .environmentObject on, and you usually need one ViewModel for only one View.
struct ContentView: View {
#State private var showPopup1 = false
var body: some View {
VStack {
Button(action: { withAnimation { self.showPopup1.toggle()}}){
Text("showPopup1") }
Text("title")
DetailView().environmentObject(ViewModel()) // Pass your ViewModel() as an environmentObject
}
}
}
struct DetailView:View {
#EnvironmentObject var vm: ViewModel // you can now use your vm, and access it the same say in all childs view of DetailView
var body : some View {
Text("value from VM")
}
}
class ViewModel: ObservableObject {
#Published var title:String = ""
}
I wanted to create quiet a simple app on watchOS 6, but after Apple has changed the ObjectBindig in Xcode 11 beta 5 my App does not run anymore. I simply want to synchronize data between two Views.
So I have rewritten my App with the new #Published, but I can't really set it up:
class UserInput: ObservableObject {
#Published var score: Int = 0
}
struct ContentView: View {
#ObservedObject var input = UserInput()
var body: some View {
VStack {
Text("Hello World\(self.input.score)")
Button(action: {self.input.score += 1})
{
Text("Adder")
}
NavigationLink(destination: secondScreen()) {
Text("Next View")
}
}
}
}
struct secondScreen: View {
#ObservedObject var input = UserInput()
var body: some View {
VStack {
Text("Button has been pushed \(input.score)")
Button(action: {self.input.score += 1
}) {
Text("Adder")
}
}
}
}
Your code has a couple of errors:
1) You didn't put your ContentView in a NavigationView, so the navigation between the two views never happened.
2) You used data binding in a wrong way. If you need the second view to rely on some state belonging to the first view you need to pass a binding to that state to the second view. Both in your first view and in your second view you had an #ObservedObject created inline:
#ObservedObject var input = UserInput()
so, the first view and the second one worked with two totally different objects. Instead, you are interested in sharing the score between the views. Let the first view own the UserInput object and just pass a binding to the score integer to the second view. This way both the views will work on the same value (you can copy paste the code below and try yourself).
import SwiftUI
class UserInput: ObservableObject {
#Published var score: Int = 0
}
struct ContentView: View {
#ObservedObject var input = UserInput()
var body: some View {
NavigationView {
VStack {
Text("Hello World\(self.input.score)")
Button(action: {self.input.score += 1})
{
Text("Adder")
}
NavigationLink(destination: secondScreen(score: self.$input.score)) {
Text("Next View")
}
}
}
}
}
struct secondScreen: View {
#Binding var score: Int
var body: some View {
VStack {
Text("Button has been pushed \(score)")
Button(action: {self.score += 1
}) {
Text("Adder")
}
}
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
If you really need it you can even pass the entire UserInput object to the second view:
import SwiftUI
class UserInput: ObservableObject {
#Published var score: Int = 0
}
struct ContentView: View {
#ObservedObject var input = UserInput() //please, note the difference between this...
var body: some View {
NavigationView {
VStack {
Text("Hello World\(self.input.score)")
Button(action: {self.input.score += 1})
{
Text("Adder")
}
NavigationLink(destination: secondScreen(input: self.input)) {
Text("Next View")
}
}
}
}
}
struct secondScreen: View {
#ObservedObject var input: UserInput //... and this!
var body: some View {
VStack {
Text("Button has been pushed \(input.score)")
Button(action: {self.input.score += 1
}) {
Text("Adder")
}
}
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
I tried a lot of different approaches on how to pass data from one view to another and came up with a solution that fits for simple and complex views / view models.
Version
Apple Swift version 5.3.1 (swiftlang-1200.0.41 clang-1200.0.32.8)
This solution works with iOS 14.0 upwards, because you need the .onChange() view modifier. The example is written in Swift Playgrounds. If you need an onChange like modifier for lower versions, you should write your own modifier.
Main View
The main view has a #StateObject viewModel handling all of the views logic, like the button tap and the "data" (testingID: String) -> Check the ViewModel
struct TestMainView: View {
#StateObject var viewModel: ViewModel = .init()
var body: some View {
VStack {
Button(action: { self.viewModel.didTapButton() }) {
Text("TAP")
}
Spacer()
SubView(text: $viewModel.testingID)
}.frame(width: 300, height: 400)
}
}
Main View Model (ViewModel)
The viewModel publishes a testID: String?. This testID can be any kind of object (e.g. configuration object a.s.o, you name it), for this example it is just a string also needed in the sub view.
final class ViewModel: ObservableObject {
#Published var testingID: String?
func didTapButton() {
self.testingID = UUID().uuidString
}
}
So by tapping the button, our ViewModel will update the testID. We also want this testID in our SubView and if it changes, we also want our SubView to recognize and handle these changes. Through the ViewModel #Published var testingID we are able to publish changes to our view. Now let's take a look at our SubView and SubViewModel.
SubView
So the SubView has its own #StateObject to handle its own logic. It is completely separated from other views and ViewModels. In this example the SubView only presents the testID from its MainView. But remember, it can be any kind of object like presets and configurations for a database request.
struct SubView: View {
#StateObject var viewModel: SubviewModel = .init()
#Binding var test: String?
init(text: Binding<String?>) {
self._test = text
}
var body: some View {
Text(self.viewModel.subViewText ?? "no text")
.onChange(of: self.test) { (text) in
self.viewModel.updateText(text: text)
}
.onAppear(perform: { self.viewModel.updateText(text: test) })
}
}
To "connect" our testingID published by our MainViewModel we initialize our SubView with a #Binding. So now we have the same testingID in our SubView. But we don't want to use it in the view directly, instead we need to pass the data into our SubViewModel, remember our SubViewModel is a #StateObject to handle all the logic. And we can't pass the value into our #StateObject during view initialization. Also if the data (testingID: String) changes in our MainViewModel, our SubViewModel should recognize and handle these changes.
Therefore we are using two ViewModifiers.
onChange
.onChange(of: self.test) { (text) in
self.viewModel.updateText(text: text)
}
The onChange modifier subscribes to changes in our #Binding property. So if it changes, these changes get passed to our SubViewModel. Note that your property needs to be Equatable. If you pass a more complex object, like a Struct, make sure to implement this protocol in your Struct.
onAppear
We need onAppear to handle the "first initial data" because onChange doesn't fire the first time your view gets initialized. It is only for changes.
.onAppear(perform: { self.viewModel.updateText(text: test) })
Ok and here is the SubViewModel, nothing more to explain to this one I guess.
class SubviewModel: ObservableObject {
#Published var subViewText: String?
func updateText(text: String?) {
self.subViewText = text
}
}
Now your data is in sync between your MainViewModel and SubViewModel and this approach works for large views with many subviews and subviews of these subviews and so on. It also keeps your views and corresponding viewModels enclosed with high reusability.
Working Example
Playground on GitHub:
https://github.com/luca251117/PassingDataBetweenViewModels
Additional Notes
Why I use onAppear and onChange instead of only onReceive: It appears that replacing these two modifiers with onReceive leads to a continuous data stream firing the SubViewModel updateText multiple times. If you need to stream data for presentation, it could be fine but if you want to handle network calls for example, this can lead to problems. That's why I prefer the "two modifier approach".
Personal Note: Please don't modify the StateObject outside the corresponding view's scope. Even if it is somehow possible, it is not what its meant for.
My question is still related to how to pass data between two views but I have a more complicated JSON data set and I am running into problems both with the passing the data and with it's initialization. I have something that works but I am sure it is not correct. Here is the code. Help!!!!
/ File: simpleContentView.swift
import SwiftUI
// Following is the more complicated #ObservedObject (Buddy and class Buddies)
struct Buddy : Codable, Identifiable, Hashable {
var id = UUID()
var TheirNames: TheirNames
var dob: String = ""
var school: String = ""
enum CodingKeys1: String, CodingKey {
case id = "id"
case Names = "Names"
case dob = "dob"
case school = "school"
}
}
struct TheirNames : Codable, Identifiable, Hashable {
var id = UUID()
var first: String = ""
var middle: String = ""
var last: String = ""
enum CodingKeys2: String, CodingKey {
case id = "id"
case first = "first"
case last = "last"
}
}
class Buddies: ObservableObject {
#Published var items: [Buddy] {
didSet {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(items) {UserDefaults.standard.set(encoded, forKey: "Items")}
}
}
#Published var buddy: Buddy
init() {
if let items = UserDefaults.standard.data(forKey: "Items") {
let decoder = JSONDecoder()
if let decoded = try? decoder.decode([Buddy].self, from: items) {
self.items = decoded
// ??? How to initialize here
self.buddy = Buddy(TheirNames: TheirNames(first: "c", middle: "r", last: "c"), dob: "1/1/1900", school: "hard nocks")
return
}
}
// ??? How to initialize here
self.buddy = Buddy(TheirNames: TheirNames(first: "c", middle: "r", last: "c"), dob: "1/1/1900", school: "hard nocks")
self.items = []
}
}
struct simpleContentView: View {
#Environment(\.presentationMode) var presentationMode
#State private var showingSheet = true
#ObservedObject var buddies = Buddies()
var body: some View {
VStack {
Text("Simple View")
Button(action: {self.showingSheet.toggle()}) {Image(systemName: "triangle")
}.sheet(isPresented: $showingSheet) {
simpleDetailView(buddies: self.buddies, item: self.buddies.buddy)}
}
}
}
struct simpleContentView_Previews: PreviewProvider {
static var previews: some View {
simpleContentView()
}
}
// End of File: simpleContentView.swift
// This is in a separate file: simpleDetailView.swift
import SwiftUI
struct simpleDetailView: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var buddies = Buddies()
var item: Buddy
var body: some View {
VStack {
Text(/*#START_MENU_TOKEN#*/"Hello, World!"/*#END_MENU_TOKEN#*/)
Text("First Name = \(item.TheirNames.first)")
Button(action: {self.presentationMode.wrappedValue.dismiss()}){ Text("return"); Image(systemName: "gobackward")}
}
}
}
// ??? Correct way to make preview call
struct simpleDetailView_Previews: PreviewProvider {
static var previews: some View {
// ??? Correct way to call here
simpleDetailView(item: Buddy(TheirNames: TheirNames(first: "", middle: "", last: ""), dob: "", school: "") )
}
}
// end of: simpleDetailView.swift
Using directly #State variable will help you to achieve this, but if you want to sync that variable for both the screens using view model or #Published, this is what you can do. As the #State won't be binded to the #Published property. To achieve this follow these steps.
Step1: - Create a delegate to bind the value on pop or disappearing.
protocol BindingDelegate {
func updateOnPop(value : Int)
}
Step 2:- Follow the code base for Content View
class UserInput: ObservableObject {
#Published var score: Int = 0
}
struct ContentView: View , BindingDelegate {
#ObservedObject var input = UserInput()
#State var navIndex : Int? = nil
var body: some View {
NavigationView {
VStack {
Text("Hello World\(self.input.score)")
Button(action: {self.input.score += 1}) {
Text("Adder")
}
ZStack {
NavigationLink(destination: secondScreen(score: self.$input.score,
del: self, navIndex: $navIndex),
tag: 1, selection: $navIndex) {
EmptyView()
}
Button(action: {
self.navIndex = 1
}) {
Text("Next View")
}
}
}
}
}
func updateOnPop(value: Int) {
self.input.score = value
}
}
Step 3: Follow these steps for secondScreen
final class ViewModel : ObservableObject {
#Published var score : Int
init(_ value : Int) {
self.score = value
}
}
struct secondScreen: View {
#Binding var score: Int
#Binding var navIndex : Int?
#ObservedObject private var vm : ViewModel
var delegate : BindingDelegate?
init(score : Binding<Int>, del : BindingDelegate, navIndex : Binding<Int?>) {
self._score = score
self._navIndex = navIndex
self.delegate = del
self.vm = ViewModel(score.wrappedValue)
}
private var btnBack : some View { Button(action: {
self.delegate?.updateOnPop(value: self.vm.score)
self.navIndex = nil
}) {
HStack {
Text("Back")
}
}
}
var body: some View {
VStack {
Text("Button has been pushed \(vm.score)")
Button(action: {
self.vm.score += 1
}) {
Text("Adder")
}
}
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: btnBack)
}
}