#ObservedObject does not get updated - ios

I'm trying to update a view with a simple Observable pattern, but it doesn't happen for some reason. The Publisher gets updated, but the subscriber doesn't. I've simplified to the code below. When you click the Add button, the view doesn't get updated and also the variable.
I'm using this function (NetworkManager.shared.saveNew()), because I update after a CloudKit notification. If you know a workaround, I'd be pleased to know!
import SwiftUI
import Combine
public class NetworkManager: ObservableObject {
static let shared = NetworkManager()
#Published var list = [DataSource]()
init() {
self.list = [DataSource(id: "Hello", action: [1], link: "http://hello.com", year: 2020)]
}
func saveNew(item: DataSource) {
self.list.append(item)
}
}
struct DataSource: Identifiable, Codable {
var id: String
var action: [Int]
var link: String
var year: Int
init(id: String, action: [Int], link: String, year: Int) {
self.id = id
self.action = action
self.link = link
self.year = year
}
}
struct ContentView: View {
#ObservedObject var list = NetworkManager()
var body: some View {
VStack {
Button("Add") {
NetworkManager.shared.saveNew(item: DataSource(id: "GoodBye", action: [1], link: "http://goodbye", year: 2030))
}
List(list.list, id:\.id) { item in
Text(item.id)
}
}
}
}

It should be used same instance of NetworkManager, so here is fixed variant (and better to name manager as manager but not as list)
struct ContentView: View {
#ObservedObject var manager = NetworkManager.shared // << fix !!
var body: some View {
VStack {
Button("Add") {
self.manager.saveNew(item: DataSource(id: "GoodBye", action: [1], link: "http://goodbye", year: 2030))
}
List(manager.list, id:\.id) { item in
Text(item.id)
}
}
}
}

Related

Binding with optional struct

