SwiftUI NavigationStack pass vars through enum? - swiftui

Can I pass a variable through my Route enum in SwiftUI for my NavigationStack? In this example I am attempting to pass input: String through the Route enum into my views #State var output: String.
#available(iOS 16.0, *)
enum Route: Hashable {
case first(input: String)
#ViewBuilder
func view(_ path: Binding<NavigationPath>) -> some View{
switch self {
//ERROR HERE: cannot find input var to pass to view
case .first: AView1(output: input)
}
}
}
#available(iOS 16.0, *)
struct AView1: View {
#State var output: String
#State var path: NavigationPath = .init()
var body: some View {
NavigationStack(path: $path){
VStack{
Text(output)
NavigationLink(value: Route.first(input: "Hello"), label: {Text("Click")})
}
.navigationDestination(for: Route.self){ route in
route.view($path)
}
}
}
}

try changing
case .first: AView1(output: input)
to
case .first(let input): AView1(output: input)

Related

SwiftUI pass binding through NavigationStack enum

I am attempting to pass a Binding through my NavigationStack enum into my View. I'm not sure if I can pass Binding into an enum, if I cannot then how should I go about this. Thanks in advance!
#available(iOS 16.0, *)
enum Route: Hashable, Equatable {
//ERROR HERE: Not sure how to get Binding in enum or if possible
case gotoBView(input: Binding<String>)
#ViewBuilder
func view(_ path: Binding<NavigationPath>) -> some View{
switch self {
case .gotoBView(let input): BView1(bvar: input)
}
}
var isEmpty: Bool {
return false
}
}
//START VIEW
#available(iOS 16.0, *)
struct ContentView25: View {
#State var input = "Hello"
#State var path: NavigationPath = .init()
var body: some View {
NavigationStack(path: $path){
NavigationLink(value: Route.gotoBView(input: $input), label: {Text("Go To A")})
.navigationDestination(for: Route.self){ route in
route.view($path)
}
}
}
}
//View to navigate to with binding
#available(iOS 16.0, *)
struct BView1: View {
#Binding var bvar: String
var body: some View {
Text(bvar)
}
}
Here is a workaround, in previous iOS versions this has dismissed the NavigationLink, In iOS 16.2 it does not behave this way, I would do extensive testing before using this in a production app.
import SwiftUI
#available(iOS 16.0, *)
enum Route: Hashable, Equatable {
case gotoBView(input: Binding<String>)
#ViewBuilder
func view(_ path: Binding<NavigationPath>) -> some View{
switch self {
case .gotoBView(let input): BView1(bvar: input)
}
}
//Create a custom implementation of Hashable that ignores Binding
func hash(into hasher: inout Hasher) {
switch self {
case .gotoBView(let input):
hasher.combine(input.wrappedValue)
}
}
//Create a custom implementation of Equatable that ignores Binding
static func == (lhs: Route, rhs: Route) -> Bool {
return lhs.hashValue == rhs.hashValue
}
}
SwiftUI is all about identity and NavigationPath uses Hashable and Equatable to function. This bypasses SwiftUI's implementation.
With the stack, you don't need an enum, you can have multiple navigationDestination for each value type. To use it with a binding you can do it the old way, with a func that looks it up by ID, like how we did it before ForEach supported bindings, e.g.
struct NumberItem: Identifiable {
let id = UUID()
var number: Int
var text = ""
}
struct ContentView: View {
#State var numberItems = [NumberItem(number: 1), NumberItem(number: 2), NumberItem(number: 3), NumberItem(number: 4), NumberItem(number: 5)]
var body: some View {
NavigationStack {
List {
ForEach(numberItems) { numberItem in
NavigationLink(value: numberItem.id) {
Text("\(numberItem.number)")
}
}
}
.navigationDestination(for: NumberItem.ID.self) { numberItemID in
ChildDetailView(numberItems: $numberItems, numberItemID: numberItemID)
}
//.navigationDestination(for: AnotherItem.ID.self) { anotherItemID in
// ...
//}
}
}
}
// this wrapper View was originally needed to make bindings in
// navigationDestinations work at all, now its needed to fix a bug
// with the cursor jumping to the end of a text field which is
// using the binding.
struct ChildDetailView: View {
#Binding var numberItems: [NumberItem]
let numberItemID: UUID
var body: some View {
ChildDetailView2(numberItem: binding(for: numberItemID))
}
private func binding(for numberItemID: UUID) -> Binding<NumberItem> {
guard let index = numberItems.firstIndex(where: { $0.id == numberItemID }) else {
fatalError("Can't find item in array")
}
return $numberItems[index]
}
}
struct ChildDetailView2: View {
#Binding var numberItem: NumberItem
var body: some View {
VStack {
Text("\(numberItem.number)")
TextField("Test", text: $numberItem.text) // cursor jumps to the end if not wrapped in an extra View like this one is.
Button {
numberItem.number += 10
} label: {
Text("Add 10")
}
}
.navigationTitle("Detail")
}
}

