SwiftUI not update view using #Published - swift

I can't update the color of my Text base on the current status of my object.
The text should change color base on the variable status true or false.
I try below to simplify the code of where the data come from.
My contentview:
struct ContentView: View {
#StateObject var gm = GameManager()
#State var openSetting = false
var body: some View {
Button {
openSetting.toggle()
} label: {
Text("Setting")
}
}
}
ContentView has a SettingView where I'm selecting setting and where I want to update my textColor based on the status of object
struct SettingView: View {
#StateObject var gm : GameManager
var body: some View {
ScrollView(.horizontal, showsIndicators: true) {
HStack(spacing: 20) {
ForEach(gm.cockpit.ecamManager.door.doorarray) { doorName in
Button {
gm.close(door: doorName.doorName)
} label: {
Text(doorName.doorName)
// Here where I want to change color
.foregroundColor(doorName.isopen ? .orange : .green)
}
}
}
}
}
}
The data come from GameManager which inside has a variable called cockpit:
class GameManager: NSObject, ObservableObject, ARSessionDelegate, ARSCNViewDelegate {
#Published var cockpit = MakeCockpit() // create the cockpit
// do other stuff
}
MakeCockpit :
class MakeCockpit: SCNNode, ObservableObject {
#Published var ecamManager = ECAMManager()
// do other stuff
ECAMManager:
class ECAMManager: ObservableObject {
#Published var door = ECAMDoor()
#Published var stanby = ECAMsby()
}
And Finally... the Array I want to watch is in ECAMDoor class:
class ECAMDoor: ObservableObject {
#Published var doorarray : [Door] = [] // MODEL
}
Now everything work fine as expected but the #Publish of the door array not update my color in the setting view. I need to close the view and open again to se the color update.
Is someone can tell me where I mistake? I probably missed something .. hope I been clear (to many instance of class inside other class)

Related

SwiftUI How to have class viewable from all views

I am fairly new to SwiftUI and I am trying to build an app where you can favorite items in a list. It works in the ContentView but I would like to have the option to favorite and unfavorite an item in its DetailView.
I know that vm is not in the scope but how do I fix it?
Here is some of the code in the views. The file is long so I am just showing the relevant code
struct ContentView: View {
#StateObject private var vm = ViewModel()
//NavigationView with a List {
//This is the code I call for showing the icon. The index is the item in the list
Image(systemName: vm.contains(index) ? "heart.fill" : "heart")
.onTapGesture{
vm.toggleFav(item: index)
}
}
struct DetailView: View {
Hstack{
Image(systemName: vm.contains(entry) ? "heart.fill" : "heart") //Error is "Cannot find 'vm' in scope"
}
}
Here is the code that that vm is referring to
import Foundation
import SwiftUI
extension ContentView {
final class ViewModel: ObservableObject{
#Published var items = [Biase]()
#Published var showingFavs = false
#Published var savedItems: Set<Int> = [1, 7]
// Filter saved items
var filteredItems: [Biase] {
if showingFavs {
return items.filter { savedItems.contains($0.id) }
}
return items
}
private var BiasStruct: BiasData = BiasData.allBias
private var db = Database()
init() {
self.savedItems = db.load()
self.items = BiasStruct.biases
}
func sortFavs(){
withAnimation() {
showingFavs.toggle()
}
}
func contains(_ item: Biase) -> Bool {
savedItems.contains(item.id)
}
// Toggle saved items
func toggleFav(item: Biase) {
if contains(item) {
savedItems.remove(item.id)
} else {
savedItems.insert(item.id)
}
db.save(items: savedItems)
}
}
}
This is the list view...
enter image description here
Detail view...
enter image description here
I tried adding this code under the List(){} in the ContentView .environmentObject(vm)
And adding this under the DetailView #EnvironmentObject var vm = ViewModel() but it said it couldn't find ViewModel.
To put the view model inside the ContentView struct is wrong. Delete the enclosing extension.
If the view model is supposed to be accessed from everywhere it must be on the top level.
In the #main struct create the instance of the view model and inject it into the environment
#main
struct MyGreatApp: App {
#StateObject var viewModel = ViewModel()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(viewModel)
}
}
}
And in any struct you want to use it add
#EnvironmentObject var vm : ViewModel
without parentheses.

