How to observer a property in swift ui - swift

How to observe property value in SwiftUI.
I know some basic publisher and observer patterns. But here is a scenario i am not able to implement.
class ScanedDevice: NSObject, Identifiable {
//some variables
var currentStatusText: String = "Pending"
}
here CurrentStatusText is changed by some other callback method that update the status.
Here there is Model class i am using
class SampleModel: ObservableObject{
#Published var devicesToUpdated : [ScanedDevice] = []
}
swiftui component:
struct ReviewView: View {
#ObservedObject var model: SampleModel
var body: some View {
ForEach(model.devicesToUpdated){ device in
Text(device.currentStatusText)
}
}
}
Here in UI I want to see the real-time status
I tried using publisher inside ScanDevice class but sure can to use it in 2 layer

You can observe your class ScanedDevice, however you need to manually use a objectWillChange.send(),
to action the observable change, as shown in this example code.
class ScanedDevice: NSObject, Identifiable {
var name: String = "some name"
var currentStatusText: String = "Pending"
init(name: String) {
self.name = name
}
}
class SampleViewModel: ObservableObject{
#Published var devicesToUpdated: [ScanedDevice] = []
}
struct ReviewView: View {
#ObservedObject var viewmodel: SampleViewModel
var body: some View {
VStack (spacing: 33) {
ForEach(viewmodel.devicesToUpdated){ device in
HStack {
Text(device.name)
Text(device.currentStatusText).foregroundColor(.red)
}
Button("Change \(device.name)") {
viewmodel.objectWillChange.send() // <--- here
device.currentStatusText = UUID().uuidString
}.buttonStyle(.bordered)
}
}
}
}
struct ContentView: View {
#StateObject var viewmodel = SampleViewModel()
var body: some View {
ReviewView(viewmodel: viewmodel)
.onAppear {
viewmodel.devicesToUpdated = [ScanedDevice(name: "device-1"), ScanedDevice(name: "device-2")]
}
}
}

Related

Using Sub Interactor for an Interactor in SwiftUI - Failed to produce diagnostic for expression

