#Published variable causes crash without didSet - swiftui

I have an ObservableObject view model with a #Published variable that's an enum indicating the status of the viewModel:
class BatchesViewModel: ObservableObject {
#Published private var lookupStatus: BatchLookupStatus = .none
...
}
enum BatchLookupStatus {
case none
case lookingUp(searchText: String)
case error(errorMessage: String)
case validated(batch: Batch)
}
I set the status when there's an error, or there's a validation, etc. I set the status back to .none when the error is dismissed.
Everything works fine, except I get a EXC_BAD_ACCESS crash randomly.
I wanted to see what the status was when that happens, so I put a print statement when the status is set:
#Published private var lookupStatus: BatchLookupStatus = .none {
didSet {
print(_lookupStatus)
}
}
I noticed that with that print statement, the crash no longer happens. In fact, if I just leave an empty didSet, it prevents the crash as well:
#Published private var lookupStatus: BatchLookupStatus = .none { didSet {} }
As soon as I remove the didSet, it crashes again.
What in the world is going on?

I assume the reason is not in the provided model, because as tested it all works as expected - I mean with Published-Refresh flow.
Here is an example of your model usage I did:
import SwiftUI
import Combine
class BatchesViewModel: ObservableObject {
#Published var lookupStatus: BatchLookupStatus = .none
}
enum BatchLookupStatus {
case none
case lookingUp(searchText: String)
case error(errorMessage: String)
}
struct TestPublishCrashed: View {
#ObservedObject var viewModel = BatchesViewModel()
var body: some View {
VStack {
Button(action: {
self.viewModel.lookupStatus = .error(errorMessage: "Search failed")
}) {
Text("Emulate Error").padding().background(Color.yellow)
}.padding()
Button(action: {
self.viewModel.lookupStatus = .lookingUp(searchText: "hello world")
}) {
Text("Emulate Query").padding().background(Color.yellow)
}.padding()
status.padding()
}
}
var status: some View {
switch viewModel.lookupStatus {
case .none:
return Text("No status")
case .error(let error):
return Text("Got error: \(error)")
case .lookingUp(let query):
return Text("Searching: \(query)")
}
}
}
struct TestPublishCrashed_Previews: PreviewProvider {
static var previews: some View {
TestPublishCrashed()
}
}

Related

Updating SwiftUI from HealthKit Query

