Why does the TextField in the List stop updating? - swiftui

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

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

How can I navigate to a detail view of an item by using an #EnvironmentObject to route the views?

I have the following code in SwiftUI. I am expecting it to navigate from the list view to the PetView() with the proper name showing when tapping on one of the items in the ForEach loop or the button that says "Go to first pet". However, when I tap on an item or the button, the app doesn't do anything. What am I doing wrong? Thank you for your help!
import SwiftUI
#main
struct TestListAppApp: App {
var body: some Scene {
WindowGroup {
ContentView().environmentObject(ViewRouter())
}
}
}
import SwiftUI
struct ContentView: View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
ForEach(viewRouter.pets) { pet in
NavigationLink(
destination: PetView(),
tag: pet,
selection: $viewRouter.selectedPet,
label: {
Text(pet.name)
}
)
}
Button("Go to first pet.") {
viewRouter.selectedPet = viewRouter.pets[0]
}
}
}
import Foundation
class ViewRouter: ObservableObject {
#Published var selectedPet: Pet? = nil
#Published var pets: [Pet] = [Pet(name: "Louie"), Pet(name: "Fred"), Pet(name: "Stanley")]
}
import SwiftUI
struct PetView: View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
Text(viewRouter.selectedPet!.name)
}
}
import Foundation
struct Pet: Identifiable, Hashable {
var name: String
var id: String { name }
}
try this:
#main
struct TestListAppApp: App {
#StateObject var viewRouter = ViewRouter()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(viewRouter)
}
}
}
struct PetView: View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
if let pet = viewRouter.selectedPet {
Text(pet.name)
} else {
EmptyView()
}
}
}
struct ContentView: View {
#EnvironmentObject var viewRouter: ViewRouter
var body: some View {
NavigationView {
List {
ForEach(viewRouter.pets) { pet in
NavigationLink(destination: PetView(),
tag: pet,
selection: $viewRouter.selectedPet,
label: {
Text(pet.name)
})
}
Button("Go to first pet.") {
viewRouter.selectedPet = viewRouter.pets[0]
}
}
}
}
}

SwiftUI Model by ViewModel edit other ViewModel

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/

How can I have multiple instance of a Class/Model in SwiftUI?

