SwiftUI Model by ViewModel edit other ViewModel - swiftui

let's say you have a list of some objects - let's say cats. There is a common viewmodel that stores the data of the cats received from the server. Each element on the view is a small sector of data for one cat. Within this sector, the data can be changed by the user. How to correctly link the viewmodel of the list of cats and the viewmodel of one cat?
struct Cat : Identifiable {
let id = UUID()
var name: String
var breed: String
}
struct CatListView : View {
#ObservedObject private(set) var viewModel: ViewModel
var body: some View {
List(self.viewModel.items) { (item) in
CatView(viewModel: self.viewModel.createViewModel(item: item))
}
}
}
extension CatListView {
class ViewModel : ObservableObject {
#Published var items: [Cat]
init(items: [Cat]) {
self._items = .init(initialValue: items)
}
public func createViewModel(item: Cat) -> CatView.ViewModel {
.init(data: item)
}
}
}
struct CatView : View {
#ObservedObject private(set) var viewModel: ViewModel
var body: some View {
VStack {
TextField("Name", text: self.$viewModel.data.name)
TextField("Breed", text: self.$viewModel.data.breed)
}
}
}
extension CatView {
class ViewModel : ObservableObject {
#Published var data: Cat
init(data: Cat) {
self._data = .init(initialValue: data)
}
}
}
How to properly transfer changes from CatView.ViewModel to CatListView.ViewModel?
Is Binding possible? But I think this is a variant of the connection between View and View.
Perhaps this option will be correct?
extension CatListView {
class ViewModel : ObservableObject {
#Published private(set) var items: [Cat]
private var cancelItems: [Int: AnyCancellable] = [:]
init(items: [Cat]) {
self._items = .init(initialValue: items)
}
public func createViewModel(item: Cat) -> CatView.ViewModel {
let model: CatView.ViewModel = .init(data: item)
if let index = self.items.firstIndex(where: { $0.id == item.id }) {
self.cancelItems[index] = model.$data
.dropFirst()
.removeDuplicates()
.sink {
self.items[index] = $0
}
}
return model
}
}
}

You have to manage these items:
Model: single Cat Model
ViewModel: Cat Collection Model that stores all cats and fetch them
View 1 with cats List
View 2 (optional) A View with single cat, can be inside View 1 or separate, but for illustrate this we will separate the View 2.
struct Cat: Identifiable, Hashable {
let id = UUID()
var name: String
var colorDescription: String
}
class CatsModel: ObservableObject {
#Published var catsBag: [Cat]
init() {
// Fetch here the cats from your server
}
// CRUD cicle
func create(_ cat: Cat) { ... }
func update(_ cat: Cat) { ... }
func delete(_ cat: Cat) { ... }
}
struct CatsList: View {
#StateObject var catsModel = CatsModel()
var body: some View {
List {
ForEach(catsModel.catsBag.indices, id: \.self) { catIndice in
CatCell(cat: $catsModel.catsBag[catIndice])
}
}
}
}
struct CatCell: View {
#Binding var cat: Cat
var body: some View {
TextField("The cat name is: ", text: $cat.name)
.padding()
}
}
You will appreciate at this point that if you delete the last cat of that list the app crashes. This could be a SwiftUI error, you can find the solution here: https://www.swiftbysundell.com/articles/bindable-swiftui-list-elements/

Related

Keeping the original datasource in sync with downstream bindings

