How to change #EnvironmentObject from class, and not a View - swiftui

I have a TabView with two child views - PageOneView and PageTwoView. I also have an #EnvironmentObject Data initialized in SceneDelegate. I also have a nested child for each of the PageOneView and PageTwoView. For PageTwoView the nested child is also a view and inside it I can access and modify #EnvironmentObject, and all of the top level views (PageOneView and PageTwoView) reflect the change. But for PageOneView the nested child is a class, not a view, and when I try to modify it within the child of PageOneView I get an error: Thread 1: Fatal error: No ObservableObject of type Data found. A View.environmentObject(_:) for Data may be missing as an ancestor of this view. Is there a way to change #EnvironmentObject from within nested child that is not a View, but a class?
struct AppView: View {
#EnvironmentObject var data: Data
var body: some View {
TabView {
PageOneView().environmentObject(data)
.tabItem {
Text("PageOne")
}
PageTwoView().environmentObject(data)
.tabItem {
Text("PageTwo")
}
}
}
}
struct PageTwoView: View {
#EnvironmentObject var data: Data
var body: some View {
VStack {
Text("Data: \(data.Name)")
ChildView()//.environmentObject(data)
}
}
}
struct ChildView: View {
#EnvironmentObject var data: Data
var body: some View {
VStack {
Text("Data: \(data.Name)")
Button(action: {
self.updateData()
}) {
Text("Update Data")
}
}
}
func updateData()
{
self.data.Name = "Updated Name"
}
}
struct PageOneView: View {
#EnvironmentObject var data: Data
var body: some View {
VStack {
Text("Data: \(data.Name)")
Button(action: {
self.updateData()
}) {
Text("Update Data")
}
}
}
func updateData()
{
var child = Child()
child.updateData()
}
}
class Child: NSObject{
#EnvironmentObject var data: Data
func updateData()
{
data.Name = "Updated Name Again"
}
}

Here is corrected part (you don't need #EnvironmentObject wrapper in class, it is designed for View, your Data is a class itself, so you can pass its instance by reference to your Child)
func updateData()
{
var child = Child(data: self.data)
child.updateData()
}
}
class Child: NSObject{
let data: Data
init(data: Data) {
self.data = data
}
func updateData()
{
data.Name = "Updated Name Again"
}
}

Related

SwiftUI: Cannot find type in scope

I'm having trouble coming up a good way to ask this question, so I'll instead show a simple example. I have a model, an #ObservableObject, that contains a struct:
class MyModel: ObservableObject {
#Published var allData: [TheData] = []
struct TheData: Hashable {
let thePosition: Int
let theChar: Character
}
func initState() {
let allChars = Array("abd")
for (index, element) in allChars.enumerated() {
allData.append(TheData(thePosition: index, theChar: element))
}
}
}
In my view, I'm attempting to reach the model from two different structs (as a result of an annoying The compiler is unable to type-check this expression in reasonable time..), but I get an error:
struct ContentView: View {
#StateObject var theModel = MyModel()
var body: some View {
VStack {
Image(systemName: "globe")
Text("Hello, world!")
HStack {
ForEach(theModel.allData, id: \.self) { theElement in
letterAcross(myLetter: theElement)
}
}
}
.onAppear {
theModel.initState()
}
.environmentObject(theModel)
}
}
struct letterAcross: View {
#EnvironmentObject var theModel: MyModel
var myLetter: TheData // <----- the ERROR is here
var body: some View {
HStack {
Text(String(myLetter.theChar))
}
}
}
The error is Cannot find type TheData in scope. It appears I am somehow messing up the #StateObject and #EnvironmentObject. What is the correct way to do this?
It's a nested type, so it's MyModel.TheData:
var myLetter: MyModel.TheData

SwiftUI: Must an ObservableObject be passed into a View as an EnvironmentObject?