swiftui text view does not update when the property of class changes

A ton of similar questions found, but they all have different conditions - none of them seems to apply to my case.
The Text() view does not update with the property of the class, but the bush button prints the correct value. More info in the snippets.
The View:
import SwiftUI
struct ContentView: View {
#State private var client : Client = Client()
var body: some View {
VStack{
Text(client.message) // This view remains constant
...
Button( action: {
print("\(client.message)") // Prints correct updated value
}){
Text("Mess")
}
}
}
}
The Class:
class Client : ObservableObject{
private var mqtt: CocoaMQTT!
#Published var message : String = "Empty"
...
func get_message(mess : String){
self.message = mess
self.objectWillChange.send()
}
}
Even objectWillChange.send() does not seem to trigger the change in the view. Any ideas?
For Objects use #StateObject property wrapper and for String, Int, etc use #State property wrapper.
#State: We use this property wrapper when we observe a property that is exists inside our ContentView.
#StateObject: We use this property wrapper when we are observing properties that exists outside of our ContentView like in our Object, we can observe that property changes by firstly conforming that class to ObservableObject Protocol and marking that property as #Published inside our Object.
struct ContentView: View {
#StateObject private var client : Client = Client()
var body: some View {
VStack{
Text(client.message)
Button( action: {
print("\(client.message)")
}){
Text("Mess")
}
}
}
}
class Client : ObservableObject{
private var mqtt: CocoaMQTT!
#Published var message : String = "Empty"
func get_message(mess : String){
self.message = mess
}
}

Swift : Why is my View Model with a binding not working with the Toggle in my View?

I have a view with a separate view model for it's logic. In my view model I've created a binding and I want the Toggle in my View to reflect and act on this. But it the moment, the view reads the initial state of my binding, but it does not allow me to update it (I can't move the toggle box). And I have no idea why this is.
This is my View (simplified):
public var viewModel: MyViewModel // passed as a dependency to my view
var body: some View {
Toggle("Test", isOn: viewModel.isOn)
}
This is the code in my ViewModel:
public var isOn: Binding<Bool> = .constant(false) {
didSet {
print("Binding in practice!")
}
}
As I said, the code runs, and the initial value for isOn is respected: if I set it to .constant(true) or .constant(false) the checkbox is in the correct state. But I am not allowed to change the checkbox. Also the print()-statement is never executed. So it seems to me there are 2 problems:
I cannot change the state for isOn
The didSet is not executed
Can anyone point me in the right direction?
Edit:
So, actually some of you pointed my in the direction I've already tried and that did not work. In the above writing I simplified my use case, but it's actually a bit more abstract than that: I've got a protocol for my View Model that I use as a dependency. What I've got working now (with binding and observing) is the following:
ViewModelProtocol + ViewModel:
public protocol ViewModelProtocol: ObservableObject {
var isOn: Binding<Bool> { get }
}
public class ViewModel: ViewModelProtocol {
private var _isOn: Bool = false
public var isOn: Binding<Bool> {
Binding<Bool>(
get: { self._isOn },
set: {
self._isOn = $0
// custom code
}
)
}
// More code that has #Published properties
}
View:
struct MyView<Model>: View where Model: ViewModelProtocol {
#ObservedObject var viewModel: Model
var body: some View {
Toggle("Test", isOn: viewModel.isOn)
// More code that uses #Published properties
}
}
Once again, I simplified the example and stripped out all clutter, but with this setup I am able to do what I want. However, I'm still not sure if this is the correct way to do this.
The implementation of making my view generic is based on https://stackoverflow.com/a/59504489/1471590
If you want to drive your view from the properties of the view model, then make it an ObservableObject
class ViewModel: ObservableObject {
#Published var isOn = false {
didSet {
print("Binding in practice")
}
}
}
And in your view, you can bind the toggle to this value:
struct ContentView: View {
#StateObject private var viewModel = ViewModel()
var body: some View {
Toggle("Test", isOn: $viewModel.isOn)
}
}
After some more tinkering and the response of #RajaKishan I came up with the following solution that works:
private var _isOn: Bool = false
public var isOn: Binding<Bool> {
Binding<Bool>(
get: { self._isOn },
set: {
self._isOn = $0
print("Room for custom logic")
}
)
}