I get the error "Value of optional type 'Photo?' must be unwrapped to refer to member 'name' of wrapped base type 'Photo'" when I try to send a optional struct on a binding of a TextField.
The content view code example:
import SwiftUI
struct ContentView: View {
#StateObject var viewModel = ContentViewViewModel()
private var photos = [
Photo(id: UUID(), name: "Exclamation", data: (UIImage(systemName: "exclamationmark.triangle")?.jpegData(compressionQuality: 1))!),
Photo(id: UUID(), name: "Circle", data: (UIImage(systemName: "circle.fill")?.jpegData(compressionQuality: 1))!)
]
var body: some View {
VStack {
DetailView(viewModel: viewModel)
}
.onAppear {
viewModel.selectedPhoto = photos[0]
}
}
}
The view model code example:
import Foundation
#MainActor
final class ContentViewViewModel: ObservableObject {
#Published var photos = [Photo]()
#Published var selectedPhoto: Photo?
}
The detail view code example (that uses the content view's view model):
import SwiftUI
struct DetailView: View {
#ObservedObject var viewModel: ContentViewViewModel
var body: some View {
TextField("Photo name here...", text: $viewModel.selectedPhoto.name)
}
}
Note that for some reasons I need the selectedPhoto property be optional.
You can create your own custom #Binding, so you can handle the process between getting data and set the updated here is an example:
If what you want is to select the photo in a List or Foreach you can bind directly to the array.
import SwiftUI
struct Photo {
let id: UUID
var name: String
let data: Data
}
#MainActor
final class ContentViewViewModel: ObservableObject {
#Published var photos = [Photo]()
#Published var selectedPhoto: Photo?
}
struct optionalBinding: View {
#StateObject var viewModel = ContentViewViewModel()
private var photos = [
Photo(id: UUID(), name: "Exclamation", data: (UIImage(systemName: "exclamationmark.triangle")?.jpegData(compressionQuality: 1))!),
Photo(id: UUID(), name: "Circle", data: (UIImage(systemName: "circle.fill")?.jpegData(compressionQuality: 1))!)
]
var body: some View {
VStack {
DetailView2(viewModel: viewModel)
Button("Select Photo") {
viewModel.selectedPhoto = photos[0]
}
}
}
}
struct DetailView2: View {
#ObservedObject var viewModel: ContentViewViewModel
var body: some View {
Text(viewModel.selectedPhoto?.name ?? "No photo selected")
TextField("Photo name here...", text: optionalBinding())
}
func optionalBinding() -> Binding<String> {
return Binding<String>(
get: {
guard let photo = viewModel.selectedPhoto else {
return ""
}
return photo.name
},
set: {
guard let _ = viewModel.selectedPhoto else {
return
}
viewModel.selectedPhoto?.name = $0
//Todo: also update the array
}
)
}
}

SwiftUI - Should you use `#State var` or `let` in child view when using ForEach

I think I've a gap in understanding what exactly #State means, especially when it comes to displaying contents from a ForEach loop.
My scenario: I've created minimum reproducible example. Below is a parent view with a ForEach loop. Each child view has aNavigationLink.
// Parent code which passes a Course instance down to the child view - i.e. CourseView
struct ContentView: View {
#StateObject private var viewModel: ViewModel = .init()
var body: some View {
NavigationView {
VStack {
ForEach(viewModel.courses) { course in
NavigationLink(course.name + " by " + course.instructor) {
CourseView(course: course, viewModel: viewModel)
}
}
}
}
}
}
class ViewModel: ObservableObject {
#Published var courses: [Course] = [
Course(name: "CS101", instructor: "John"),
Course(name: "NS404", instructor: "Daisy")
]
}
struct Course: Identifiable {
var id: String = UUID().uuidString
var name: String
var instructor: String
}
Actual Dilemma: I've tried two variations for the CourseView, one with let constant and another with a #State var for the course field. Additional comments in the code below.
The one with the let constant successfully updates the child view when the navigation link is open. However, the one with #State var doesn't update the view.
struct CourseView: View {
// Case 1: Using let constant (works as expected)
let course: Course
// Case 2: Using #State var (doesn't update the UI)
// #State var course: Course
#ObservedObject var viewModel: ViewModel
var body: some View {
VStack {
Text("\(course.name) by \(course.instructor)")
Button("Edit Instructor", action: editInstructor)
}
}
// Case 1: It works and UI gets updated
// Case 2: Doesn't work as is.
// I've to directly update the #State var instead of updating the clone -
// which sometimes doesn't update the var in my actual project
// (that I'm trying to reproduce). It definitely works here though.
private func editInstructor() {
let instructor = course.instructor == "Bob" ? "John" : "Bob"
var course = course
course.instructor = instructor
save(course)
}
// Simulating a database save, akin to something like GRDB
// Here, I'm just updating the array to see if ForEach picks up the changes
private func save(_ courseToSave: Course) {
guard let index = viewModel.courses.firstIndex(where: { $0.id == course.id }) else {
return
}
viewModel.courses[index] = courseToSave
}
}
What I'm looking for is the best practice for a scenario where looping through an array of models is required and the model is updated in DB from within the child view.
Here is a right way for you, do not forget that we do not need put logic in View! the view should be dummy as possible!
struct ContentView: View {
#StateObject private var viewModel: ViewModel = ViewModel.shared
var body: some View {
NavigationView {
VStack {
ForEach(viewModel.courses) { course in
NavigationLink(course.name + " by " + course.instructor, destination: CourseView(course: course, viewModel: viewModel))
}
}
}
}
}
struct CourseView: View {
let course: Course
#ObservedObject var viewModel: ViewModel
var body: some View {
VStack {
Text("\(course.name) by \(course.instructor)")
Button("Update Instructor", action: { viewModel.update(course) })
}
}
}
class ViewModel: ObservableObject {
static let shared: ViewModel = ViewModel()
#Published var courses: [Course] = [
Course(name: "CS101", instructor: "John"),
Course(name: "NS404", instructor: "Daisy")
]
func update(_ course: Course) {
guard let index = courses.firstIndex(where: { $0.id == course.id }) else {
return
}
courses[index] = Course(name: course.name, instructor: (course.instructor == "Bob") ? "John" : "Bob")
}
}
struct Course: Identifiable {
let id: String = UUID().uuidString
var name: String
var instructor: String
}