SwiftUI View don't see property of ObservableObject marked with #Published

I'm writing my app using SwiftUI and VIPER. And to save the idea of viper(testability, protocols and etc) and SwiftUI reactivity I want to add 1 more layer - ViewModel. My presenter will ask data from interactor and will put in ViewModel, then view will just read this value.I checked does method that put data into view model works - and yes it does. But my view just don't see the property of view model (shows empty list) even if it conforms to ObservableObject and property is marked with Published. What is more interesting that if I store data in presenter and also mark it with published and observable object it will work. Thank in advance!
class BeersListPresenter: BeersListPresenterProtocol, ObservableObject{
var interactor: BeersListInteractorProtocol
#ObservedObject var viewModel = BeersListViewModel()
init(interactor: BeersListInteractorProtocol){
self.interactor = interactor
}
func loadList(at page: Int){
interactor.loadList(at: page) { beers in
DispatchQueue.main.async {
self.viewModel.beers.append(contentsOf: beers)
print(self.viewModel.beers)
}
}
}
class BeersListViewModel:ObservableObject{
#Published var beers = [Beer]()
}
struct BeersListView: View{
var presenter : BeersListPresenterProtocol
#StateObject var viewModel : BeersListViewModel
var body: some View {
NavigationView{
List{
ForEach(viewModel.beers, id: \.id){ beer in
HStack{
VStack(alignment: .leading){
Text(beer.name)
.font(.headline)
Text("Vol: \(presenter.formattedABV(beer.abv))")
.font(.subheadline)
}
Some things to note.
You can't chain ObservableObjects so #ObservedObject var viewModel = BeersListViewModel() inside the class won't work.
The second you have 2 ViewModels one in the View and one in the Presenter you have to pick one. One will not know what the other is doing.
Below is how to get your code working
import SwiftUI
struct Beer: Identifiable{
var id: UUID = UUID()
var name: String = "Hops"
var abv: String = "H"
}
protocol BeersListInteractorProtocol{
func loadList(at: Int, completion: ([Beer])->Void)
}
struct BeersListInteractor: BeersListInteractorProtocol{
func loadList(at: Int, completion: ([Beer]) -> Void) {
completion([Beer(), Beer(), Beer()])
}
}
protocol BeersListPresenterProtocol: ObservableObject{
var interactor: BeersListInteractorProtocol { get set }
var viewModel : BeersListViewModel { get set }
func formattedABV(_ abv: String) -> String
func loadList(at page: Int)
}
class BeersListPresenter: BeersListPresenterProtocol, ObservableObject{
var interactor: BeersListInteractorProtocol
//You can't chain `ObservedObject`s
#Published var viewModel = BeersListViewModel()
init(interactor: BeersListInteractorProtocol){
self.interactor = interactor
}
func loadList(at page: Int){
interactor.loadList(at: page) { beers in
DispatchQueue.main.async {
self.viewModel.beers.append(contentsOf: beers)
print(self.viewModel.beers)
}
}
}
func formattedABV(_ abv: String) -> String{
"**\(abv)**"
}
}
//Change to struct
struct BeersListViewModel{
var beers = [Beer]()
}
struct BeerListView<T: BeersListPresenterProtocol>: View{
//This is what will trigger view updates
#StateObject var presenter : T
//The viewModel is in the Presenter
//#StateObject var viewModel : BeersListViewModel
var body: some View {
NavigationView{
List{
Button("load list", action: {
presenter.loadList(at: 1)
})
ForEach(presenter.viewModel.beers, id: \.id){ beer in
HStack{
VStack(alignment: .leading){
Text(beer.name)
.font(.headline)
Text("Vol: \(presenter.formattedABV(beer.abv))")
.font(.subheadline)
}
}
}
}
}
}
}
struct BeerListView_Previews: PreviewProvider {
static var previews: some View {
BeerListView(presenter: BeersListPresenter(interactor: BeersListInteractor()))
}
}
Now I am not a VIPER expert by any means but I think you are mixing concepts. Mixing MVVM and VIPER.Because in VIPER the presenter Exists below the View/ViewModel, NOT at an equal level.
I found this tutorial a while ago. It is for UIKit but if we use an ObservableObject as a replacement for the UIViewController and the SwiftUI View serves as the storyboard.
It makes both the ViewModel that is an ObservableObject and the View that is a SwiftUI struct a single View layer in terms of VIPER.
You would get code that looks like this
protocol BeersListPresenterProtocol{
var interactor: BeersListInteractorProtocol { get set }
func formattedABV(_ abv: String) -> String
func loadList(at: Int, completion: ([Beer]) -> Void)
}
struct BeersListPresenter: BeersListPresenterProtocol{
var interactor: BeersListInteractorProtocol
init(interactor: BeersListInteractorProtocol){
self.interactor = interactor
}
func loadList(at: Int, completion: ([Beer]) -> Void) {
interactor.loadList(at: at) { beers in
completion(beers)
}
}
func formattedABV(_ abv: String) -> String{
"**\(abv)**"
}
}
protocol BeersListViewProtocol: ObservableObject{
var presenter: BeersListPresenterProtocol { get set }
var beers: [Beer] { get set }
func loadList(at: Int)
func formattedABV(_ abv: String) -> String
}
class BeersListViewModel: BeersListViewProtocol{
#Published var presenter: BeersListPresenterProtocol
#Published var beers: [Beer] = []
init(presenter: BeersListPresenterProtocol){
self.presenter = presenter
}
func loadList(at: Int) {
DispatchQueue.main.async {
self.presenter.loadList(at: at, completion: {beers in
self.beers = beers
})
}
}
func formattedABV(_ abv: String) -> String {
presenter.formattedABV(abv)
}
}
struct BeerListView<T: BeersListViewProtocol>: View{
#StateObject var viewModel : T
var body: some View {
NavigationView{
List{
Button("load list", action: {
viewModel.loadList(at: 1)
})
ForEach(viewModel.beers, id: \.id){ beer in
HStack{
VStack(alignment: .leading){
Text(beer.name)
.font(.headline)
Text("Vol: \(viewModel.formattedABV(beer.abv))")
.font(.subheadline)
}
}
}
}
}
}
}
struct BeerListView_Previews: PreviewProvider {
static var previews: some View {
BeerListView(viewModel: BeersListViewModel(presenter: BeersListPresenter(interactor: BeersListInteractor())))
}
}
If you don't want to separate the VIPER View Layer into the ViewModel and the SwiftUI View you can opt to do something like the code below but it makes it harder to replace the UI and is generally not a good practice. Because you WON'T be able to call methods from the presenter when there are updates from the interactor.
struct BeerListView<T: BeersListPresenterProtocol>: View, BeersListViewProtocol{
var presenter: BeersListPresenterProtocol
#State var beers: [Beer] = []
var body: some View {
NavigationView{
List{
Button("load list", action: {
loadList(at: 1)
})
ForEach(beers, id: \.id){ beer in
HStack{
VStack(alignment: .leading){
Text(beer.name)
.font(.headline)
Text("Vol: \(formattedABV(beer.abv))")
.font(.subheadline)
}
}
}
}
}
}
func loadList(at: Int) {
DispatchQueue.main.async {
self.presenter.loadList(at: at, completion: {beers in
self.beers = beers
})
}
}
func formattedABV(_ abv: String) -> String {
presenter.formattedABV(abv)
}
}
struct BeerListView_Previews: PreviewProvider {
static var previews: some View {
BeerListView<BeersListPresenter>(presenter: BeersListPresenter(interactor: BeersListInteractor()))
}
}

#State variable doesn't change its value or re-render the view

I'm trying to change a #State from its parent but nothing is changed in the view.
I have a simple CustomView with an enum that control its content:
struct CustomView: View {
enum ViewType: String {
case first, second, third
}
#State var viewType: ViewType
var body: some View {
content
}
#ViewBuilder private var content: some View {
switch viewType {
case .first:
Text("The first view")
case .second:
Text("The second view")
case .third:
Text("The last view")
}
}
}
And a superview with a button that tries to change its viewType value.
struct ContentView: View {
let customView = CustomView(viewType: .first)
var body: some View {
VStack(spacing: 40) {
customView
Button("Change") {
customView.viewType = .second
}
}
}
}
Maybe I shouldn't use #State but I had no success using Binding with enums.
Any idea?
Your suspicion about using a #Binding was correct -- the parent view should own the #State and pass it down to the child view.
enum ViewType: String {
case first, second, third
}
struct ContentView: View {
#State var viewType: ViewType = .first
var body: some View {
VStack(spacing: 40) {
CustomView(viewType: $viewType)
Button("Change") {
viewType = .second
}
}
}
}
struct CustomView: View {
#Binding var viewType : ViewType
var body: some View {
content
}
#ViewBuilder private var content: some View {
switch viewType {
case .first:
Text("The first view")
case .second:
Text("The second view")
case .third:
Text("The last view")
}
}
}
In general, by the way, it's not a good idea to try to store references to Views like you did with let customView = CustomView(viewType: .first). Instead, the View should be declared in the hierarchy and its state should be passed via parameters (including #Bindings).

How to store EnumTypes with #AppStorage in SwiftUI

#pretep
Hi,
I want to store a map-status in UserDefaults. Is it possible to do something like this:
#AppStorage("myMapType") var mapType: MKMapType = .standard
Or do I have to access the rawValue of the MKMapType instead? How could I do this? Thanks in advance
Peter
import SwiftUI
import MapKit
struct MapTypeSwitcherView: View {
#AppStorage("myMapType") var mapType: Int = 0
let mapCases: [MKMapType] = [.hybrid,.hybridFlyover, .mutedStandard,.satellite,.satelliteFlyover,.standard]
var body: some View {
VStack{
MapViewUIKit()
ForEach(mapCases, id: \.self ){ type in
Button(type.rawValue.description, action: {
mapType = Int(type.rawValue)
})
}
}
}
}
struct MapViewUIKit: UIViewRepresentable {
#AppStorage("myMapType") var mapType: Int = 0
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.mapType = MKMapType(rawValue: UInt(mapType)) ?? .standard
return mapView
}
func updateUIView(_ mapView: MKMapView, context: Context) {
mapView.mapType = MKMapType(rawValue: UInt(mapType)) ?? .standard
}
}
If it is a custom enumthat you can make conform to Codable it can be much simpler
enum MyValues: String, Codable, CaseIterable{
case first
case second
case third
}
struct NewListView: View {
#AppStorage("myEnumType") var enumType: MyValues = .first
var body: some View {
VStack{
Text("Hello World!")
Text(enumType.rawValue)
Picker("myEnums", selection: $enumType, content: {
ForEach(MyValues.allCases, id: \.self, content: { item in
Text(item.rawValue).tag(item)
})
})
}
}
}
You can storing the following way:
#AppStorage("darkThemeOptionSelection") var darkThemeOptionSelection: DarkThemeOptions = .nord
enum DarkThemeOptions: String, CaseIterable {
case nord
}

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