SwiftUI macOS Commands (menu bar) and View

Hi I am starting to learn SwiftUI and macOS development. I am using the SwiftUI life cycle. How do I call a function from the focused window from the menu bar.
Besides Apple documentation, I found this reference and am able to create menu items using Commands but I have no idea how to call a function from my view.
For example:
Suppose this is my App struct:
import SwiftUI
#main
struct ExampleApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}.commands {
CommandMenu("First menu") {
Button("Action!") {
// How do I call the views action function?
}
}
}
}
and this is my View:
struct ContentView: View {
public func action() {
print("It works")
}
var body: some View {
Text("Example")
}
}
I just typed the example code sorry if there are any typos but I hope you can get the idea.
Because Views in SwiftUI are transient, you can't hold a reference to a specific instance of ContentView to call a function on it. What you can do, though, is change part of your state that gets passed down to the content view.
For example:
#main
struct ExampleApp: App {
#StateObject var appState = AppState()
var body: some Scene {
WindowGroup {
ContentView(appState: appState)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}.commands {
CommandMenu("First menu") {
Button("Action!") {
appState.textToDisplay = "\(Date())"
}
}
}
}
}
class AppState : ObservableObject {
#Published var textToDisplay = "(not clicked yet)"
}
struct ContentView: View {
#ObservedObject var appState : AppState
var body: some View {
Text(appState.textToDisplay)
}
}
Note that the .commands modifier goes on WindowGroup { }
In this example, AppState is an ObservableObject that holds some state of the app. It's passed through to ContentView using a parameter. You could also pass it via an Environment Object (https://www.hackingwithswift.com/quick-start/swiftui/how-to-use-environmentobject-to-share-data-between-views)
When the menu item is clicked, it sets textToDisplay which is a #Published property on AppState. ContentView will get updated any time a #Published property of AppState gets updated.
This is the general idea of the pattern you'd use. If you have a use case that isn't covered by this pattern, let me know in the comments.
Updates, based on your comments:
import SwiftUI
import Combine
#main
struct ExampleApp: App {
#StateObject var appState = AppState()
var body: some Scene {
WindowGroup {
ContentView(appState: appState)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}.commands {
CommandMenu("First menu") {
Button("Action!") {
appState.textToDisplay = "\(Date())"
}
Button("Change background color") {
appState.contentBackgroundColor = Color.green
}
Button("Toggle view") {
appState.viewShown.toggle()
}
Button("CustomCopy") {
appState.customCopy.send()
}
}
}
}
}
class AppState : ObservableObject {
#Published var textToDisplay = "(not clicked yet)"
#Published var contentBackgroundColor = Color.clear
#Published var viewShown = true
var customCopy = PassthroughSubject<Void,Never>()
}
class ViewModel : ObservableObject {
#Published var text = "The text I have here"
var cancellable : AnyCancellable?
func connect(withAppState appState: AppState) {
cancellable = appState.customCopy.sink(receiveValue: { _ in
print("Do custom copy based on my state: \(self.text) or call a function")
})
}
}
struct ContentView: View {
#ObservedObject var appState : AppState
#State var text = "The text I have here"
#StateObject private var viewModel = ViewModel()
var body: some View {
VStack {
Text(appState.textToDisplay)
.background(appState.contentBackgroundColor)
if appState.viewShown {
Text("Shown?")
}
}
.onReceive(appState.$textToDisplay) { (newText) in
print("Got new text: \(newText)")
}
.onAppear {
viewModel.connect(withAppState: appState)
}
}
}
In my updates, you can see that I've addressed the question of the background color, showing hiding a view, and even getting a notification (via onReceive) when one of the #Published properties changes.
You can also see how I use a custom publisher (customCopy) to pass along an action to ContentView's ViewModel

