Picker drops selected segment when data changed - swiftui

I got a problem with Picker. When I try to change state of data in selected segment it's drops it (gif attached). How I can have stay selected segment in Picker when displaying data is changed (when I press "love" button, selected segment should stay without changes)
Single item has option:
struct Landmark: Identifiable, Codable, Hashable {
var id: Int
var name: String
var imageName: String
var mainImage: Image {
Image(imageName)
var liked: Bool
var popular: Bool
var recommended: Bool
}
I got class with array of objects:
final class ModelData: ObservableObject {
#Published var landmarks: [Landmark]
}
And trying use picker like that:
struct tempSegmentControl: View {
#EnvironmentObject var modelData: ModelData
#State private var selected = ModelData().landmarks
var popularLandmarks: [Landmark] {
modelData.landmarks.filter { landmark in
(landmark.popular)
}
}
var rocommendedLandmarks: [Landmark] {
modelData.landmarks.filter { landmark in
(landmark.recommended)
}
}
var body: some View {
VStack {
Picker("Selection", selection: $selected) {
Text("All").tag(modelData.landmarks)
Text("Popular").tag(popularLandmarks)
Text("Recommended").tag(rocommendedLandmarks)
}
.pickerStyle(SegmentedPickerStyle())
.frame(width: UIScreen.main.bounds.width * 0.95)
ForEach(selected, id: \.id) { land in
NavigationLink(
destination: DetailView2(landmark: land)) {
MainItem3(landmark: land)
}
}
}
}
}
Drop selected segment

selection: isn't going to work properly with an array type like this ([Landmark]).
Instead, you'll probably want to do something simple like:
#State private var selection = 0
//...
Picker("Selection", selection: $selection) {
Text("All").tag(0)
Text("Popular").tag(1)
Text("Recommended").tag(2)
And then use a computed property for your array that you iterate through later:
var selected : [Landmark] {
switch selection {
case 0:
return modelData.landmarks
case 1:
//etc
}
}
You could do this with an enum : Int as well, which would probably be a little safer later on if you cases expand (and it would give you a limited set in your computed property).

Related

Update text with Slider value in List from an array in SwiftUI

I have a list of sliders, but I have a problem updating the text that shows the slider value.
The app workflow is like this:
User taps to add a new slider to the list.
An object that defines the slider is created and stored in an array.
The class that has the array as a property (Db) is an ObservableObject and triggers a View update for each new item.
The list is updated with a new row.
So far, so good. Each row has a slider whose value is stored in a property in an object in an array. However, the value text doesn't update as soon as the slider is moved, but when a new item is added. Please see the GIF below:
The Slider doesn't update the text value when moved
How can I bind the slider movements to the text value? I thought that by defining
#ObservedObject var slider_value: SliderVal = SliderVal()
and binding that variable to the slider, the value would be updated simultaneously but that is not the case. Thanks a lot for any help.
import SwiftUI
import Combine
struct ContentView: View {
#ObservedObject var db: Db
var body: some View {
NavigationView{
List(db.criteria_db){criteria in
VStack {
HStack{
Text(criteria.name).bold()
Spacer()
Text(String(criteria.slider_value.value)) //<-- Problem here
}
Slider(value: criteria.$slider_value.value, in:0...100, step: 1)
}
}
.navigationBarTitle("Criteria")
.navigationBarItems(trailing:
Button(action: {
Criteria.count += 1
db.criteria_db.append(Criteria(name: "Criteria\(Criteria.count)"))
dump(db.criteria_db)
}, label: {
Text("Add Criteria")
})
)
}
.listStyle(InsetGroupedListStyle())
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(db: Db())
}
}
struct Criteria: Identifiable {
var id = UUID()
var name: String
#ObservedObject var slider_value: SliderVal = SliderVal()
static var count: Int = 0
init(name: String) {
self.name = name
}
}
class Db: ObservableObject {
#Published var criteria_db: [Criteria] = []
}
class SliderVal: ObservableObject {
#Published var value:Double = 50
}
The #ObservableObject won't work within a struct like that -- it's only useful inside a SwiftUI View or a DynamicProperty. With your use case, because the class is a reference type, the #Published property has no way of knowing that the SliderVal was changed, so the owner View never gets updated.
You can fix this by turning your model into a struct:
struct Criteria: Identifiable {
var id = UUID()
var name: String
var slider_value: SliderVal = SliderVal()
static var count: Int = 0
init(name: String) {
self.name = name
}
}
struct SliderVal {
var value:Double = 50
}
The problem, once you do this, is you don't have a Binding to use in your List. If you're lucky enough to be on SwiftUI 3.0 (iOS 15 or macOS 12), you can use $criteria within your list to get a binding to the element being currently iterated over.
If you're on an earlier version, you'll need to either use indexes to iterate over the items, or, my favorite, create a custom binding that is tied to the id of the item. It looks like this:
struct ContentView: View {
#ObservedObject var db: Db = Db()
private func bindingForId(id: UUID) -> Binding<Criteria> {
.init {
db.criteria_db.first { $0.id == id } ?? Criteria(name: "")
} set: { newValue in
db.criteria_db = db.criteria_db.map {
$0.id == id ? newValue : $0
}
}
}
var body: some View {
NavigationView{
List(db.criteria_db){criteria in
VStack {
HStack{
Text(criteria.name).bold()
Spacer()
Text(String(criteria.slider_value.value))
}
Slider(value: bindingForId(id: criteria.id).slider_value.value, in:0...100, step: 1)
}
}
.navigationBarTitle("Criteria")
.navigationBarItems(trailing:
Button(action: {
Criteria.count += 1
db.criteria_db.append(Criteria(name: "Criteria\(Criteria.count)"))
dump(db.criteria_db)
}, label: {
Text("Add Criteria")
})
)
}
.listStyle(InsetGroupedListStyle())
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(db: Db())
}
}
class Db: ObservableObject {
#Published var criteria_db: [Criteria] = []
}
Now, because the models are all value types (structs), the View and #Published know when to update and your sliders work as expected.
try something like this:
Slider(value: criteria.$slider_value.value, in:0...100, step: 1)
.onChange(of: criteria.slider_value.value) { newVal in
DispatchQueue.main.async {
criteria.slider_value.value = newVal
}
}

Whats the best way to get SwiftUI Picker() selection to update on app state change?

I'm trying to make a UI with SwiftUI that lists a collection of things that can be of different kinds and can each be updated. I'd like to make the type settable in the UI with a Picker, and I also want the view to update when the item is modified some other way (say, from another part of the UI, or over the network.)
I like a "redux"-style setup, and I don't want to jettison that.
Here's a simple example that shows two items, each with a "randomize" button that changes the item at random and a Picker that lets you choose the new item type. The latter works as expected: the Picker changes the #State var, the store gets updated, etc. The 'randomize' button updates the store and the let property label, but the #State and the Picker don't update.
I would love some advice on good ways to get this to work the way I want.
import SwiftUI
import Combine
#main
struct PuffedWastApp: App {
var store = Store()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(store)
}
}
}
enum ItemState:String, CaseIterable {
case apple
case bannana
case coconut
}
enum Action {
case set(Int,ItemState)
}
class Store: ObservableObject {
#Published var state:[ItemState] = [.apple, .apple]
func reduce(_ action:Action) {
print("do an action")
switch (action) {
case let .set(index,new_state):
print("set \(index) to \(new_state)")
state[index] = new_state
self.objectWillChange.send()
}
}
}
struct ContentView: View {
#EnvironmentObject var store: Store
var body: some View {
VStack {
ForEach(store.state.indices) { index in
ItemContainer(index: index)
//Text("\(index)")
}
}
.padding()
}
}
struct ItemContainer: View {
#EnvironmentObject var store: Store
let index: Int
var body: some View {
ItemView(
index: index,
label: store.state[index], // let property: updates on change in the Store
localLabel: store.state[index], //#State variable; doesn't update on change in the Store
dispatch: store.reduce
)
.padding()
}
}
struct ItemView: View {
let index: Int
let label: ItemState
#State var localLabel: ItemState
let dispatch: (Action)->()
var body: some View {
HStack{
//Text("\(index)")
Text(label.rawValue)
Text(localLabel.rawValue)
Button("Randomize") { dispatch( .set(index, ItemState.allCases.randomElement() ?? .apple ) ) }
Picker("Item type", selection: $localLabel ) {
ForEach( ItemState.allCases , id: \.self ) {
Text($0.rawValue).tag($0)
}
}.onChange(of: localLabel) { dispatch(.set(index, $0)) }
}
.padding()
}
}
Try changing this line:
#State var localLabel: ItemState
to
#Binding var localLabel: ItemState
and pass it in your ItemView init as:
ItemView(
index: index,
label: store.state[index],
localLabel: $store.state[index],
dispatch: store.reduce
)