The first part of question is answered. Let's elaborate this example to:
TextField view:
struct CreateNewCard: View {
#ObservedObject var viewModel: CreateNewCardViewModel
var body: some View {
TextField("placeholder...", text: $viewModel.definition)
.foregroundColor(.black)
}
}
ViewModel:
class CreateNewCardViewModel: ObservableObject {
#Published var id: Int
#Published var definition: String = ""
}
Main View:
struct MainView: View {
#State var showNew = false
var body: some View {
ForEach(0...10, id: \.self) { index in // <<<---- this represents the id
Button(action: { showNew = true }, label: { Text("Create") })
.sheet(isPresented: $showNew, content: {
// now I have to pass the id, but this
// leads to that I create a new viewModel every time, right?
CreateNewCard(viewModel: CreateNewCardViewModel(id: index))
})
}
}
My problem is now that when I type something into the TextField and press the return button on the keyboard the text is removed.
This is the most strange way of coding that i seen, how ever I managed to make it work:
I would like say that you can use it as leaning and testing, but not good plan for real app, How ever it was interesting to me to make it working.
import SwiftUI
struct ContentView: View {
var body: some View {
MainView()
}
}
class CreateNewCardViewModel: ObservableObject, Identifiable, Equatable {
init(_ id: Int) {
self.id = id
}
#Published var id: Int
#Published var definition: String = ""
#Published var show = false
static func == (lhs: CreateNewCardViewModel, rhs: CreateNewCardViewModel) -> Bool {
return lhs.id == rhs.id
}
}
let arrayOfModel: [CreateNewCardViewModel] = [ CreateNewCardViewModel(0), CreateNewCardViewModel(1), CreateNewCardViewModel(2),
CreateNewCardViewModel(3), CreateNewCardViewModel(4), CreateNewCardViewModel(5),
CreateNewCardViewModel(6), CreateNewCardViewModel(7), CreateNewCardViewModel(8),
CreateNewCardViewModel(9) ]
struct ReadModelView: View {
#ObservedObject var viewModel: CreateNewCardViewModel
var body: some View {
TextField("placeholder...", text: $viewModel.definition)
.foregroundColor(.black)
}
}
struct MainView: View {
#State private var arrayOfModelState = arrayOfModel
#State private var showModel: Int?
#State private var isPresented: Bool = false
var body: some View {
VStack {
ForEach(Array(arrayOfModelState.enumerated()), id:\.element.id) { (index, item) in
Button(action: { showModel = index; isPresented = true }, label: { Text("Show Model " + item.id.description) }).padding()
}
if let unwrappedValue: Int = showModel {
Color.clear
.sheet(isPresented: $isPresented, content: { ReadModelView(viewModel: arrayOfModelState[unwrappedValue]) })
}
}
.padding()
}
}

How do I update a List in SwiftUI?

My code is a little more complex than this so I created an example that gets the same error.
When I navigate into a view, I have a function I want to perform with a variable passed into this view. That function then produces an array. I then want to put that array into a List, but I get an error.
How do I get the List to show the produced array?
I think the issue is the List can't be updated because it already has the declared blank array.
struct ContentView : View {
#State var array = [String]()
var body: some View {
List(self.array,id: \.self) { item in
Text("\(item)")
}
.onAppear(perform: createArrayItems)
}
func createArrayItems() {
array = ["item1", "item2", "item3", "item4", "item5"]
}
}
You can use ObservableObject data providers(eg : ViewModel) with #Published properties.
struct ListView: View {
#ObservedObject var viewModel = ListViewModel()
var body: some View {
NavigationView {
List(){
ForEach(viewModel.items) { item in
Text(item)
}
}
}
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ListView()
}
}
#endif
class ListViewModel: ObservableObject {
#Published var items = ["item1", "item2", "item3", "item4", "item5","item6"]
func addItem(){
items.append("item7")
}
}
You can use combine framework to update the list.
Whenever a change is made in DataProvider Object it will automatically update the list.
struct ContentView : View {
#EnvironmentObject var data: DataProvider
var body: some View {
NavigationView {
NavigationButton(destination: SecondPage()) {
Text("Go to Second Page")
}
List {
ForEach(data.array.identified(by: \.self)) { item in
Text("\(item)")
}
}
}
}
}
Add items in the list
struct SecondPage : View {
#State var counter = 1
#EnvironmentObject var tempArray: DataProvider
var body: some View {
VStack {
Button(action: {
self.tempArray.array.append("item\(self.counter)")
self.counter += 1
}) {
Text("Add items")
}
Text("Number of items added \(counter-1)")
}
}
}
It will simply notify the change
import Combine
final class DataProvider: BindableObject {
let didChange = PassthroughSubject<DataProvider, Never>()
var array = [String]() {
didSet {
didChange.send(self)
}
}
}
You also need to do some update in the SceneDelegate. This update ensures that ContentView has a DataProvider object in the environment.
window.rootViewController = UIHostingController(rootView: ContentView().environmentObject(DataProvider()))
#txagPman
I too have your problem to understand how to modify a list.
I was able to write this code.
I hope it's useful.
import SwiftUI
struct ContentView: View {
#State private var array = createArrayItems()
// #State private var array = [""] - This work
// #State private var array = [] - This not work
#State private var text = ""
var body: some View {
VStack {
TextField("Text", text: $text, onCommit: {
// self.array = createArrayItems() - This work after press return on textfield
self.array.append(self.text)
}).padding()
List (self.array, id: \.self) {item in
Text("\(item)")
}
}
// .onAppear {
// self.array = createArrayItems() - This not work
// }
}
}
func createArrayItems() -> [String] {
return ["item_01","item_02","item_03","item_04" ]
}
A dumb UI is a good UI
Keep your views dumb try the following code to create a dynamic List
import UIKit
import SwiftUI
import PlaygroundSupport
struct ContentView : View {
#State var array = [String]()
var body: some View {
List{
ForEach(array.identified(by: \.self)) { item in
Text("\(item)")
}
}
}
}
func createArrayItems()->[String] {
return ["item1", "item2", "item3", "item4", "item5","item6"]
}
PlaygroundPage.current.liveView = UIHostingController(rootView: ContentView(array: createArrayItems()))
Use this:
class ObservableArray<T>: ObservableObject {
#Published var array: [T]
init(array: [T] = ) {
self.array = array
}
init(repeating value: T, count: Int) {
array = Array(repeating: value, count: count)
}
}
struct YourView: View {
#ObservedObject var array = ObservableArray<String>()
var body: some View {
}
}