I want to output the variable 'healthStore.valueTest' via ContentView in SwiftUI.
The class healtStore is structured as follows:
class HealthStore {
var healthStore: HKHealthStore?
var query: HKStatisticsQuery?
var valueTest: HKQuantity?
init() {
if HKHealthStore.isHealthDataAvailable() {
healthStore = HKHealthStore()
}
}
func calculateBloodPressureSystolic() {
guard let bloodPressureSystolic = HKObjectType.quantityType(forIdentifier: .bloodPressureSystolic) else {
// This should never fail when using a defined constant.
fatalError("*** Unable to get the bloodPressure count ***")
}
// let startDate = Calendar.current.date(byAdding: .day, value: -7, to: Date())
// let anchorDate = Date.mondayAt12AM()
// let daily = DateComponents(day: 1)
// let predicate = HKQuery.predicateForSamples(withStart: startDate, end: Date(), options: .strictStartDate)
query = HKStatisticsQuery(quantityType: bloodPressureSystolic,
quantitySamplePredicate: nil,
options: .discreteAverage) {
query, statistics, error in
DispatchQueue.main.async{
self.valueTest = statistics?.averageQuantity()
}
}
healthStore!.execute(query!)
}
}
ContentView is built as follows:
import SwiftUI
import HealthKit
struct ContentView: View {
private var healthStore: HealthStore?
init() {
healthStore = HealthStore()
}
var body: some View {
Text("Hello, world!")
.padding().onAppear(){
if let healthStore = healthStore {
healthStore.requestAuthorization { success in
if success {
healthStore.calculateBloodPressureSystolic()
print(healthStore.query)
print(healthStore.valueTest)
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The value for the variable self.valueTest is assigned in the process DispatchQueue.main.async. Nevertheless, I get only a nil back when querying via ContentView.
You could set up your HealthStore class and use it as an EnvironmentObject. Assuming your app uses the SwiftUI lifecycle you can inject HealthStore into the environment in the #main entry point of your app.
import SwiftUI
#main
struct NameOfYourHeathApp: App {
let healthStore = HealthStore()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(healthStore)
}
}
}
Change your HealthStore class to this. (I removed your commented out code in my sample below)
import HealthKit
class HealthStore: ObservableObject {
var healthStore: HKHealthStore?
var query: HKStatisticsQuery?
var valueTest: HKQuantity?
init() {
if HKHealthStore.isHealthDataAvailable() {
healthStore = HKHealthStore()
}
}
// I moved the HealthStore conditional check out of your View logic
// and placed it here instead.
func setUpHealthStore() {
let typesToRead: Set = [
HKQuantityType.quantityType(forIdentifier: .bloodPressureSystolic)!
]
// I left the `toShare` as nil as I did not dig into adding bloodpressure reading to HealthKit.
healthStore?.requestAuthorization(toShare: nil, read: typesToRead, completion: { success, error in
if success {
self.calculateBloodPressureSystolic()
}
})
}
func calculateBloodPressureSystolic() {
guard let bloodPressureSystolic = HKObjectType.quantityType(forIdentifier: .bloodPressureSystolic) else {
// This should never fail when using a defined constant.
fatalError("*** Unable to get the bloodPressure count ***")
}
query = HKStatisticsQuery(quantityType: bloodPressureSystolic,
quantitySamplePredicate: nil,
options: .discreteAverage) {
query, statistics, error in
DispatchQueue.main.async{
self.valueTest = statistics?.averageQuantity()
}
}
healthStore!.execute(query!)
}
}
Then use it in your ContentView like this.
import SwiftUI
struct ContentView: View {
#EnvironmentObject var healthStore: HealthStore
var body: some View {
Text("Hello, world!")
.onAppear {
healthStore.setUpHealthStore()
}
}
}
I didn't go through the trouble of setting up the proper permissions in the .plist file, but you'll also need to set up the Health Share Usage Description as well as Health Update Usage Description. I assume you have already done this but I just wanted to mention it.

SwiftUI View reads stale ObservableObject property value

I am seeing a very strange state behavior when updating ObservableObjects on iOS 14.5 under a very particular condition. I suspect this is a SwiftUI bug - I extracted code to reproduce this from my application for further investigation.
The question is: Is this actually a bug in SwiftUI or is there an explanation for the behavior?
The bug is hard to reproduce, so let's start simple: Assume we have two models FlipModel (storing a value flipped) and CounterModel (keeping the number the value has been flipped). The flipped value is observed via onReceive and if a value change occurs, the counter is incremented. This is just an arbitrary, simplified example to be able to reproduce the bug.
class FlipModel: ObservableObject {
#Published var flipped = true {
didSet {
print("new value for flipped: \(self.flipped)")
}
}
}
class CounterModel: ObservableObject {
#Published var counter = 0
}
struct StrangeStateGlitchExampleView: View {
#StateObject var flipModel = FlipModel()
#StateObject var counterModel = CounterModel()
// MARK: - View
var body: some View {
VStack(spacing: 10) {
Button("Toggle value from SwiftUI") {
self.flipModel.flipped.toggle()
}
Text("Flipped: \(String(describing: flipModel.flipped))")
Text("Flipped \(self.counterModel.counter) times")
}
.onReceive(self.flipModel.$flipped, perform: { _ in
withAnimation {
self.counterModel.counter += 1
}
})
}
}
struct StrangeStateGlitchExampleView_Previews: PreviewProvider {
static var previews: some View {
StrangeStateGlitchExampleView()
}
}
This works fine:
Now let's add three things to the example that are all necessary for the bug to occur:
The change is not done from SwiftUI, but from a background thread via DispatchQueue.main.async.
FlipModel is also observed from another view.
The counter change is done in an withAnimation block.
Example code:
class FlipModel: ObservableObject {
#Published var flipped = true {
didSet {
print("new value for flipped: \(self.flipped)")
}
}
}
class CounterModel: ObservableObject {
#Published var counter = 0
}
struct StrangeStateGlitchExampleView: View {
#StateObject var flipModel = FlipModel()
#StateObject var counterModel = CounterModel()
// MARK: - View
var body: some View {
VStack(spacing: 10) {
Button("Toggle value from SwiftUI") {
self.flipModel.flipped.toggle()
}
Button("Toggle value from external thread") {
// --> 1) The change is not done from SwiftUI.
// but from a background thread via `DispatchQueue.main.async`.
DispatchQueue.main.async {
self.flipModel.flipped.toggle()
}
}
// -- 2) FlipModel is also observed from another view.
OtherView(flipModel: self.flipModel)
Text("Flipped (ExampleView): \(String(describing: flipModel.flipped))")
Text("Flipped \(self.counterModel.counter) times")
}
.onReceive(self.flipModel.$flipped, perform: { _ in
// --> 3) The counter change is done in an `withAnimation` block.
withAnimation {
self.counterModel.counter += 1
}
})
}
}
struct OtherView: View {
#ObservedObject var flipModel: FlipModel
var body: some View {
Text("Flipped (OtherView): \(String(describing: flipModel.flipped))")
}
}
struct StrangeStateGlitchExampleView_Previews: PreviewProvider {
static var previews: some View {
StrangeStateGlitchExampleView()
}
}
Now, if the change is triggered like that, the OtherView reads an old value for flipped:

SwiftUI: #State property not updated without weird workaround

I'm experiencing strange behavior with an #State property that isn't being properly updated in its originating view after being changed in another view. I'm using Xcode 12.3 and iOS 14.
What happens is that an #State "session" value-based item and #State "flow" value-based item are sent as bound parameters to another view. When a button is tapped there, it changes their values, and a fullScreenCover call in the originating view is supposed to get the correct view to display next in the flow from a switch statement. But the "session" item is nil in that switch statement unless I include an onChange modifier that looks for changes in either of the two #State properties. The onChange call doesn't have to have any code in it to have this effect.
I'm still relatively new to SwiftUI (although fairly experienced with iOS and Mac development). But this is confusing the heck out of me. I don't understand why it isn't working as expected, nor why adding an empty onChange handler makes it work.
If you'd like to experience this for yourself, here's code to assemble a simple demo project:
// the model types
struct ObservationSession: Codable {
public let id: UUID
public var name: String
public init(name: String) {
self.name = name
self.id = UUID()
}
}
struct SessionListModals {
enum Flow: Identifiable {
case configuration
case observation
case newSession
var id: Flow { self }
}
}
// ContentView
struct ContentView: View {
#State private var mutableSession: ObservationSession?
#State private var flow: SessionListModals.Flow?
var body: some View {
VStack {
Button("New Session", action: {
mutableSession = ObservationSession(name: "")
flow = .newSession
})
.padding()
}
.fullScreenCover(item: $flow) {
viewForFlow($0)
}
// Uncomment either of these 2 onChange blocks to see successful execution of this flow
// Why does that make a difference?
// .onChange(of: mutableSession?.name, perform: { value in
// //
// })
// .onChange(of: flow, perform: { value in
// //
// })
}
#ViewBuilder private func viewForFlow(_ flow: SessionListModals.Flow) -> some View {
switch flow {
case .newSession:
// MARK: - Show New Session View
NavigationView {
NewSessionView(session: $mutableSession, flow: $flow)
.navigationTitle("Create a session")
.navigationBarItems(leading: Button("Cancel", action: {
self.flow = nil
}))
}
case .observation:
// MARK: - Show RecordingView
NavigationView {
let name = mutableSession?.name ?? "Unnamed session"
RecordingView(sessionName: name)
.navigationBarItems(leading: Button("Close", action: {
self.flow = nil
}))
}
default:
NavigationView {
EmptyView()
.navigationBarItems(leading: Button("Close", action: {
self.flow = nil
}))
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
// NewSessionView
struct NewSessionView: View {
#Binding var session: ObservationSession?
#Binding var flow: SessionListModals.Flow?
var body: some View {
VStack {
Text("Tap button to create a new session")
Button("New Session", action: {
createNewSession()
})
.padding()
}
}
private func createNewSession() {
let newSession = ObservationSession(name: "Successfully Created A New Session")
session = newSession
flow = .observation
}
}
struct NewSessionView_Previews: PreviewProvider {
static let newSession = ObservationSession(name: "Preview")
static let flow: SessionListModals.Flow = .newSession
static var previews: some View {
NewSessionView(session: .constant(newSession), flow: .constant(flow))
}
}
// RecordingView
struct RecordingView: View {
var sessionName: String
var body: some View {
Text(sessionName)
}
}
struct RecordingView_Previews: PreviewProvider {
static var previews: some View {
RecordingView(sessionName: "Preview")
}
}
class ObservationSession: //Codable, //implement Codable manually
ObservableObject {
public let id: UUID
//This allows you to observe the individual variable
#Published public var name: String
public init(name: String) {
self.name = name
self.id = UUID()
}
}
struct SessionListModals {
enum Flow: Identifiable {
case configuration
case observation
case newSession
var id: Flow { self }
}
}
// ContentView
class ContentViewModel: ObservableObject {
#Published var mutableSession: ObservationSession?
}
struct ContentView: View {
//State stores the entire object and observes it as a whole it does not individually observe its variables that is why .onChange works
#StateObject var vm: ContentView3Model = ContentView3Model()
#State private var flow: SessionListModals.Flow?
var body: some View {
VStack {
Button("New Session", action: {
//Since you want to change it programatically you have to put them in another object
vm.mutableSession = ObservationSession(name: "")
flow = .newSession
})
.padding()
}
.fullScreenCover(item: $flow) {
viewForFlow($0)
}
}
#ViewBuilder private func viewForFlow(_ flow: SessionListModals.Flow) -> some View {
switch flow {
case .newSession:
// MARK: - Show New Session View
NavigationView {
NewSessionView(session: $vm.mutableSession, flow: $flow)
.navigationTitle("Create a session")
.navigationBarItems(leading: Button("Cancel", action: {
self.flow = nil
}))
}
case .observation:
// MARK: - Show RecordingView
NavigationView {
let name = vm.mutableSession?.name ?? "Unnamed session"
RecordingView(sessionName: name)
.navigationBarItems(leading: Button("Close", action: {
self.flow = nil
}))
}
default:
NavigationView {
EmptyView()
.navigationBarItems(leading: Button("Close", action: {
self.flow = nil
}))
}
}
}
}

SwiftUI – Alert is only showing once

I have a strange problem with the SwiftUI Alert view. In an ObservableObject, I do some network requests and in case of a error I will show a alert. This is my simplified model:
class MyModel: ObservableObject {
let objectWillChange = ObservableObjectPublisher()
#Published var isError: Bool = false
public func network() {
Service.call() {
self.isError = true
DispatchQueue.main.async {
self.objectWillChange.send()
}
}
}
}
Service.call is a dummy for my network request. My view looks like:
struct MyView: View {
#ObservedObject var model: MyModel
var body: some View {
…
.alert(isPresented: self.$model.isError) {
print("Error Alert")
return Alert(title: Text("Alert"))
}
}
}
On the first call, everything works and the alert is shown. For all further calls,print("Error Alert") will be executed and Error Alert appears in the console, but the alert is not shown.
Does anyone have any idea why Alert is only shown once?
Try to use instead (there is already default publisher for #Published properties)
class MyModel: ObservableObject {
#Published var isError: Bool = false
public func network() {
Service.call() {
DispatchQueue.main.async {
self.isError = true // << !!! important place to call
}
}
}
}

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