Binding value from an ObservableObject

Aim:
I have a model which is an ObservableObject. It has a Bool property, I would like to use this Bool property to initialise a #Binding variable.
Questions:
How to convert an #ObservableObject to a #Binding ?
Is creating a #State the only way to initialise a #Binding ?
Note:
I do understand I can make use of #ObservedObject / #EnvironmentObject, and I see it's usefulness, but I am not sure a simple button needs to have access to the entire model.
Or is my understanding incorrect ?
Code:
import SwiftUI
import Combine
import SwiftUI
import PlaygroundSupport
class Car : ObservableObject {
#Published var isReadyForSale = true
}
struct SaleButton : View {
#Binding var isOn : Bool
var body: some View {
Button(action: {
self.isOn.toggle()
}) {
Text(isOn ? "On" : "Off")
}
}
}
let car = Car()
//How to convert an ObservableObject to a Binding
//Is creating an ObservedObject or EnvironmentObject the only way to handle a Observable Object ?
let button = SaleButton(isOn: car.isReadyForSale) //Throws a compilation error and rightly so, but how to pass it as a Binding variable ?
PlaygroundPage.current.setLiveView(button)
Binding variables can be created in the following ways:
#State variable's projected value provides a Binding<Value>
#ObservedObject variable's projected value provides a wrapper from which you can get the Binding<Subject> for all of it's properties
Point 2 applies to #EnvironmentObject as well.
You can create a Binding variable by passing closures for getter and setter as shown below:
let button = SaleButton(isOn: .init(get: { car.isReadyForSale },
set: { car.isReadyForSale = $0} ))
Note:
As #nayem has pointed out you need #State / #ObservedObject / #EnvironmentObject / #StateObject (added in SwiftUI 2.0) in the view for SwiftUI to detect changes automatically.
Projected values can be accessed conveniently by using $ prefix.
You have several options to observe the ObservableObject. If you want to be in sync with the state of the object, it's inevitable to observe the state of the stateful object. From the options, the most commons are:
#State
#ObservedObject
#EnvironmentObject
It is upto you, which one suits your use case.
No. But you need to have an object which can be observed of any change made to that object in any point in time.
In reality, you will have something like this:
class Car: ObservableObject {
#Published var isReadyForSale = true
}
struct ContentView: View {
// It's upto you whether you want to have other type
// such as #State or #ObservedObject
#EnvironmentObject var car: Car
var body: some View {
SaleButton(isOn: $car.isReadyForSale)
}
}
struct SaleButton: View {
#Binding var isOn: Bool
var body: some View {
Button(action: {
self.isOn.toggle()
}) {
Text(isOn ? "Off" : "On")
}
}
}
If you are ready for the #EnvironmentObject you will initialize your view with:
let contentView = ContentView().environmentObject(Car())
struct ContentView: View {
#EnvironmentObject var car: Car
var body: some View {
SaleButton(isOn: self.$car.isReadyForSale)
}
}
class Car: ObservableObject {
#Published var isReadyForSale = true
}
struct SaleButton: View {
#Binding var isOn: Bool
var body: some View {
Button(action: {
self.isOn.toggle()
}) {
Text(isOn ? "On" : "Off")
}
}
}
Ensure you have the following in your SceneDelegate:
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
.environmentObject(Car())
In my case i used .constant(viewModel) to pass viewModel to ListView #Binding var viewModel
Example
struct CoursesView: View {
#StateObject var viewModel = CoursesViewModel()
var body: some View {
ZStack {
ListView(viewModel: .constant(viewModel))
ProgressView().opacity(viewModel.isShowing)
}
}
}
struct ListView: View {
#Binding var viewModel: CoursesViewModel
var body: some View {
List {
ForEach(viewModel.courses, id: \.id) { course in
Text(couse.imageUrl)
}
}
}
}