SwiftUI MVVM Binding List Item

I am trying to create a list view and a detailed screen like this:
struct MyListView: View {
#StateObject var viewModel: MyListViewModel = MyListViewModel()
LazyVStack {
// https://www.swiftbysundell.com/articles/bindable-swiftui-list-elements/
ForEach(viewModel.items.identifiableIndicies) { index in
MyListItemView($viewModel.items[index])
}
}
}
class MyListViewModel: ObservableObject {
#Published var items: [Item] = []
...
}
struct MyListItemView: View {
#Binding var item: Item
var body: some View {
NavigationLink(destination: MyListItemDetailView(item: $item), label: {
...
})
}
}
struct MyListItemDetailView: View {
#Binding var item: Item
#StateObject var viewModel: MyListViewItemDetailModel
init(item: Binding<Item>) {
viewModel = MyListViewItemDetailModel(item: item)
}
var body: some View {
...
}
}
class MyListViewItemDetailModel: ObservableObject {
var item: Binding<Item>
...
}
I am not sure what's wrong with it, but I found that item variables are not synced with each other, even between MyListItemDetailView and MyListItemDetailViewModel.
Is there anyone who can provide the best practice and let me know what's wrong in my implmentation?
I think you should think about a minor restructure of your code, and use only 1
#StateObject/ObservableObject. Here is a cut down version of your code using
only one StateObject source of truth:
Note: AFAIK Binding is meant to be used in View struct not "ordinary" classes.
PS: what is identifiableIndicies?
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct Item: Identifiable {
let id = UUID().uuidString
var name: String = ""
}
struct MyListView: View {
#StateObject var viewModel: MyListViewModel = MyListViewModel()
var body: some View {
LazyVStack {
ForEach(viewModel.items.indices) { index in
MyListItemView(item: $viewModel.items[index])
}
}
}
}
class MyListViewModel: ObservableObject {
#Published var items: [Item] = [Item(name: "one"), Item(name: "two")]
}
struct MyListItemView: View {
#Binding var item: Item
var body: some View {
NavigationLink(destination: MyListItemDetailView(item: $item)){
Text(item.name)
}
}
}
class MyAPIModel {
func fetchItemData(completion: #escaping (Item) -> Void) {
// do your fetching here
completion(Item(name: "new data from api"))
}
}
struct MyListItemDetailView: View {
#Binding var item: Item
let myApiModel = MyAPIModel()
var body: some View {
VStack {
Button(action: fetchNewData) {
Text("Fetch new data")
}
TextField("edit item", text: $item.name).border(.red).padding()
}
}
func fetchNewData() {
myApiModel.fetchItemData() { itemData in
item = itemData
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
MyListView()
}.navigationViewStyle(.stack)
}
}
EDIT1:
to setup an API to call some functions, you could use something like this:
class MyAPI {
func fetchItemData(completion: #escaping (Item) -> Void) {
// do your stuff
}
}
and use it to obtain whatever data you require from the server.
EDIT2: added some code to demonstrate the use of an API.

For a List-Detail interface - Data is updated in the Detail View, and the Data is changed but not immediately reflected in the Detail view