If I have a collection of fruits, and I pass one of them to a detail view, how do I edit that item so that both the item and it's original datasource are updated?
final class Merchant: ObservableObject {
#Published
var selection: Fruit?
#Published
var fruits = [
Fruit(name: "Banana"),
Fruit(name: "Apple")
]
}
struct FruitsView: View {
#EnvironmentObject var merchant: Merchant
var body: some View {
VStack {
ForEach(merchant.fruits) { fruit in
Button {
merchant.selection = fruit
} label: {
Text(fruit.name)
}
.buttonStyle(.borderedProminent)
}
}
.sheet(item: $merchant.selection, content: {
FruitDetailView(item: $0)
})
}
}
struct FruitDetailView: View {
let item: Fruit
init(item: Fruit) {
self.item = item
}
var body: some View {
VStack {
Text(item.name)
Button("Press Me") {
item.name = "Watermelon" // error
}
}
}
}
Changing the item on FruitDetailView to a binding doesn't change the original datasource.
There are a number of ways to achieve what you ask. This is one simple way using the model constructs you already have. It uses the Merchant selection to update the Merchant fruits data.
struct ContentView: View {
#StateObject var merchant = Merchant()
var body: some View {
FruitsView().environmentObject(merchant)
}
}
struct Fruit: Identifiable {
let id = UUID()
var name: String
}
final class Merchant: ObservableObject {
#Published var selection: Fruit? = nil {
didSet {
if selection != nil,
let index = fruits.firstIndex(where: {$0.id == selection!.id}) {
fruits[index].name = selection!.name
}
}
}
#Published var fruits = [Fruit(name: "Banana"), Fruit(name: "Apple")]
}
struct FruitsView: View {
#EnvironmentObject var merchant: Merchant
var body: some View {
VStack {
ForEach(merchant.fruits) { fruit in
Button {
merchant.selection = fruit
} label: {
Text(fruit.name)
}.buttonStyle(.borderedProminent)
}
}
.sheet(item: $merchant.selection) { _ in
FruitDetailView().environmentObject(merchant)
}
}
}
struct FruitDetailView: View {
#EnvironmentObject var merchant: Merchant
var body: some View {
VStack {
Text(merchant.selection?.name ?? "no selection name")
Button("Press Me") {
merchant.selection?.name = "Watermelon"
}
}
}
}
EDIT-1:
This is another way of keeping the model in sync. It uses a function updateFruits in the Merchant ObservableObject class, to update the model's data.
It separates the UI interaction part using a local #State var selection: Fruit? from the main data in the Merchant model.
final class Merchant: ObservableObject {
#Published var fruits = [Fruit(name: "Banana"), Fruit(name: "Apple")]
func updateFruits(with item: Fruit) {
if let index = fruits.firstIndex(where: {$0.id == item.id}) {
fruits[index].name = item.name
}
}
}
struct FruitsView: View {
#EnvironmentObject var merchant: Merchant
#State var selection: Fruit?
var body: some View {
VStack {
ForEach(merchant.fruits) { fruit in
Button {
selection = fruit
} label: {
Text(fruit.name)
}.buttonStyle(.borderedProminent)
}
}
.sheet(item: $selection) { item in
FruitDetailView(item: item).environmentObject(merchant)
}
}
}
struct FruitDetailView: View {
#EnvironmentObject var merchant: Merchant
#State var item: Fruit
var body: some View {
VStack {
Text(item.name)
Button("Press Me") {
item.name = "Watermelon"
merchant.updateFruits(with: item)
}
}
}
}

Why does the TextField in the List stop updating?