How to add an observable property when other properties change

I have the following model object that I use to populate a List with a Toggle for each row, which is bound to measurement.isSelected
final class Model: ObservableObject {
struct Measurement: Identifiable {
var id = UUID()
let name: String
var isSelected: Binding<Bool>
var selected: Bool = false
init(name: String) {
self.name = name
let selected = CurrentValueSubject<Bool, Never>(false)
self.isSelected = Binding<Bool>(get: { selected.value }, set: { selected.value = $0 })
}
}
#Published var measurements: [Measurement]
#Published var hasSelection: Bool = false // How to set this?
init(measurements: [Measurement]) {
self.measurements = measurements
}
}
I'd like the hasSelection property to be true whenever any measurement.isSelected is true. I'm guessing somehow Model needs to observe changes in measurements and then update its hasSelection property… but I've no idea where to start!
The idea is that hasSelection will be bound to a Button to enable or disable it.
Model is used as follows…
struct MeasurementsView: View {
#ObservedObject var model: Model
var body: some View {
NavigationView {
List(model.measurements) { measurement in
MeasurementView(measurement: measurement)
}
.navigationBarTitle("Select Measurements")
.navigationBarItems(trailing: NavigationLink(destination: NextView(), isActive: $model.hasSelection, label: {
Text("Next")
}))
}
}
}
struct MeasurementView: View {
let measurement: Model.Measurement
var body: some View {
HStack {
Text(measurement.name)
.font(.subheadline)
Spacer()
Toggle(measurement.name, isOn: measurement.isSelected)
.labelsHidden()
}
}
}
For info, here's a screenshot of what I'm trying to achieve. A list of selectable items, with a navigation link that is enabled when one or more is selected, and disabled when no items are selected.
#user3441734 hasSelection should ideally be a get only property, that
is true if any of measurement.isSelected is true
struct Data {
var bool: Bool
}
class Model: ObservableObject {
#Published var arr: [Data] = []
var anyTrue: Bool {
arr.map{$0.bool}.contains(true)
}
}
example (as before) copy - paste - run
import SwiftUI
struct Data: Identifiable {
let id = UUID()
var name: String
var on_off: Bool
}
class Model: ObservableObject {
#Published var data = [Data(name: "alfa", on_off: false), Data(name: "beta", on_off: false), Data(name: "gama", on_off: false)]
var bool: Bool {
data.map {$0.on_off} .contains(true)
}
}
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
VStack {
List(0 ..< model.data.count) { idx in
HStack {
Text(verbatim: self.model.data[idx].name)
Toggle(isOn: self.$model.data[idx].on_off) {
EmptyView()
}
}
}
Text("\(model.bool.description)").font(.largeTitle).padding()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
When the model.data is updated
#Published var data ....
its publisher calls objectWillChange on ObservableObject.
Next SwiftUI recognize that ObservedObject needs the View to be "updated". The View is recreated, and that will force the model.bool.description will have fresh value.
LAST UPDATE
change this part of code
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
NavigationView {
List(0 ..< model.data.count) { idx in
HStack {
Text(verbatim: self.model.data[idx].name)
Toggle(isOn: self.$model.data[idx].on_off) {
EmptyView()
}
}
}.navigationBarTitle("List")
.navigationBarItems(trailing:
NavigationLink(destination: Text("next"), label: {
Text("Next")
}).disabled(!model.bool)
)
}
}
}
and it is EXACTLY, WHAT YOU HAVE in your updated question
Try it on real device, otherwise the NavigationLink is usable only once (this is well known simulator bug in current Xcode 11.3.1 (11C504)).
The problem with your code at the moment is that even if you observe the changes to measurements, they will not get updated when the selection updates, because you declared the var isSelected: Binding<Bool> as a Binding. This means that SwiftUI is storing it outside of your struct, and the struct itself doesn't update (stays immutable).
What you could try instead is declaring #Published var selectedMeasurementId: UUID? = nil on your model So your code would be something like this:
import SwiftUI
import Combine
struct NextView: View {
var body: some View {
Text("Next View")
}
}
struct MeasurementsView: View {
#ObservedObject var model: Model
var body: some View {
let hasSelection = Binding<Bool> (
get: {
self.model.selectedMeasurementId != nil
},
set: { value in
self.model.selectedMeasurementId = nil
}
)
return NavigationView {
List(model.measurements) { measurement in
MeasurementView(measurement: measurement, selectedMeasurementId: self.$model.selectedMeasurementId)
}
.navigationBarTitle("Select Measurements")
.navigationBarItems(trailing: NavigationLink(destination: NextView(), isActive: hasSelection, label: {
Text("Next")
}))
}
}
}
struct MeasurementView: View {
let measurement: Model.Measurement
#Binding var selectedMeasurementId: UUID?
var body: some View {
let isSelected = Binding<Bool>(
get: {
self.selectedMeasurementId == self.measurement.id
},
set: { value in
if value {
self.selectedMeasurementId = self.measurement.id
} else {
self.selectedMeasurementId = nil
}
}
)
return HStack {
Text(measurement.name)
.font(.subheadline)
Spacer()
Toggle(measurement.name, isOn: isSelected)
.labelsHidden()
}
}
}
final class Model: ObservableObject {
#Published var selectedMeasurementId: UUID? = nil
struct Measurement: Identifiable {
var id = UUID()
let name: String
init(name: String) {
self.name = name
}
}
#Published var measurements: [Measurement]
init(measurements: [Measurement]) {
self.measurements = measurements
}
}
I'm not sure exactly how you want the navigation button in the navbar to behave. For now I just set the selection to nil when it's tapped. You can modify it depending on what you want to do.
If you want to support multi-selection, you can use a Set of selected ids instead.
Also, seems like the iOS simulator has some problems with navigation, but I tested on a physical device and it worked.