I am using SwiftUI on the Apple Watch and trying to use #ObservableObject, #ObservedObject, and #Binding correctly. I'm updating a value in a DetailView, and I want to have it reflected locally, as well as have the data changed globally. The code below works, but I am using a kludge to force the DetailView to redraw itself:
Is there a better way?
-------------- ContentView.swift ---------------
import Combine
import SwiftUI
struct person: Identifiable {
var id:Int = 0
var name:String
init( id: Int, name:String) {
self.id = id
self.name = name
}
}
class AppData: ObservableObject {
#Published var people:[person] = [person(id:0, name:"John"),
person(id:1, name:"Bret"),
person(id:2,name:"Sue"),
person(id:3,name:"Amy")]
}
var gAppData = AppData()
struct ContentView: View {
#ObservedObject var model:AppData
var body: some View {
List( model.people.indices ){ index in
NavigationLink(destination: DetailView(person:self.$model.people[index])) { Text(self.model.people[index].name) }
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(model:gAppData)
}
}
-------------- DetailView.swift ---------------
import SwiftUI
struct DetailView: View {
#Binding var person: person
// Created an unnecessary var to force a redreaw of the view
#State var doRedraw:Bool = true
var body: some View {
VStack(){
Text(person.name)
Button(action:{ self.person.name = "Bob"; self.doRedraw = false }) {
Text("Set Name to Bob")
}
}
}
}
struct DestView_Previews: PreviewProvider {
static var previews: some View {
DetailView(person:.constant(person( id:0, name:"John"))) // what does ".constant" actually do?
}
}
The problem here is because your view redraws only when you changes the #State or #Binding variable. Here you do not change the Person variable, but its property, which should not affect the user interface (because you didn't say to do this). I changed your code for a little for showing how to achieve this effect, you can go ahead from this point. You need to remember, what exactly affect UI:
class Person: Identifiable, ObservableObject { // better to assign struct/class names using UpperCamelCase
#Published var name:String // now change of this variable will affect UI
var id:Int = 0
init( id: Int, name:String) {
self.id = id
self.name = name
}
}
// changes in DetailView
struct DetailView: View {
#EnvironmentObject var person: Person
var body: some View {
VStack(){
Text(person.name)
Button(action:{ self.person.name = "Bob" }) {
Text("Set Name to Bob")
}
}
}
}
// preview
struct DetailViewWithoutGlobalVar_Previews: PreviewProvider {
static var previews: some View {
DetailView()
.environmentObject(Person(id: 1, name: "John"))
}
}
update: full code for List and Detail
import SwiftUI
class Person: Identifiable, ObservableObject { // better to assign type names using UpperCamelCase
#Published var name: String //{
var id: Int = 0
init( id: Int, name:String) {
self.id = id
self.name = name
}
func changeName(_ newName: String) {
self.name = newName
}
}
class AppData: ObservableObject {
#Published var people: [Person] = [Person(id:0, name:"John"),
Person(id:1, name:"Bret"),
Person(id:2,name:"Sue"),
Person(id:3,name:"Amy")]
}
struct ContentViewWithoutGlobalVar: View {
#EnvironmentObject var model: AppData
var body: some View {
NavigationView { // you forget something to navigate between views
List(model.people.indices) { index in
NavigationLink(destination: DetailView()
.environmentObject(self.model.people[index])) {
PersonRow(person: self.$model.people[index])
}
}
}
}
}
struct PersonRow: View {
#Binding var person: Person // this struct will see changes in Person and show them
var body: some View {
Text(person.name)
}
}
struct DetailView: View {
#EnvironmentObject var person: Person
var body: some View {
VStack(){
Text(self.person.name)
Button(action:{ self.person.changeName("Bob") }) {
Text("Set Name to Bob")
}
}
}
}
struct ContentViewWithoutGlobalVar_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentViewWithoutGlobalVar()
.environmentObject(AppData())
DetailView()
.environmentObject(Person(id: 0, name: "John"))
}
}
}

is there an alternative for didSet in SwiftUI?

I want to change name right after I get User(). DidSet does not work here. is there an alternative for didSet in SwiftUI?
struct Person: Identifiable {
let id = UUID()
var name: String
var number: Int
}
class User: ObservableObject {
#Published var array = [Person(name: "Nick", number: 3),
Person(name: "John", number: 2)
]
}
struct ContentView: View {
#ObservedObject var user = User() {
didSet {
user.array[0].name = "LoL"
}
}
var body: some View {
VStack {
ForEach (user.array) { row in
Text(row.name)
}
}
}
}
If I correctly understood your expectation (having your code) there are couple of options to reach the goal:
Option 1: Set up created user in init (as properties created before init)
init() {
self.user.array[0].name = "LoL"
}
Option 2: Set up it on view appearance
VStack {
ForEach (user.array) { row in
Text(row.name)
}
}
.onAppear {
self.user.array[0].name = "LoL"
}

Resources