I have created a simple SubInteractor with #Published var and then there is a MainInteractor using that SubInteractor and a MainView that has access to MainInteractor.
That MainView needs to pass the #Published var down to a subview by accessing it through the MainInteractor.
Using the following code to create a SubInteractor inside MainInteractor gives an error:
Failed to produce diagnostic for expression; please submit a bug report (https://swift.org/contributing/#reporting-bugs) and include the project
import Foundation
import Combine
import SwiftUI
protocol SubInteractorProtocol {
var name: String { get set }
}
class SubInteractor: ObservableObject & SubInteractorProtocol {
#Published var name: String = "name"
}
protocol MainInteractorProtocol {
associatedtype SubType: ObservableObject & SubInteractorProtocol
var subInteractor: SubType { get set }
}
class MainInteractor: ObservableObject & MainInteractorProtocol {
#ObservedObject var subInteractor: SubInteractor
init(subInteractor: SubInteractor) {
self.subInteractor = subInteractor
}
}
struct NameView: View {
#Binding var name: String
var body: some View {
Text("\(name)")
}
}
struct MainView<Interactor: ObservableObject & MainInteractorProtocol>: View {
#EnvironmentObject var c: Interactor
var body: some View {
NameView(name: $c.subInteractor.name)
}
}
I also tried changing the SubInteractorProtocol and SubInteractor to this (but get the same error):
protocol SubInteractorProtocol {
var name: String { get set }
var namePublished: Published<String> { get }
var namePublisher: Published<String>.Publisher { get }
}
class SubInteractor: ObservableObject & SubInteractorProtocol {
#Published var name = "name"
var namePublished: Published<String> { _name }
var namePublisher: Published<String>.Publisher { $name }
}
Any suggestions about how to accomplish this?
I found a way to solve this, I use subInteractor and there is no need to use #Published or #ObservedObject on the SubInteractor. I pass the SubInteractor down to the view that needs it instead of passing a published variable owned by SubInteractor.
import Foundation
import Combine
import SwiftUI
class SubInteractor: ObservableObject {
#Published var count = 1
init() {
}
}
protocol MainInteractorProtocol: ObservableObject {
var subInteractor: SubInteractor { get set }
}
class MainInteractor: ObservableObject & MainInteractorProtocol {
var subInteractor: SubInteractor
init(subInteractor: SubInteractor) {
self.subInteractor = subInteractor
}
}
struct NameView: View {
#ObservedObject var s: SubInteractor
var body: some View {
Text("\(s.count)")
}
}
And to call it:
struct MainView<Interactor: ObservableObject & MainInteractorProtocol>: View {
#EnvironmentObject var c: Interactor
var body: some View {
VStack {
Button(action: {
c.subInteractor.count += 1
}, label: {
Text("increment")
})
NameView(s: c.subInteractor)
}
}
}

How to trigger automatic SwiftUI Updates with #ObservedObject using MVVM

I have a question regarding the combination of SwiftUI and MVVM.
Before we start, I have read some posts discussing whether the combination of SwiftUI and MVVM is necessary. But I don't want to discuss this here, as it has been covered elsewhere. I just want to know if it is possible and, if yes, how. :)
So here comes the code. I tried to add the ViewModel Layer in between the updated Object class that contains a number that should be updated when a button is pressed. The problem is that as soon as I put the ViewModel Layer in between, the UI does not automatically update when the button is pressed.
View:
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
#ObservedObject var numberStorage = NumberStorage()
var body: some View {
VStack {
// Text("\(viewModel.getNumberObject().number)")
// .padding()
// Button("IncreaseNumber") {
// viewModel.increaseNumber()
// }
Text("\(numberStorage.getNumberObject().number)")
.padding()
Button("IncreaseNumber") {
numberStorage.increaseNumber()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
ViewModel:
class ViewModel: ObservableObject {
#Published var number: NumberStorage
init() {
self.number = NumberStorage()
}
func increaseNumber() {
self.number.increaseNumber()
}
func getNumberObject() -> NumberObject {
self.number.getNumberObject()
}
}
Model:
class NumberStorage:ObservableObject {
#Published var numberObject: NumberObject
init() {
numberObject = NumberObject()
}
public func getNumberObject() -> NumberObject {
return self.numberObject
}
public func increaseNumber() {
self.numberObject.number+=1
}
}
struct NumberObject: Identifiable {
let id = UUID()
var number = 0
} ```
Looking forward to your feedback!
I think your code is breaking MVVM, as you're exposing to the view a storage model. In MVVM, your ViewModel should hold only two things:
Values that your view should display. These values should be automatically updated using a binding system (in your case, Combine)
Events that the view may produce (in your case, a button tap)
Having that in mind, your ViewModel should wrap, adapt and encapsulate your model. We don't want model changes to affect the view. This is a clean approach that does that:
View:
struct ContentView: View {
#StateObject // When the view creates the object, it must be a state object, or else it'll be recreated every time the view is recreated
private var viewModel = ViewModel()
var body: some View {
VStack {
Text("\(viewModel.currentNumber)") // We don't want to use functions here, as that will create a new object , as SwiftUI needs the same reference in order to keep track of changes
.padding()
Button("IncreaseNumber") {
viewModel.increaseNumber()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
ViewModel:
class ViewModel: ObservableObject {
#Published
private(set) var currentNumber: Int = 0 // Private set indicates this should only be mutated by the viewmodel
private let numberStorage = NumberStorage()
init() {
numberStorage.currentNumber
.map { $0.number }
.assign(to: &$currentNumber) // Here we're binding the current number on the storage to the published var that the view is listening to.`&$` basically assigns it to the publishers address
}
func increaseNumber() {
self.numberStorage.increaseNumber()
}
}
Model:
class NumberStorage {
private let currentNumberSubject = CurrentValueSubject<NumberObject, Never>(NumberObject())
var currentNumber: AnyPublisher<NumberObject, Never> {
currentNumberSubject.eraseToAnyPublisher()
}
func increaseNumber() {
let currentNumber = currentNumberSubject.value.number
currentNumberSubject.send(.init(number: currentNumber + 1))
}
}
struct NumberObject: Identifiable { // I'd not use this, just send and int directly
let id = UUID()
var number = 0
}
It's a known problem. Nested observable objects are not supported yet in SwiftUI. I don't think you need ViewModel+Model here since ViewModel seems to be enough.
To make this work you have to trigger objectWillChange of your viewModel manually when objectWillChange of your model is triggered:
class ViewModel: ObservableObject {
init() {
number.objectWillChange.sink { [weak self] (_) in
self?.objectWillChange.send()
}.store(in: &cancellables)
}
}
You better listen to only the object you care not the whole observable class if it is not needed.
Plus:
Since instead of injecting, you initialize your viewModel in your view, you better use StateObject instead of ObservedObject. See the reference from Apple docs: Managing model data in your app
One way you could handle this is to observe the publishers in your Storage class and send the objectWillChange publisher when it changes. I have done this in personal projects by adding a class that all my view models inherit from which provides a nice interface and handles the Combine stuff like this:
Parent ViewModel
import Combine
class ViewModel: ObservableObject {
private var cancellables: Set<AnyCancellable> = []
func publish<T>(on publisher: Published<T>.Publisher) {
publisher.sink { [weak self] _ in self?.objectWillChange.send() }
.store(in: &cancellables)
}
}
Specific ViewModel
class ContentViewModel: ViewModel {
private let numberStorage = NumberStorage()
var number: Int { numberStorage.numberObject.number }
override init() {
super.init()
publish(on: numberStorage.$numberObject)
}
func increaseNumber() {
numberStorage.increaseNumber()
}
}
View
struct ContentView: View {
#StateObject var viewModel = ContentViewModel()
var body: some View {
VStack {
Text("\(viewModel.number)")
.padding()
Button("IncreaseNumber") {
viewModel.increaseNumber()
}
}
}
}
Model/Storage
class NumberStorage:ObservableObject {
#Published var numberObject: NumberObject
init() {
numberObject = NumberObject()
}
public func increaseNumber() {
self.numberObject.number += 1
}
}
struct NumberObject: Identifiable {
let id = UUID()
var number = 0
}
This results in the view re-rendering any time Storage.numberObject changes.

SwiftUI Sharing model data between views

I have such a structure, this structure seems wrong to me as the approach I want to ask you. So when I want to use 2 models in 1 view, I have to put it in foreach in one view. This is what I want. Using the data I use in my user's profile on other pages I want. How should I do this? How do you guys do it?
Let me give an example for your better understanding:
I want to show my user's Username data on the Homepage, how should I do this?. In fact, after initializing my model once, I want to use it in other views. What is the right approach.
import SwiftUI
struct ContentView: View {
#StateObject var network = ProfileNetwork()
var body: some View {
TabView{
ProfileView().tabItem { Image(systemName: "house") }
ForEach(self.network.userprofile,id:\.id){a in
ShopView(profile_model: a)
}.tabItem { Image(systemName: "house") }
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class ProfileNetwork : ObservableObject {
#Published var userprofile : [UserPRofile] = [UserPRofile(name: "Test", coin: 1, id: "dsa")]
}
struct ProfileView : View {
#StateObject var network = ProfileNetwork()
var body: some View {
ForEach(self.network.userprofile, id:\.id){ i in
ProfileViewModel(profile_model: i)
}
}
}
struct ProfileViewModel : View {
var profile_model : UserPRofile
var body: some View {
Text(self.profile_model.name)
}
}
struct UserPRofile : Decodable{
var name : String
var coin : Int
var id : String
}
class ShopeNetwork : ObservableObject {
#Published var shop : [ShopStore] = [ShopStore(id: "sda", image: "dasd", price: 100, name: "sda")]
}
struct ShopView : View {
#StateObject var network = ShopeNetwork()
var profile_model : UserPRofile
var body: some View {
ForEach(self.network.shop, id:\.id){ c in
ShopViewModel(shop_model: c, profile_model: profile_model)
}
}
}
struct ShopViewModel : View {
var shop_model : ShopStore
var profile_model : UserPRofile
var body: some View {
Text(profile_model.name)
Text(self.shop_model.name)
}
}
struct ShopStore : Decodable {
var id : String
var image : String
var price : Int
var name : String
}
A possible solution is to create an #EnvironmentObject and inject it at the root level:
class AppState: ObservableObject {
#Published var userProfile: UserPRofile?
}
#main
struct TestApp: App {
#StateObject private var appState = AppState()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(appState)
}
}
}
struct ProfileView: View {
#EnvironmentObject private var appState: AppState // access as an `#EnvironmentObject`
#StateObject var network = ProfileNetwork()
var body: some View {
VStack {
ForEach(self.network.userprofile, id: \.id) { i in
ProfileViewModel(profile_model: i)
}
}
.onAppear {
appState.userProfile = network.userprofile.first // set `userProfile` globally
}
}
}
struct ShopView: View {
#EnvironmentObject private var appState: AppState // use it in any other view
...
Swift 5, iOS 14
Make the class a singleton so it becomes shareable easily.
class ProfileNetwork : ObservableObject {
#Published var userprofile : [UserPRofile] = [UserPRofile(name: "Test", coin: 1, id: "dsa")]
static var shared = ProfileNetwork()
}
And then refer to it with the shared handle.
struct ContentView: View {
#StateObject var network = ProfileNetwork.shared
var body: some View {
....
}
struct ProfileView : View {
#StateObject var network = ProfileNetwork.shared
var body: some View {
....
}

Change on ObservableObject in #Published array does not update the view

I've been struggling for hours on an issue with SwiftUI.
Here is a simplified example of my issue :
class Parent: ObservableObject {
#Published var children = [Child()]
}
class Child: ObservableObject {
#Published var name: String?
func loadName() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
// Async task here...
self.objectWillChange.send()
self.name = "Loaded name"
}
}
}
struct ContentView: View {
#ObservedObject var parent = Parent()
var body: some View {
Text(parent.children.first?.name ?? "null")
.onTapGesture {
self.parent.objectWillChange.send()
self.parent.children.first?.loadName() // does not update
}
}
}
I have an ObservableObject (Parent) storing a #Published array of ObservableObjects (Child).
The issue is that when the name property is changed via an async task on one object in the array, the view is not updated.
Do you have any idea ?
Many thanks
Nicolas
I would say it is design issue. Please find below preferable approach that uses just pure SwiftUI feature and does not require any workarounds. The key idea is decomposition and explicit dependency injection for "view-view model".
Tested with Xcode 11.4 / iOS 13.4
class Parent: ObservableObject {
#Published var children = [Child()]
}
class Child: ObservableObject {
#Published var name: String?
func loadName() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
// Async task here...
self.name = "Loaded name"
}
}
}
struct FirstChildView: View {
#ObservedObject var child: Child
var body: some View {
Text(child.name ?? "null")
.onTapGesture {
self.child.loadName()
}
}
}
struct ParentContentView: View {
#ObservedObject var parent = Parent()
var body: some View {
// just for demo, in real might be conditional or other UI design
// when no child is yet available
FirstChildView(child: parent.children.first ?? Child())
}
}
Make sure your Child model is a struct! Classes doesn't update the UI properly.
this alternative approach works for me:
class Parent: ObservableObject {
#Published var children = [Child()]
}
class Child: ObservableObject {
#Published var name: String?
func loadName(handler: #escaping () -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
// Async task here...
self.name = UUID().uuidString // just for testing
handler()
}
}
}
struct ContentView8: View {
#ObservedObject var parent = Parent()
var body: some View {
Text(parent.children.first?.name ?? "null").padding(10).border(Color.black)
.onTapGesture {
self.parent.children.first?.loadName(){
self.parent.objectWillChange.send()
}
}
}
}

How to use Bind an Associative Swift enum?

I have a GroupView that accepts a binding as a parameter because I want the GroupView to modify the data in the enum.
Can some help me on how to accomplish this?
import SwiftUI
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
VStack {
GroupView(group: /* What do i put here? */) // <----------------
}
}
}
struct GroupView: View {
#Binding var group: Group
var body: some View {
Text("Hello World")
}
}
class ViewModel : ObservableObject {
#Published var instruction: Instruction!
init() {
instruction = .group(Group(groupTitle: "A Group struct"))
}
}
enum Instruction {
case group(Group)
}
struct Group { var groupTitle: String }
Well, this certainly will work... but probably there's a better approach to your problem. But no one is in a better position than you, to determine that. So I'll just answer your question about how to pass a binding.
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
VStack {
GroupView(group: viewModel.groupBinding)
}
}
}
class ViewModel : ObservableObject {
#Published var instruction: Instruction!
init() {
instruction = .group(Group(groupTitle: "A Group struct"))
}
var groupBinding: Binding<Group> {
return Binding<Group>(get: {
if case .group(let g) = self.instruction {
return g
} else {
return Group(groupTitle: "")
}
}, set: {
self.instruction = .group($0)
})
}
}