How make work a Picker with an ObservedObject in SwiftUI?

I'm trying to get a list of Datacenters from a Rest API and show them in a Picker, so the user can choose one. When I do it with a static list it works fine. However, retrieving the Datacenters dinamically seems not to work fine.
I'm using Xcode 11 (GM)
This is the Datacenter Object
struct Datacenter:Codable, Hashable, Identifiable{
let id: String
var location: String
}
This is the ObservedObject (it has the property datacenters that is an array of Datacenter objects)
#ObservedObject var datacenters_controller : DatacentersController
#State private var selectedDatacenter = 0
This was my first attempt:
Picker(selection: $selectedDatacenter, label: Text("Datacenter")) {
ForEach(0 ..< datacenters_controller.datacenters.count) {
Text(self.datacenters_controller.datacenters[$0].location)
}
}
Swift complained with the following error:
ForEach<Range<Int>, Int, Text> count (4) != its initial count (0). `ForEach(_:content:)` should only be used for *constant* data. Instead conform data to `Identifiable` or use `ForEach(_:id:content:)` and provide an explicit `id`!
Then I switched to:
Picker(selection: $selectedDatacenter, label: Text("Datacenter")) {
ForEach(datacenters_controller.datacenters) { datacenter in
Text(datacenter.location)
}
}
It "works" (no error), but the result is not the expected because although I can select a datacenter, it is not "stored", not shown in the Picker as selected.
Actual result
Expected result
Any idea? What I'm doing wrong?
Here's a working example. The key is that selectedDatacenter needs to be the same type as Datacenter.id (in this case, String).
struct ContentView: View {
#ObservedObject var datacenters_controller = DatacentersController()
#State private var selectedDatacenter = ""
var body: some View {
NavigationView {
Form {
Picker(selection: $selectedDatacenter, label: Text("Datacenter")) {
ForEach(datacenters_controller.datacenters) { datacenter in
Text(datacenter.location)
}
}
// Just here for demonstration
Text("selectedDatacenter (id): \(selectedDatacenter.isEmpty ? "Nothing yet" : selectedDatacenter)")
}
}
}
}
Here's the supporting code
struct Datacenter:Codable, Hashable, Identifiable{
let id: String
var location: String
}
class DatacentersController: ObservableObject {
#Published var datacenters: [Datacenter] = []
init() {
datacenters = [
Datacenter(id: "ABQ", location: "Albuquerque"),
Datacenter(id: "BOS", location: "Boston"),
Datacenter(id: "COS", location: "Colorado Springs")
]
}
}
I think you are missing tag on your picker:
Picker(selection: $selectedDatacenter, label: Text("Datacenter")) {
ForEach(datacenters_controller.datacenters) {
Text($0.location).tag($0)
}
}
Apple docs on tag:
Sets the tag of the view, used for selecting from a list of View
options.
In your second attempt, you need to use the tag modifier (as described by LuLuGaGa). You also need to change the type of selectedDatacenter to match. For example:
struct ContentView: View {
init(_ controller: DatacentersController) {
self.datacenters_controller = controller
self._selectedDatacenter = State(initialValue: controller.datacenters[0].id)
}
var body: some View {
NavigationView {
Form {
Picker(selection: $selectedDatacenter, label: Text("Datacenter")) {
ForEach(datacenters_controller.datacenters) {
Text($0.location).tag($0)
}
}
}
}
}
#ObservedObject private var datacenters_controller: DatacentersController
#State private var selectedDatacenter: String
}

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