Keeping data in SwiftUI MVVM hierarchy without persistence - mvvm

I am new to SwiftUI, coming from Java. I know MVC pattern and try to understand MVVM in SwiftUI. It kindly works, but I don't know how to keep data between View hierarchy... also the View should not know anything about models, so I tried using the ViewModels passing around the views. Each ViewModel than should manage their Model.
First Question: Is this a good way to implement MVVM in SwiftUI like I did?
Second: Anyone here who could help me out to divide strictly Model from View and getting this code working?
For now I can add a Garage, go into GarageView and add certain Car models. After switching back to GarageListView and adding another Garage model all Car models from first Garage model are gone... what am I doing wrong? ;(
import SwiftUI
// Models
struct Car: Identifiable, Codable {
var id = UUID()
var name: String
}
struct Garage: Identifiable, Codable {
var id = UUID()
var name: String
var cars: [Car] = []
}
// ViewModels
class GarageListViewModel: ObservableObject {
#Published var garages: [Garage] = []
}
class GarageViewModel: ObservableObject {
#Published var garage: Garage
init(garage: Garage) {
self.garage = garage
}
}
class CarViewModel: ObservableObject {
#Published var car: Car
init(car: Car) {
self.car = car
}
}
// Views
struct GarageListView: View {
#ObservedObject var viewModel: GarageListViewModel
var body: some View {
List {
ForEach(viewModel.garages) { garage in
NavigationLink {
GarageView(viewModel: GarageViewModel(garage: garage))
} label: {
Text(garage.name)
}
}
}
.toolbar {
Button {
viewModel.garages.append(Garage(name: "My Garage"))
} label: {
Label("Add", systemImage: "plus")
}
}
}
}
struct GarageView: View {
#ObservedObject var viewModel: GarageViewModel
var body: some View {
List {
ForEach(viewModel.garage.cars) { car in
NavigationLink {
CarDetailView(viewModel: CarViewModel(car: car))
} label: {
Text(car.name)
}
}
}
.toolbar {
Button {
viewModel.garage.cars.append(Car(name: "My Car"))
} label: {
Label("Add", systemImage: "plus")
}
}
}
}
struct CarDetailView: View {
#ObservedObject var viewModel: CarViewModel
var body: some View {
Text(viewModel.car.name)
}
}
struct ContentView: View {
#StateObject var viewModel = GarageListViewModel()
var body: some View {
NavigationView {
GarageListView(viewModel: viewModel)
}
}
}

I would switch from using struct to class. Structs are passed by value, so you'd never get the exact same struct but a copy of it. By adding a new GarageViewModel, SwiftUI will rerender the List, that's why you lose your garages' cars when you add a new garage.
On a side note, you also forgot to add a NavigationView around your GarageListView and the List in the GarageView, rendering the NavigationLinks useless.

It can be a bit overwhelming to get into MVVC. I would definitely go through Apple's SwiftUI Tutorials as they subtly go through how to set up an MVVC.
As #loremipsum said, you should never init your ViewModel in a view struct because structs are immutable value types, and SwiftUI simply discards the view and recreates it, recreating EVERYTHING inside of it. Also, as you said yourself, your views shouldn't know about the inner workings of the model, so the model should change itself. Therefore, adding garages and cars should be handled in the ViewModel.
Another thing with the ViewModel; you do not need one for EACH view. In this case, one GaragesViewModel can handle all your data. And it should. Apple talks about having a "single source of truth'. That means that there is ONE place your views can go and get the data. That is your ViewModel. Unless you have a wholly unrelated set of data, keep it in one model.
Lastly, before some example code, not every view needs to have the model, or even a mutable parameter. Remember, views are disposable, and with them their parameters are disposed of. If you mutate the model, you will get a new view, so let constants are fine to use. I left your models alone and removed the CarViewModel. It is not needed in this example.
// ViewModel
class GaragesViewModel: ObservableObject {
// This initializes the model IN the model and provides a Singleton.
// You can refer to it anywhere you need to in code.
static var shared = GaragesViewModel()
#Published var garages: [Garage] = []
// Data manipulation in the model
public func addNewGarage() {
garages.append(Garage(name: "My Garage"))
}
public func add(car: Car, to garage:Garage) {
// The guard will stop the func if garage is not in garages.
guard let index = garages.firstIndex(where: { $0.id == garage.id }) else { return }
garages[index].cars.append(car)
}
}
// Views
struct ContentView: View {
var body: some View {
NavigationView {
GarageListView()
}
}
}
struct GarageListView: View {
// Since this is the only view that needs the model, it is called here.
// There is no need to pass it in.
#ObservedObject var viewModel = GaragesViewModel.shared
var body: some View {
List {
ForEach(viewModel.garages) { garage in
NavigationLink {
GarageView(garage: garage)
} label: {
Text(garage.name)
}
}
}
.toolbar {
Button {
GaragesViewModel.shared.addNewGarage()
} label: {
Label("Add", systemImage: "plus")
}
}
}
}
struct GarageView: View {
// GarageView doesn't mutate garage, so it can be a let constant. KISS principal.
let garage: Garage
var body: some View {
List {
ForEach(garage.cars) { car in
NavigationLink {
CarDetailView(car: car)
} label: {
Text(car.name)
}
}
}
.toolbar {
Button {
// if you wanted, you could add a func to car to return a new "My Car"
// and further separate the model.
GaragesViewModel.shared.add(car: Car(name: "My Car"), to: garage)
} label: {
Label("Add", systemImage: "plus")
}
}
}
}
struct CarDetailView: View {
// CarDetailView doesn't mutate car, so it can be a let constant.
let car: Car
var body: some View {
Text(car.name)
}
}
Welcome to StackOverflow! You just got yourself too far into the weeds trying to implement this, but it was a good first shot.
edit:
You can add something like:
public func changeName(of car: Car, in garage: Garage, to name: String) {
guard let garageIndex = garages.firstIndex(where: { $0.id == garage.id }),
let carIndex = garage.cars.firstIndex(where: { $0.id == car.id }) else { return }
garages[garageIndex].cars[carIndex].name = name
}
to your model, and then use it like this:
struct CarDetailView: View {
// Make car an #State variable so you can change it, and then pass the change to the func in the model.
#State var car: Car
let garage: Garage
var body: some View {
VStack {
TextField("", text: $car.name)
.padding()
Button {
GaragesViewModel.shared.changeName(of: car, in: garage)
} label: {
Text("Save")
}
}
}
}
Now, this is for learning purposes only. I would not use this in a shipping app as you have no persistence of your data. If you need to create a database of cars and garages, etc. I would use CoreData to track it, and it works a little differently than just using the structs and class we have here.
Also, if you have any more questions, you really need to make a new question. The purpose of StackOverflow is to get discrete answers to discrete questions, and so follow on questions are discouraged.

Related

Is there a way to pass mutating closures as arguments to a SwiftUI View?

The Idea
In one of the views of my application I need to mutate some data. To make the code clear, testable and just to test how far I can get without logic in ViewModels, I've moved the mutating logic to the Model layer.
Say this is my Model
struct Model {
var examples: [Example] = []
/* lots of other irrelevant properties and a constructor here */
}
struct Example: Identifiable {
var id = UUID()
var isEnabled: Bool = true
/* other irrelevant properties and a constructor here */
}
And the function that mutates stuff is
// MARK: mutating methods
extension Model {
mutating func disableExamples(with ids: Set<UUID>) {
// do whatever, does not matter now
}
mutating func enableExamples(with ids: Set<UUID>) {
// do whatever, does not matter now
}
}
Now, let's display it in views, shall we?
struct ContentView: View {
#State private var model = Model()
var body: some View {
VStack {
Text("That's the main view.")
// simplified: no navigation/sheets/whatever
ExampleMutatingView(examples: $model.examples)
}
}
}
struct ExampleMutatingView: View {
#Binding var examples: [Example]
var body: some View {
VStack {
Text("Here you mutate the examples.")
List($examples) {
// TODO: do stuff
}
}
}
}
The attempt
Since I don't want to put the whole Model into the ExampleMutatingView, both because I don't need the whole thing and due to performance reasons, I tried to supply the view with necessary methods.
I've also added the ability to select examples by providing a State variable.
struct ContentView: View {
#State private var model = Model()
var body: some View {
VStack {
Text("That's the main view.")
// simplified: no navigation/sheets/whatever
ExampleMutatingView(examples: $model.examples,
operationsOnExamples: (enable: model.enableExamples, disable: model.disableExamples))
}
}
}
struct ExampleMutatingView: View {
#Binding var examples: [Example]
let operationsOnExamples: (enable: ((Set<UUID>) -> Void, disable: (Set<UUID>) -> Void)
#State private var multiSelection = Set<UUID>()
var body: some View {
VStack {
Text("Here you mutate the examples.")
List($examples, selection: $multiSelection) { example in
Text("\(example.id)")
}
HStack {
Button { operationsOnExamples.enable(with: multiSelection) } label: { Text("Enable selected") }
Button { operationsOnExamples.disable(with: multiSelection) } label: { Text("Disable selected") }
}
}
}
}
The problem
The thing is, with such setup the ContentView greets me with Cannot reference 'mutating' method as function value error. Not good, but mysterious for me for the very reason that fixes it: supplying the actual Model into the view.
The (non ideal) solution
Showing only the parts that changed
// ContentView
1. ExampleMutatingView(model: $model)
// ExampleMutatingView
1. #Binding var model: Model
2. List($model.examples/*...*/)
3. Button { model.enableExamples(with: multiSelection) } /*...*/
4. Button { model.disableExamples(with: multiSelection) } /*...*/
The discussion
Why is it the case? The only difference I see and cannot explain accurately between these two is that supplying the model might give the method access to its self, which is, otherwise, not available. If that's the case, maybe wrapping the methods in some kind of closure with an [unowned self] would help?
I'm fresh to the topic of self in Swift, so I honestly have no idea.
TL;DR: why does it work when I supply the object defining the methods, but does not when I supply only the methods?

Is there a way to update only certain elements in an EnvironmentObject

For example, I would like each list element to be independent of the other's data.
Such that, changing the name of one will not affect the other. But I also want the list to be updated if they are altered from another view.
struct ContentView: View {
var listView = People()
let tempPerson = Person()
var body: some View {
VStack {
Button(action: {
self.listView.people.append(self.tempPerson)
}){
Text("Add person")
}
ListView().environmentObject(listView)
}.onAppear{self.listView.people.append(self.tempPerson)}
}
}
struct ListView : View {
#EnvironmentObject var listView : People
var body: some View{
List(listView.people.indices, id: \.self) { index in
TextField("StringProtocol", text: self.$listView.people[index].name)
}
}
}
class Person {
var name = "abc"
}
class People : ObservableObject {
#Published var people : [Person]
init() {
self.people = []
}
}
Person is a class. Which means if you create:
let tempPerson = Person()
then in every place inside your ContentView you'll refer to the same Person instance - and you'll append the same person in your button action:
Button(action: {
self.listView.people.append(self.tempPerson)
}) {
Text("Add person")
}
I recommend you change your Person to be a struct instead of a class:
struct Person {
var name = "abc"
}
As structs are copied, every time you append a new item:
self.listView.people.append(self.tempPerson)
it will be a copy of the original tempPerson and you'll be able to change your items independently.
You can read more here: Structures and Classes

SwiftUI NavigationLink - "lazy" destination? Or how to not code duplicate `label`?

(on macOS Big Sur with Xcode 12 beta)
One thing I've been struggling with in SwiftUI is navigating from e.g. a SetUpGameView which needs to create a Game struct depending on e.g. player name inputted by the user in this view and then proceed with navigation to GameView via a NavigationLink, being button-like, which should be disabled when var game: Game? is nil. The game cannot be initialized until all necessary data has been inputted by the user, but in my example below I just required playerName: String to be non empty.
I have a decent solution, but it seems too complicated.
Below I will present multiple solutions, all of which seems too complicated, I was hoping you could help me out coming up with an even simpler solution.
Game struct
struct Game {
let playerName: String
init?(playerName: String) {
guard !playerName.isEmpty else {
return nil
}
self.playerName = playerName
}
}
SetUpGameView
The naive (non-working) implementation is this:
struct SetUpGameView: View {
// ....
var game: Game? {
Game(playerName: playerName)
}
var body: some View {
NavigationView {
// ...
NavigationLink(
destination: GameView(game: game!),
label: { Label("Start Game", systemImage: "play") }
)
.disabled(game == nil)
// ...
}
}
// ...
}
However, this does not work, because it crashes the app. It crashes the app because the expression: GameView(game: game!) as destionation in the NavigationLink initializer does not evaluate lazily, the game! evaluates early, and will be nil at first thus force unwrapping it causes a crash. This is really confusing for me... It feels just... wrong! Because it will not be used until said navigation is used, and using this particular initializer will not result in the destination being used until the NavigationLink is clicked. So we have to handle this with an if let, but now it gets a bit more complicated. I want the NavigationLink label to look the same, except for disabled/enabled rendering, for both states game nil/non nil. This causes code duplication. Or at least I have not come up with any better solution than the ones I present below. Below is two different solutions and a third improved (refactored into custom View ConditionalNavigationLink View) version of the second one...
struct SetUpGameView: View {
#State private var playerName = ""
init() {
UITableView.appearance().backgroundColor = .clear
}
var game: Game? {
Game(playerName: playerName)
}
var body: some View {
NavigationView {
VStack {
Form {
TextField("Player name", text: $playerName)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
// All these three solution work
// navToGameSolution1
// navToGameSolution2
navToGameSolution2Refactored
}
}
}
// MARK: Solutions
// N.B. this helper view is only needed by solution1 to avoid duplication in if else, but also used in solution2 for convenience. If solution2 is used this code could be extracted to only occur inline with solution2.
var startGameLabel: some View {
// Bug with new View type `Label` see: https://stackoverflow.com/questions/62556361/swiftui-label-text-and-image-vertically-misaligned
HStack {
Image(systemName: "play")
Text("Start Game")
}
}
var navToGameSolution1: some View {
Group { // N.B. this wrapping `Group` is only here, if if let was inline in the `body` it could have been removed...
if let game = game {
NavigationLink(
destination: GameView(game: game),
label: { startGameLabel }
)
} else {
startGameLabel
}
}
}
var navToGameSolution2: some View {
NavigationLink(
destination: game.map { AnyView(GameView(game: $0)) } ?? AnyView(EmptyView()),
label: { startGameLabel }
).disabled(game == nil)
}
var navToGameSolution2Refactored: some View {
NavigatableIfModelPresent(model: game) {
startGameLabel
} destination: {
GameView(game: $0)
}
}
}
NavigatableIfModelPresent
Same solution as navToGameSolution2 but refactored, so that we do not need to repeat the label, or construct multiple AnyView...
struct NavigatableIfModelPresent<Model, Label, Destination>: View where Label: View, Destination: View {
let model: Model?
let label: () -> Label
let destination: (Model) -> Destination
var body: some View {
NavigationLink(
destination: model.map { AnyView(destination($0)) } ?? AnyView(EmptyView()),
label: label
).disabled(model == nil)
}
}
Feels like I'm missing something here... I don't want to automatically navigate when game becomes non-nil and I don't want the NavigationLink to be enabled until game is non nil.
You could try use #Binding instead to keep track of your Game. This will mean that whenever you navigate to GameView, you have the latest Game as it is set through the #Binding and not through the initializer.
I have made a simplified version to show the concept:
struct ContentView: View {
#State private var playerName = ""
#State private var game: Game?
var body: some View {
NavigationView {
VStack {
Form {
TextField("Player Name", text: $playerName) {
setGame()
}
}
NavigationLink("Go to Game", destination: GameView(game: $game))
Spacer()
}
}
}
private func setGame() {
game = Game(title: "Some title", playerName: playerName)
}
}
struct Game {
let title: String
let playerName: String
}
struct GameView: View {
#Binding var game: Game!
var body: some View {
VStack {
Text(game.title)
Text(game.playerName)
}
}
}

SwiftUI: ObservableObject does not persist its State over being redrawn

Problem
In Order to achieve a clean look and feel of the App's code, I create ViewModels for every View that contains logic.
A normal ViewModel looks a bit like this:
class SomeViewModel: ObservableObject {
#Published var state = 1
// Logic and calls of Business Logic goes here
}
and is used like so:
struct SomeView: View {
#ObservedObject var viewModel = SomeViewModel()
var body: some View {
// Code to read and write the State goes here
}
}
This workes fine when the Views Parent is not being updated. If the parent's state changes, this View gets redrawn (pretty normal in a declarative Framework). But also the ViewModel gets recreated and does not hold the State afterward. This is unusual when you compare to other Frameworks (eg: Flutter).
In my opinion, the ViewModel should stay, or the State should persist.
If I replace the ViewModel with a #State Property and use the int (in this example) directly it stays persisted and does not get recreated:
struct SomeView: View {
#State var state = 1
var body: some View {
// Code to read and write the State goes here
}
}
This does obviously not work for more complex States. And if I set a class for #State (like the ViewModel) more and more Things are not working as expected.
Question
Is there a way of not recreating the ViewModel every time?
Is there a way of replicating the #State Propertywrapper for #ObservedObject?
Why is #State keeping the State over the redraw?
I know that usually, it is bad practice to create a ViewModel in an inner View but this behavior can be replicated by using a NavigationLink or Sheet.
Sometimes it is then just not useful to keep the State in the ParentsViewModel and work with bindings when you think of a very complex TableView, where the Cells themself contain a lot of logic.
There is always a workaround for individual cases, but I think it would be way easier if the ViewModel would not be recreated.
Duplicate Question
I know there are a lot of questions out there talking about this issue, all talking about very specific use-cases. Here I want to talk about the general problem, without going too deep into custom solutions.
Edit (adding more detailed Example)
When having a State-changing ParentView, like a list coming from a Database, API, or cache (think about something simple). Via a NavigationLink you might reach a Detail-Page where you can modify the Data. By changing the data the reactive/declarative Pattern would tell us to also update the ListView, which would then "redraw" the NavigationLink, which would then lead to a recreation of the ViewModel.
I know I could store the ViewModel in the ParentView / ParentView's ViewModel, but this is the wrong way of doing it IMO. And since subscriptions are destroyed and/or recreated - there might be some side effects.
Finally, there is a Solution provided by Apple: #StateObject.
By replacing #ObservedObject with #StateObject everything mentioned in my initial post is working.
Unfortunately, this is only available in ios 14+.
This is my Code from Xcode 12 Beta (Published June 23, 2020)
struct ContentView: View {
#State var title = 0
var body: some View {
NavigationView {
VStack {
Button("Test") {
self.title = Int.random(in: 0...1000)
}
TestView1()
TestView2()
}
.navigationTitle("\(self.title)")
}
}
}
struct TestView1: View {
#ObservedObject var model = ViewModel()
var body: some View {
VStack {
Button("Test1: \(self.model.title)") {
self.model.title += 1
}
}
}
}
class ViewModel: ObservableObject {
#Published var title = 0
}
struct TestView2: View {
#StateObject var model = ViewModel()
var body: some View {
VStack {
Button("StateObject: \(self.model.title)") {
self.model.title += 1
}
}
}
}
As you can see, the StateObject Keeps it value upon the redraw of the Parent View, while the ObservedObject is being reset.
I agree with you, I think this is one of many major problems with SwiftUI. Here's what I find myself doing, as gross as it is.
struct MyView: View {
#State var viewModel = MyViewModel()
var body : some View {
MyViewImpl(viewModel: viewModel)
}
}
fileprivate MyViewImpl : View {
#ObservedObject var viewModel : MyViewModel
var body : some View {
...
}
}
You can either construct the view model in place or pass it in, and it gets you a view that will maintain your ObservableObject across reconstruction.
Is there a way of not recreating the ViewModel every time?
Yes, keep ViewModel instance outside of SomeView and inject via constructor
struct SomeView: View {
#ObservedObject var viewModel: SomeViewModel // << only declaration
Is there a way of replicating the #State Propertywrapper for #ObservedObject?
No needs. #ObservedObject is-a already DynamicProperty similarly to #State
Why is #State keeping the State over the redraw?
Because it keeps its storage, ie. wrapped value, outside of view. (so, see first above again)
You need to provide custom PassThroughSubject in your ObservableObject class. Look at this code:
//
// Created by Франчук Андрей on 08.05.2020.
// Copyright © 2020 Франчук Андрей. All rights reserved.
//
import SwiftUI
import Combine
struct TextChanger{
var textChanged = PassthroughSubject<String,Never>()
public func changeText(newValue: String){
textChanged.send(newValue)
}
}
class ComplexState: ObservableObject{
var objectWillChange = ObservableObjectPublisher()
let textChangeListener = TextChanger()
var text: String = ""
{
willSet{
objectWillChange.send()
self.textChangeListener.changeText(newValue: newValue)
}
}
}
struct CustomState: View {
#State private var text: String = ""
let textChangeListener: TextChanger
init(textChangeListener: TextChanger){
self.textChangeListener = textChangeListener
print("did init")
}
var body: some View {
Text(text)
.onReceive(textChangeListener.textChanged){newValue in
self.text = newValue
}
}
}
struct CustomStateContainer: View {
//#ObservedObject var state = ComplexState()
var state = ComplexState()
var body: some View {
VStack{
HStack{
Text("custom state View: ")
CustomState(textChangeListener: state.textChangeListener)
}
HStack{
Text("ordinary Text View: ")
Text(state.text)
}
HStack{
Text("text input: ")
TextInput().environmentObject(state)
}
}
}
}
struct TextInput: View {
#EnvironmentObject var state: ComplexState
var body: some View {
TextField("input", text: $state.text)
}
}
struct CustomState_Previews: PreviewProvider {
static var previews: some View {
return CustomStateContainer()
}
}
First, I using TextChanger to pass new value of .text to .onReceive(...) in CustomState View. Note, that onReceive in this case gets PassthroughSubject, not the ObservableObjectPublisher. In last case you will have only Publisher.Output in perform: closure, not the NewValue. state.text in that case would have old value.
Second, look at the ComplexState class. I made an objectWillChange property to make text changes send notification to subscribers manually. Its almost the same like #Published wrapper do. But, when the text changing it will send both, and objectWillChange.send() and textChanged.send(newValue). This makes you be able to choose in exact View, how to react on state changing. If you want ordinary behavior, just put the state into #ObservedObject wrapper in CustomStateContainer View. Then, you will have all the views recreated and this section will get updated values too:
HStack{
Text("ordinary Text View: ")
Text(state.text)
}
If you don't want all of them to be recreated, just remove #ObservedObject. Ordinary text View will stop updating, but CustomState will. With no recreating.
update:
If you want more control, you can decide while changing the value, who do you want to inform about that change.
Check more complex code:
//
//
// Created by Франчук Андрей on 08.05.2020.
// Copyright © 2020 Франчук Андрей. All rights reserved.
//
import SwiftUI
import Combine
struct TextChanger{
// var objectWillChange: ObservableObjectPublisher
// #Published
var textChanged = PassthroughSubject<String,Never>()
public func changeText(newValue: String){
textChanged.send(newValue)
}
}
class ComplexState: ObservableObject{
var onlyPassthroughSend = false
var objectWillChange = ObservableObjectPublisher()
let textChangeListener = TextChanger()
var text: String = ""
{
willSet{
if !onlyPassthroughSend{
objectWillChange.send()
}
self.textChangeListener.changeText(newValue: newValue)
}
}
}
struct CustomState: View {
#State private var text: String = ""
let textChangeListener: TextChanger
init(textChangeListener: TextChanger){
self.textChangeListener = textChangeListener
print("did init")
}
var body: some View {
Text(text)
.onReceive(textChangeListener.textChanged){newValue in
self.text = newValue
}
}
}
struct CustomStateContainer: View {
//var state = ComplexState()
#ObservedObject var state = ComplexState()
var body: some View {
VStack{
HStack{
Text("custom state View: ")
CustomState(textChangeListener: state.textChangeListener)
}
HStack{
Text("ordinary Text View: ")
Text(state.text)
}
HStack{
Text("text input with full state update: ")
TextInput().environmentObject(state)
}
HStack{
Text("text input with no full state update: ")
TextInputNoUpdate().environmentObject(state)
}
}
}
}
struct TextInputNoUpdate: View {
#EnvironmentObject var state: ComplexState
var body: some View {
TextField("input", text: Binding( get: {self.state.text},
set: {newValue in
self.state.onlyPassthroughSend.toggle()
self.state.text = newValue
self.state.onlyPassthroughSend.toggle()
}
))
}
}
struct TextInput: View {
#State private var text: String = ""
#EnvironmentObject var state: ComplexState
var body: some View {
TextField("input", text: Binding(
get: {self.text},
set: {newValue in
self.state.text = newValue
// self.text = newValue
}
))
.onAppear(){
self.text = self.state.text
}.onReceive(state.textChangeListener.textChanged){newValue in
self.text = newValue
}
}
}
struct CustomState_Previews: PreviewProvider {
static var previews: some View {
return CustomStateContainer()
}
}
I made a manual Binding to stop broadcasting objectWillChange. But you still need to gets new value in all the places you changing this value to stay synchronized. Thats why I modified TextInput too.
Is that what you needed?
My solution is use EnvironmentObject and don't use ObservedObject at view it's viewModel will be reset, you pass through hierarchy by
.environmentObject(viewModel)
Just init viewModel somewhere it will not be reset(example root view).

SwiftUI: How to iterate over an array of bindable objects?

I'm trying to learn SwiftUI, and how bindings work.
I have this code that works, that shows a list of projects. When one project is tapped, a binding to that project is passed to the child view:
struct ProjectsView: View {
#ObjectBinding var state: AppState
#State var projectName: String = ""
var body: some View {
NavigationView {
List {
ForEach(0..<state.projects.count) { index in
NavigationLink(destination: ProjectView(project: self.$state.projects[index])) {
Text(self.state.projects[index].title)
}
}
}
.navigationBarTitle("Projects")
}
}
}
The child view, where I'm mutating the project using a binding:
struct ProjectView: View {
#Binding var project: Project
#State var projectName: String = ""
var body: some View {
VStack {
Text(project.title)
TextField(
$projectName,
placeholder: Text("Change project name"),
onCommit: {
self.project.title = self.projectName
self.projectName = ""
})
.padding()
}
}
}
However, I would rather iterate over the projects array without using indexes (beacuse I want to learn, and its easier to read), but not sure how I then can pass the binding to a single project. I tried it like this, but then I can't get access to project.title, since it's a binding, and not a String.
ForEach($state.projects) { project in
NavigationLink(destination: ProjectView(project: project)) {
Text(project.title)
}
}
How can I achieve this?
Note: I use Xcode 11.2, #ObjectBinding is obsoleted in it (so you need to update to verify below code).
I asked about model, because it might matter for approach. For similar functionality I preferred Combine's ObservableObject, so model is reference not value types.
Here is the approach, which I tune for your scenario. It is not exactly as you requested, because ForEach requires some sequence, but you try to feed it with unsupported type.
Anyway you may consider below just as alternate (and it is w/o indexes). It is complete module so you can paste it in Xcode 11.2 and test in preview. Hope it would be helpful somehow.
Preview:
Solution:
import SwiftUI
import Combine
class Project: ObservableObject, Identifiable {
var id: String = UUID().uuidString
#Published var title: String = ""
init (title: String) {
self.title = title
}
}
class AppState: ObservableObject {
#Published var projects: [Project] = []
init(_ projects: [Project]) {
self.projects = projects
}
}
struct ProjectView: View {
#ObservedObject var project: Project
#State var projectName: String = ""
var body: some View {
VStack {
Text(project.title)
TextField("Change project name",
text: $projectName,
onCommit: {
self.project.title = self.projectName
self.projectName = ""
})
.padding()
}
}
}
struct ContentView: View {
#ObservedObject var state: AppState = AppState([Project(title: "1"), Project(title: "2")])
#State private var refreshed = false
var body: some View {
NavigationView {
List {
ForEach(state.projects) { project in
NavigationLink(destination: ProjectView(project: project)) {
// !!! existance of .refreshed state property somewhere in ViewBuilder
// is important to inavidate view, so below is just a demo
Text("Named: \(self.refreshed ? project.title : project.title)")
}
.onReceive(project.$title) { _ in
self.refreshed.toggle()
}
}
}
.navigationBarTitle("Projects")
.navigationBarItems(trailing: Button(action: {
self.state.projects.append(Project(title: "Unknown"))
}) {
Text("New")
})
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I'm sort of stuck on the same issue you are, and I found a partial solution. But first I should point out that iterating over the index with ForEach(0..<state.projects.count) { index in ... } is not a good idea because index is an Int, which does not conform to Identifiable. Because of that, the UI will not update when your array changes, and you'll see a warning in the console.
My solution directly accesses the state.projects array when creating ProjectView and using firstIndex(of:) to get a bindable form of the project element. It's kind of icky but it's as far as I could get to making it more SwiftUI-y.
ForEach(state.projects) { project in
NavigationLink(destination: ProjectView(project: self.$state.projects[self.state.projects.firstIndex(of: project)!]))) {
Text(project.title)
}
}
I've found this works:
In your AppState, when you add a project, observe its changes:
import Combine
class AppState: ObservableObject {
#Published var projects: [Project]
var futures = Set<AnyCancellable>()
func addProject(project: Project) {
project.objectWillChange
.sink {_ in
self.objectWillChange.send()
}
.store(in: &futures)
}
...
}
If you ever need to create a binding for a project var in your outer view, do it like this:
func titleBinding(forProject project: Project) -> Binding<String> {
Binding {
project.title
} set: { newValue in
project.title = newValue
}
}
You shouldn't need it if you are passing the project into another view, though.
Unfortunately this doesn't seem to be possible at this time. The only way to achieve this is like the following:
ForEach(state.projects.indices) { index in
NavigationLink(destination: ProjectView(project: state.projects[index])) {
Text(state.projects[index].title)
}
}
NOTE: I didn't compile this code. This is just to give you the gesture for how to go about it. i.e. use and index of type Index and not Int.