If I create an ObservableObject with a #Published property and inject it into a SwifUI view with .environmentObject(), the view responds to changes in the ObservableObject as expected.
class CounterStore: ObservableObject {
#Published private(set) var counter = 0
func increment() {
counter += 1
}
}
struct ContentView: View {
#EnvironmentObject var store: CounterStore
var body: some View {
VStack {
Text("Count: \(store.counter)")
Button(action: { store.increment() }) {
Text("Increment")
}
}
}
}
Tapping on "Increment" will increase the count.
However, if I don't use the EnvironmentObject and instead pass the store instance into the view, the compiler does not complain, the store method increment() is called when the button is tapped, but the count in the View does not update.
struct ContentViewWithStoreAsParameter: View {
var store: CounterStore
var body: some View {
VStack {
Text("Count: \(store.counter) (DOES NOT UPDATE)")
Button(action: { store.increment() }) {
Text("Increment")
}
}
}
}
Here's how I'm calling both Views:
#main
struct testApp: App {
var store = CounterStore()
var body: some Scene {
WindowGroup {
VStack {
ContentView().environmentObject(store) // works
ContentViewWithStoreAsParameter(store: store) // broken
}
}
}
}
Is there a way to pass an ObservableObject into a View as a parameter? (Or what magic is .environmentalObject() doing behind the scenes?)
It should be observed somehow, so next works
struct ContentViewWithStoreAsParameter: View {
#ObservedObject var store: CounterStore
//...
You can pass down your store easily as #StateObject:
#main
struct testApp: App {
#StateObject var store = CounterStore()
var body: some Scene {
WindowGroup {
VStack {
ContentView().environmentObject(store) // works
ContentViewWithStoreAsParameter(store: store) // also works
}
}
}
}
struct ContentViewWithStoreAsParameter: View {
#StateObject var store: CounterStore
var body: some View {
VStack {
Text("Count: \(store.counter)") // now it does update
Button(action: { store.increment() }) {
Text("Increment")
}
}
}
}
However, the store should normally only be available for the views that need it, why this solution would make the most sense in this context:
struct ContentView: View {
#StateObject var store = CounterStore()
var body: some View {
VStack {
Text("Count: \(store.counter)")
Button(action: { store.increment() }) {
Text("Increment")
}
}
}
}

SwiftUI - Subclassed viewModel doesn't trigger view refresh

I have this situation where I have a a BaseView containing some common elements and a BaseViewModel containing some common functions, but when its #Published var get updated no BaseView refresh occurs.
The setup is this:
class BaseViewModel: ObservableObject {
#Published var overlayView: AnyView = EmptyView().convertToAnyView()
func forceViewRefresh() {
self.objectWillChange.send()
}
func setOverlayView(overlayView: AnyView) {
self.overlayView = overlayView
}
}
This view model subclasses BaseViewModel:
class FirstViewModel: BaseViewModel {
func showOverlayView() {
self.setOverlayView(overlayView: OverlayView().convertToAnyView())
}
}
also I have a BaseView where I use the overlayView
struct BaseView<Content: View>: View {
let content: Content
#ObservedObject var viewModel = BaseViewModel()
init(content: () -> Content) {
self.content = content()
}
var body: some View {
ZStack {
Color.green.edgesIgnoringSafeArea(.vertical)
content
viewModel.overlayView
}
}
}
The first view that gets displayed is FirstView, which conforms to a BaseViewProtocol and has a FirstViewModel that extends BaseViewModel.
struct FirstView: BaseViewProtocol {
#ObservedObject var viewModel = FirstViewModel()
var body: some View {
BaseView() {
Button("Show overlay") {
viewModel.showOverlayView()
}
}
}
}
Clicking the Show overlay button in First View calls the showOverlayView() func on FirstViewModel which in turn calls setOverlayView on the BaseViewModel. The value of overlayView in BaseViewModel changes as expected, but no view refresh on FirstView is called.
What am I doing wrong?
Thanks a lot.
I have just tested this code sample and works fine on Xcode 12 beta 6 & iOS 14 beta 8
struct ContentView: View {
#StateObject private var viewModel = FirstViewModel()
var body: some View {
ZStack {
Button(action: { viewModel.showOverlayView() }) {
Text("Press")
}
viewModel.overlayView
}
}
}
class BaseViewModel: ObservableObject {
#Published var overlayView: AnyView = AnyView(EmptyView())
func forceViewRefresh() {
self.objectWillChange.send()
}
func setOverlayView(overlayView: AnyView) {
self.overlayView = overlayView
}
}
class FirstViewModel: BaseViewModel {
func showOverlayView() {
self.setOverlayView(
overlayView: AnyView(
Color.blue
.opacity(0.2)
.allowsHitTesting(false)
)
)
}
}
Generally in SwiftUI you don't create views in outside the body. The view creation should be left to SwiftUI - instead you can define some other controls telling SwiftUI how and when to create a view.
Here is a simplified demo how to present different overlays for different views.
You can create a basic OverlayView:
enum OverlayType {
case overlay1, overlay2
}
struct OverlayView: View {
let overlayType: OverlayType
#ViewBuilder
var body: some View {
if overlayType == .overlay1 {
Text("Overlay1") // can be replaced with any view you want
}
if overlayType == .overlay2 {
Text("Overlay1")
}
}
}
and use it in your BaseView (if overlayType is nil the overlay will not be shown):
struct BaseView<Content>: View where Content: View {
let overlayType: OverlayType?
let content: () -> Content
var body: some View {
ZStack {
Color.green.edgesIgnoringSafeArea(.vertical)
content()
if overlayType != nil {
OverlayView(overlayType: overlayType!)
}
}
}
}
Now in the ContentView you can use the BaseView and specify its OverlayType.
struct ContentView: View {
#State var overlayType: OverlayType?
var body: some View {
BaseView(overlayType: overlayType) {
Button("Show overlay") {
overlayType = .overlay1
}
}
}
}
Some considerations:
For simplicity I used #State variables to control overlays. If there are other use cases for your ViewModels you may want to move the logic there.
Note that instead of AnyView it's preferred to use #ViewBuilder.
Also if you want to observe an ObservableObject inside a view, you need to use #ObservedObject, not #ObservableObject.

Bidirectional binding with SwiftUI and Combine

I'm trying to work out how I can correctly pass an object or a set of values between two ViewModels in a parent-child relationship so that when the child ViewModel is updated the change bubbles back up to the parent.
This is pretty simple when just using SwiftUI views and binding directly to the stores but I wanted to keep my business logic for field validation and so on separate from the SwiftUI views.
The code below shows the child updating (as expected) when the parent gets updated, but I need to somehow pass the changed values in the child back up to the parent. I'm very new to mobile app development and still learning so I'm sure I'm missing something quite simple.
import SwiftUI
import Combine
struct Person: Hashable {
var givenName: String
var familyName: String
}
// my person store - in the real app it's backed by coredata
class PersonStore: ObservableObject {
#Published var people: [Person] = [
Person(
givenName: "Test",
familyName: "Person"
)
]
static let shared = PersonStore()
}
// app entrypoint
struct PersonView: View {
#ObservedObject var viewModel: PersonView_ViewModel = PersonView_ViewModel()
var body: some View {
NavigationView {
VStack {
List(viewModel.people.indices, id: \.self) { idx in
NavigationLink(destination: PersonDetailView(viewModel: PersonDetailView_ViewModel(personIndex: idx))) {
Text(self.viewModel.people[idx].givenName)
}
}
}
}
}
}
class PersonView_ViewModel: ObservableObject {
#Published var people: [Person] = PersonStore.shared.people
}
// this is the detail view
struct PersonDetailView: View {
#ObservedObject var viewModel: PersonDetailView_ViewModel
var body: some View {
Form {
Section(header: Text("Parent View")) {
VStack {
TextField("Given Name", text: self.$viewModel.person.givenName)
Divider()
TextField("Family Name", text: self.$viewModel.person.familyName)
}
}
PersonBasicDetails(viewModel: PersonBasicDetails_ViewModel(person: viewModel.person))
}
}
}
// viewmodel associated with detail view
class PersonDetailView_ViewModel: ObservableObject {
#Published var person: Person
init(personIndex: Int) {
self.person = PersonStore.shared.people[personIndex]
}
}
// this is the child view - in the real app there are multiple sections which are conditionally rendered
struct PersonBasicDetails: View {
#ObservedObject var viewModel: PersonBasicDetails_ViewModel
var body: some View {
Section(header: Text("Child View")) {
VStack {
TextField("Given Name", text: self.$viewModel.person.givenName)
Divider()
TextField("Family Name", text: self.$viewModel.person.familyName)
}
}
}
}
class PersonBasicDetails_ViewModel: ObservableObject {
#Published var person: Person
init(person: Person) {
self.person = person
}
}
struct PersonView_Previews: PreviewProvider {
static var previews: some View {
PersonView()
}
}
In most SwiftUI TextField examples around the web the binding is provided by utilizing a #State variable which creates an instance of Binding for you.
However, you can also create a custom binding using the Binding constructor. Here's an example of what that looks like:
TextField(
"Given Name",
text: Binding(
get: { self.$viewModel.person.givenName },
set: { self.$viewModel.person.givenName = $0 }))
If you want two way works, not only you need to publish, also you have to use binding for upward.
struct Person: Hashable {
var givenName: String
var familyName: String
}
// my person store - in the real app it's backed by coredata
class PersonStore: ObservableObject {
#Published var people: [Person] = [
Person(givenName: "Test",familyName: "Person")
]
static let shared = PersonStore()
}
// app entrypoint
struct PersonView: View {
#ObservedObject var viewModel: PersonView_ViewModel = PersonView_ViewModel()
var body: some View {
NavigationView {
VStack {
List(viewModel.people.indices, id: \.self) { idx in
NavigationLink(destination: PersonDetailView(viewModel: PersonDetailView_ViewModel(person: self.$viewModel.people , index: idx ))) {
Text(self.viewModel.people[idx].givenName)
}
}
}
}
}
}
class PersonView_ViewModel: ObservableObject {
#Published var people: [Person] = PersonStore.shared.people
}
// this is the detail view
struct PersonDetailView: View {
#ObservedObject var viewModel: PersonDetailView_ViewModel
var body: some View {
Form {
Section(header: Text("Parent View")) {
VStack {
TextField("Given Name", text: self.viewModel.person.givenName)
Divider()
TextField("Family Name", text: self.viewModel.person.familyName)
}
}
PersonBasicDetails(viewModel: PersonBasicDetails_ViewModel(person: viewModel.person))
}
}
}
// viewmodel associated with detail view
class PersonDetailView_ViewModel: ObservableObject {
#Published var person: Binding<Person>
init(person: Binding<[Person]> ,index: Int) {
self.person = person[index]
}
}
// this is the child view - in the real app there are multiple sections which are conditionally rendered
struct PersonBasicDetails: View {
#ObservedObject var viewModel: PersonBasicDetails_ViewModel
var body: some View {
Section(header: Text("Child View")) {
VStack {
TextField("Given Name", text: self.viewModel.person.givenName)
Divider()
TextField("Family Name", text: self.viewModel.person.familyName)
}
}
}
}
class PersonBasicDetails_ViewModel: ObservableObject {
#Published var person: Binding<Person>
init(person: Binding<Person>) {
self.person = person //person
}
}
struct PersonView_Previews: PreviewProvider {
static var previews: some View {
PersonView()
}
}

Passing data between two views

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)
}
}