I started studying SwiftUI and wanted to make a prototype of standard reminders, like in an iPhone. It seems nothing complicated, there is a List, in each cell a TextField.
But I ran into a problem: when we change the text in the TextField using onChange, then we accordingly tell the view model to update our objects.
And when the objects are updated, the entire List is redrawn and the editing of the current TextField is reset (you can neither remove more than one character, nor add). You have to click on the text again to continue editing.
Does anyone know how to treat this?
This is my code:
import SwiftUI
struct Fruit: Identifiable {
let id = UUID()
let name: String
func updateName(newName: String) -> Fruit {
return Fruit(name: newName)
}
}
class ViewModel: ObservableObject {
#Published var fruits: [Fruit] = [Fruit(name: "apple"), Fruit(name: "banana"), Fruit(name: "orange")]
func updateName(newName: String, fruit: Fruit) {
if let index = fruits.firstIndex(where: { $0.id == fruit.id }) {
fruits[index] = fruit.updateName(newName: newName)
}
}
}
struct ListView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
List {
ForEach(viewModel.fruits) { fruit in
ListViewRow(fruit: fruit)
}
}
.environmentObject(viewModel)
}
}
struct ListViewRow: View {
#EnvironmentObject var viewModel: ViewModel
#State var fruitTextField: String
let fruit: Fruit
init(fruit: Fruit) {
self.fruit = fruit
_fruitTextField = State(initialValue: fruit.name)
}
var body: some View {
TextField("", text: $fruitTextField)
.onChange(of: fruitTextField) { newValue in
viewModel.updateName(newName: newValue, fruit: fruit)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ListView()
}
}
You can simplify it even more with:
struct Fruit: Identifiable {
let id = UUID()
var name: String
}
class ViewModel: ObservableObject {
#Published var fruits: [Fruit] = [Fruit(name: "apple"), Fruit(name: "banana"), Fruit(name: "orange")]
}
struct ListView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
List {
ForEach($viewModel.fruits) { $fruit in
ListViewRow(fruit: $fruit)
}
}
}
}
struct ListViewRow: View {
#Binding var fruit: Fruit
var body: some View {
TextField("", text: $fruit.name)
}
}
That being said, you really need to view the Apple Swift Tutorials that were linked in the comments.
Edit: Full Project Code for Lorem Ipsum:
//
// ContentView.swift
// FruitApp
//
// Created by Developer on 11/27/21.
//
import SwiftUI
struct Fruit: Identifiable {
let id = UUID()
var name: String
}
class ViewModel: ObservableObject {
#Published var fruits: [Fruit] = [Fruit(name: "apple"), Fruit(name: "banana"), Fruit(name: "orange")]
}
struct ListView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
List {
ForEach($viewModel.fruits) { $fruit in
ListViewRow(fruit: $fruit)
}
}
}
}
struct ListViewRow: View {
#Binding var fruit: Fruit
var body: some View {
TextField("", text: $fruit.name)
}
}
struct ContentView: View {
var body: some View {
ListView()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Why .append doesn't reload swiftUI view

I have the following view hierarchy
Nurse List View > Nurse Card > Favorite button
Nurse List View
struct NurseListView: View {
#State var data: [Nurse] = []
var body: some View {
List {
ForEach(data.indices, id: \.self) { index in
NurseCard(data: self.$data[index])
}
}
}
}
Nurse Card
struct NurseCard: View {
#Binding var data: Nurse
var body: some View {
FavoriteActionView(data:
Binding(
get: { self.data },
set: { self.data = $0 as! Nurse }
)
)
}
}
Favorite Action View
struct FavoriteActionView: View {
#Binding var data: FavoritableData
var body: some View {
Button(action: {
self.toggleFavIcon()
}) {
VStack {
Image(data.isFavorite ? "fav-icon" : "not-fav-icon")
Text(String(data.likes.count))
}
}
}
private func toggleFavIcon() {
if data.isFavorite {
if let index = data.likes.firstIndex(of: AppState.currentUser.uid) {
data.likes.remove(at: index)
}
} else {
data.likes.append(AppState.currentUser.uid)
}
}
}
When toggleFavIcon execute, it append/remove the user id from the likes property in data object but I can't see the change unless I go back to previous page and reopen the page. What I am missing here?
As Asperi wrote, using an ObservableObject would work well here. Something like this:
class FavoritableData: ObservableObject {
#Published var likes: [String] = []
#Published var isFavorite = false
}
struct FavoriteActionView: View {
#ObservedObject var data: FavoritableData
var body: some View {
Button(action: {
self.toggleFavIcon()
}) {
VStack {
Image(data.isFavorite ? "fav-icon" : "not-fav-icon")
Text(String(data.likes.count))
}
}
}
private func toggleFavIcon() {
if data.isFavorite {
if let index = data.likes.firstIndex(of: AppState.currentUser.uid) {
data.likes.remove(at: index)
}
} else {
data.likes.append(AppState.currentUser.uid)
}
}
}

Bidirectional binding with SwiftUI and Combine

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

Changes to array objects not saving to main ObservableObject

I'm starting with SwiftUI and I'm running into a roadblock with array items of an ObservableObject not saving to the main object.
Main object:
class Batch: Codable, Identifiable, ObservableObject {
let id: String
var items = [Item]()
}
Item object:
class Item: Codable, Identifiable, ObservableObject {
let id: String
var description: String
}
I have a BatchView which I pass a batch into:
struct BatchView: View {
#ObservedObject var batch: Batch
var body: some View {
List {
ForEach(batch.items) { item in
ItemView(item: item)
}
}
.navigationBarTitle(batch.items.reduce("", { $0 + $1.description }))
}
}
In the ItemView I change the description:
struct ItemView: View {
#ObservedObject var item: Item
#State private var descr = ""
var body: some View {
VStack(alignment: .leading) {
Text("MANUFACTURED")
TextField("", text: $descr) {
self.updateDescr(descr: self.descr)
}
}
}
private func updateDescr(descr: String) {
item.description = descr
}
}
But when I update the description for a batch item, the title of BatchView doesn't change, so the changes to the Item isn't coming back to the root Batch.
How do I make the above work?
This answer helped me. I had to explicitly add #Published in front of the variable I was changing.