I have such ObservableObject that I am injecting into views hierarchy by using environmentObject().
class MenuModel: ObservableObject {
#Published var selection: Int = 0
#Published var isMenuOpen: Bool = false
#Published var tabItems : [TabItem] = [TabItem]()
// {
// didSet {
// objectWillChange.send()
// }
// }
#Published var menuItems : [MenuItem] = [MenuItem]()
// {
// didSet {
// objectWillChange.send()
// }
// }
//var objectWillChange = PassthroughSubject<Void, Never>()
}
And here are issues I do not really understand well:
1. Above code with works correctly, as all properties are #Published.
2. But If I change it to something like this
class Click5MenuModel: ObservableObject {
#Published var selection: Int = 0
#Published var isMenuOpen: Bool = false
var tabItems : [TabItem] = [TabItem]()
{
didSet {
objectWillChange.send()
}
}
var menuItems : [MenuItem] = [MenuItem]()
{
didSet {
objectWillChange.send()
}
}
var objectWillChange = PassthroughSubject<Void, Never>()
}
Then #Published properties stop refreshing Views that depends on this ObservableObject!
Why is that. I also tried to add didSet with objectWillChange.send() but this also causes some odd behaviour and code is a little bit awkward.
Does this mean that I can only use ONLY #Published or ONLY objectWillChange approach?
Default implementation just works (whenever you have #Published properties). Here is from API declaration:
/// By default an `ObservableObject` will synthesize an `objectWillChange`
/// publisher that emits before any of its `#Published` properties changes:
...
#available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
extension ObservableObject where Self.ObjectWillChangePublisher == ObservableObjectPublisher {
/// A publisher that emits before the object has changed.
public var objectWillChange: ObservableObjectPublisher { get }
}
so remove the following:
var objectWillChange = PassthroughSubject<Void, Never>()
and use
didSet {
self.objectWillChange.send()
}
Related
The desired action in the following alert (in a SwiftUI View) only runs after the primaryButton ("Yes") is tapped a second time (on the second appearance of the alert):
.alert(isPresented: $viewModel.showingAlert) {
Alert(
title: Text("Confirm your Selection"),
message: Text("Are you sure?"),
primaryButton: .default (Text("Yes")) {
handleGameOver()
},
secondaryButton: .destructive (
Text("No (try again)"))
)
}
As you can see below, handleGameOver() updates two bools in viewModel, which is "observed" by an SKScene where "showingSolution == true" adds a childNode to the scene.
func handleGameOver() {
viewModel.showingSolution = true
viewModel.respondToTap = false
gameOver = true
}
For Further Reference...
Here is how I have things set up:
The GameViewModel:
final class GameViewModel: ObservableObject {
#Published var showingAlert = false
#Published var tapOnTarget = false
#Published var respondToTap = true
#Published var showingSolution = false
}
In the SwiftUI View:
struct GameView: View {
#ObservedObject var viewModel: GameViewModel
#Binding var showingGameScene : Bool
#Binding var gameOver: Bool
var scene: SKScene {
let scene = GameScene()
scene.size = CGSize(width: 400, height: 300)
scene.scaleMode = .aspectFit
scene.backgroundColor = UIColor(.clear)
scene.viewModel = viewModel
return scene
}
var body: some View { ...
SpriteView(scene: scene)
...
Finally, in the SKScene:
class GameScene: SKScene {
var viewModel: GameViewModel?
...
"showingAlert" is set to true with "viewModel?.showingAlert = true" in "touchesBegan."
I can't be way off, since things work on the second attempt. But clearly that's not good enough.
What am I doing wrong?🤔
Chastened by a comment from Cuneyt, I revisited my problematic post and was able to spot my error in the process:
In the GameView, I was using
#ObservedObject var viewModel: GameViewModel
The object is created in the GameView, so I needed to use:
#StateObject var viewModel: GameViewModel
The discussion at
What is the difference between ObservedObject and StateObject in SwiftUI
was helpful.
This is what I am trying to achieve:
class MyVC: UIViewController {
#State var myBoolState: Bool = false
private var subscribers = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
myBoolState.sink { value in .... }.store(in:&subscribers)
}
func createTheView() {
let vc = UIHostingController(rootView: MySwiftUIView(myBoolState: $myBoolState))
self.navigationController!.pushViewController(vc, animated: true)
}
}
struct MySwiftUIView: View {
#Binding var myBoolState: Bool
var body: some View {
Button(action: {
myBoolState = true
}) {
Text("Push Me")
}
}
}
But the above of course does not compile.
So the question is: can I somehow declare a published property inside a view controller, pass it to a SwiftUI View and get notified when the SwiftUI view changes its value?
The #State wrapper works (by design) only inside SwiftUI view, so you cannot use it in view controller. Instead there is ObsevableObject/ObservedObject pattern for such purpose because it is based on reference types.
Here is a demo of possible solution for your scenario:
import Combine
class ViewModel: ObservableObject {
#Published var myBoolState: Bool = false
}
class MyVC: UIViewController {
let vm = ViewModel()
private var subscribers = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
vm.$myBoolState.sink { value in
print(">> here it goes")
}.store(in:&subscribers)
}
func createTheView() {
let vc = UIHostingController(rootView: MySwiftUIView(vm: self.vm))
self.navigationController!.pushViewController(vc, animated: true)
}
}
struct MySwiftUIView: View {
#ObservedObject var vm: ViewModel
var body: some View {
Button(action: {
vm.myBoolState = true
}) {
Text("Push Me")
}
}
}
Given the following...
import SwiftUI
class ViewModel: ObservableObject {
var value: Bool
init(value: Bool) {
self.value = value
}
func update() {
value = !value
}
}
struct A: View {
#ObservedObject let viewModel: ViewModel
init(value: Bool) {
viewModel = ViewModel(value: value)
}
var body: some View {
Text("\(String(viewModel.value))")
.onTapGesture {
viewModel.update()
}
}
}
struct B: View {
#State var val = [true, false, true]
var body: some View {
A(value: val[0])
}
}
How do I get viewModel to update B's val? It looks like I should be able to use #Binding inside of A but I can't use #Binding inside ViewModel, which is where I want the modification code to run. Then, I don't think I'd need #ObservedObject because the renders would flow from B.
You either need Binding, or an equivalent that does the same thing, in ViewModel. Why do you say you can't use it?
struct A: View {
#ObservedObject var model: Model
init(value: Binding<Bool>) {
model = .init(value: value)
}
var body: some View {
Text(String(model.value))
.onTapGesture(perform: model.update)
}
}
extension A {
final class Model: ObservableObject {
#Binding private(set) var value: Bool
init(value: Binding<Bool>) {
_value = value
}
func update() {
value.toggle()
}
}
}
struct B: View {
#State var val = [true, false, true]
var body: some View {
A(value: $val[0])
}
}
If you want to update the value owned by a parent, you need to pass a Binding from the parent to the child. The child changes the Binding, which updates the value for the parent.
Then you'd need to update that Binding when the child's own view model updates. You can do this by subscribing to a #Published property:
struct A: View {
#ObservedObject var viewModel: ViewModel
#Binding var value: Bool // add a binding
init(value: Binding<Bool>) {
_value = value
viewModel = ViewModel(value: _value.wrappedValue)
}
var body: some View {
Button("\(String(viewModel.value))") {
viewModel.update()
}
// subscribe to changes in view model
.onReceive(viewModel.$value, perform: {
value = $0 // update the binding
})
}
}
Also, don't forget to actually make the view model's property #Published:
class ViewModel: ObservableObject {
#Published var value: Bool
// ...
}
I'm trying to fill up a Picker with data fetched asynchronously from external API.
This is my model:
struct AppModel: Identifiable {
var id = UUID()
var appId: String
var appBundleId : String
var appName: String
var appSKU: String
}
The class that fetches data and publish is:
class AppViewModel: ObservableObject {
private var appStoreProvider: AppProvider? = AppProvider()
#Published private(set) var listOfApps: [AppModel] = []
#Published private(set) var loading = false
fileprivate func fetchAppList() {
self.loading = true
appStoreProvider?.dataProviderAppList { [weak self] (appList: [AppModel]) in
guard let self = self else {return}
DispatchQueue.main.async() {
self.listOfApps = appList
self.loading = false
}
}
}
init() {
fetchAppList()
}
}
The View is:
struct AppView: View {
#ObservedObject var appViewModel: AppViewModel = AppViewModel()
#State private var selectedApp = 0
var body: some View {
ActivityIndicatorView(isShowing: self.appViewModel.loading) {
VStack{
// The Picker doesn't bind with appViewModel
Picker(selection: self.$selectedApp, label: Text("")) {
ForEach(self.appViewModel.listOfApps){ app in
Text(app.appName).tag(app.appName)
}
}
// The List correctly binds with appViewModel
List {
ForEach(self.appViewModel.listOfApps){ app in
Text(app.appName.capitalized)
}
}
}
}
}
}
While the List view binds with the observed object appViewModel, the Picker doesn't behave in the same way. I can't realize why. Any help ?
I filed bug report, FB7670992. Apple responded yesterday, suggesting that I confirm this behavior in iOS 14, beta 1. It appears to now have been resolved.
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
Picker("", selection: $viewModel.wheelPickerValue) {
ForEach(viewModel.objects) { object in
Text(object.string)
}
}
.pickerStyle(WheelPickerStyle())
.labelsHidden()
}
}
Where
struct Object: Identifiable {
let id = UUID().uuidString
let string: String
}
class ViewModel: ObservableObject {
private var counter = 0
#Published private(set) var objects: [Object] = []
#Published var segmentedPickerValue: String = ""
#Published var wheelPickerValue: String = ""
fileprivate func nextSetOfValues() {
let newCounter = counter + 3
objects = (counter..<newCounter).map { value in Object(string: "\(value)") }
let id = objects.first?.id ?? ""
segmentedPickerValue = id
wheelPickerValue = id
counter = newCounter
}
init() {
let timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { [weak self] timer in
guard let self = self else { timer.invalidate(); return }
self.nextSetOfValues()
}
timer.fire()
}
}
Results in:
I can't put this into your code because it is incomplete but here is a sample.
Pickers aren't meant to be dynamic. They have to be completely reloaded.
class DynamicPickerViewModel: ObservableObject {
#Published private(set) var listOfApps: [YourModel] = []
#Published private(set) var loading = false
fileprivate func fetchAppList() {
loading = true
DispatchQueue.main.async() {
self.listOfApps.append(YourModel.addSample())
self.loading = false
}
}
init() {
fetchAppList()
}
}
struct DynamicPicker: View {
#ObservedObject var vm = DynamicPickerViewModel()
#State private var selectedApp = ""
var body: some View {
VStack{
//Use your loading var to reload the picker when it is done
if !vm.loading{
//Picker is not meant to be dynamic, it needs to be completly reloaded
Picker(selection: self.$selectedApp, label: Text("")) {
ForEach(self.vm.listOfApps){ app in
Text(app.name!).tag(app.name!)
}
}
}//else - needs a view while the list is being loaded/loading = true
List {
ForEach(self.vm.listOfApps){ app in
Text(app.name!.capitalized)
}
}
Button(action: {
self.vm.fetchAppList()
}, label: {Text("fetch")})
}
}
}
struct DynamicPicker_Previews: PreviewProvider {
static var previews: some View {
DynamicPicker()
}
}
I have a custom embedded View with Text and a Slider that binds to an ObservedObject. I can successfully update the binding with changing the slider but the Text does not update. For some reason, I am really struggling with getting the hang of Property Wrappers and hoping to finally get it to click.
I can easily have the text update when I'm binding the slider value to local state, but no luck with the binding.
class MyItem:ObservableObject, Codable, Identifiable {
enum CodingKeys: String, CodingKey {
case calories
}
var didChange = PassthroughSubject<Void,Never>()
var id = UUID()
var calories:Double = 0 { didSet { update() }
func update() {
didChange.send()
}
}
struct ContentView:View {
#ObservedObject var item = MyItem()
var body:some View {
MySlider(value: $item.calories)
}
}
struct MySlider:View {
#Binding var value:Double
var body:some View {
VStack {
Text("\(value) ")
Slider(value: $value, in: 0...2000, step:5)
}
}
}
Everything works fine, but I can't get the text in MySlider to update as I mess with the Slider.
With property wrappers introduced in Swift 5.1, we can user #Published to simplify the state propagation. I refactored yours to user #Published. For the scenario mentioned this should work.
class MyItem: ObservableObject, Codable, Identifiable {
enum CodingKeys: String, CodingKey {
case id
case calories
}
var id = UUID()
#Published var calories: Double = 0
}
struct ContentView:View {
#ObservedObject var item = MyItem()
var body:some View {
MySlider(value: $item.calories)
}
}
struct MySlider:View {
#Binding var value:Double
var body:some View {
VStack {
Text("\(value) ")
Slider(value: $value, in: 0...2000, step:5)
}
}
}
#partha g—I was still getting a message from XCode saying that MyItem didn't conform to Codable. So after some digging, I added a couple inits and encode methods to get everything working the way I was expecting:
class MyItem: ObservableObject, Codable, Identifiable {
[...]
func encode(to encoder:Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(calories, forKey: .calories)
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
calories = try container.decode(Double.self, forKey: .calories)
}
init() { }
}
and voila.
Thanks for your help!