Can you use a Publisher directly as an #ObjectBinding property in SwiftUI? - swift

In SwiftUI, can you use an instance of a Publisher directly as an #ObjectBinding property or do you have to wrap it in a class that implements BindableObject?
let subject = PassthroughSubject<Void, Never>()
let view = ContentView(data:subject)
struct ContentView : View {
#ObjectBinding var data:AnyPublisher<Void, Never>
}
// When I want to refresh the view, I can just call:
subject.send(())
This doesn't compile for me and just hangs Xcode 11 Beta 2. But should you even be allowed to do this?

In your View body use .onReceive passing in the publisher like the example below, taken from Data Flow Through SwiftUI - WWDC 2019 # 21:23. Inside the closure you update an #State var, which in turn is referenced somewhere else in the body which causes body to be called when it is changed.

You can implement a BindableObject wich takes a publisher as initializer parameter.
And extend Publisher with a convenience function to create this BindableObject.
class BindableObjectPublisher<PublisherType: Publisher>: BindableObject where PublisherType.Failure == Never {
typealias Data = PublisherType.Output
var didChange: PublisherType
var data: Data?
init(didChange: PublisherType) {
self.didChange = didChange
_ = didChange.sink { (value) in
self.data = value
}
}
}
extension Publisher where Failure == Never {
func bindableObject() -> BindableObjectPublisher<Self> {
return BindableObjectPublisher(didChange: self)
}
}
struct ContentView : View {
#ObjectBinding var binding = Publishers.Just("test").bindableObject()
var body: some View {
Text(binding.data ?? "Empty")
}
}

Related

SwiftUI - changes in nested View Model classes not detected using onChange method

I have a nested View Model class WatchDayProgramViewModel as an ObservableObject. Within WatchDayProgramViewModel, there is a WorkoutModel that is a child class. I want to detect any updates in the currentHeartRate to trigger data transfer to iPhone.
Hence, I tried from ContentView using WatchDayProgramViewModel as an EnvironmentObject and detecting changes in WorkoutModel via onChange() method. But it seems that SwiftUI views does not detect any property changes in WorkoutModel.
I understand that this issue could be due to ObservableObject not detecting changes in child/nested level of classes, and SO answer (SwiftUI change on multilevel children Published object change) suggests using struct instead of class. But changing WorkoutModel to struct result in various #Published properties and functions to show error.
Is there any possible way to detect changes in child View Model from the ContentView itself?
ContentView
struct ContentView: View {
#State var selectedTab = 0
#StateObject var watchDayProgramVM = WatchDayProgramViewModel()
var body: some View {
NavigationView {
TabView(selection: $selectedTab) {
WatchControlView().id(0)
NowPlayingView().id(1)
}
.environmentObject(watchDayProgramVM)
.onChange(of: self.watchDayProgramVM.workoutModel.currentHeartRate) { newValue in
print("WatchConnectivity heart rate from contentView \(newValue)")
}
}
}
WatchDayProgramViewModel
class WatchDayProgramViewModel: ObservableObject {
#Published var workoutModel = WorkoutModel()
init() {
}
}
WorkoutModel
import Foundation
import HealthKit
class WorkoutModel: NSObject, ObservableObject {
let healthStore = HKHealthStore()
var session: HKWorkoutSession?
var builder: HKLiveWorkoutBuilder?
#Published var currentHeartRate: Double = 0
#Published var workout: HKWorkout?
//Other functions to start/run workout hidden
func updateForStatistics(_ statistics: HKStatistics?) {
guard let statistics = statistics else {
return
}
DispatchQueue.main.async {
switch statistics.quantityType {
case HKQuantityType.quantityType(forIdentifier: .heartRate):
let heartRateUnit = HKUnit.count().unitDivided(by: HKUnit.minute())
self.currentHeartRate = statistics.mostRecentQuantity()?.doubleValue(for: heartRateUnit) ?? 0
default:
return
}
}//end of dispatchqueue
}// end of function
}
extension WorkoutModel: HKLiveWorkoutBuilderDelegate {
func workoutBuilder(_ workoutBuilder: HKLiveWorkoutBuilder, didCollectDataOf collectedTypes: Set<HKSampleType>) {
for type in collectedTypes {
guard let quantityType = type as? HKQuantityType else {
return
}
let statistics = workoutBuilder.statistics(for: quantityType)
updateForStatistics(statistics)
}
}
}
Try to change
#StateObject var watchDayProgramVM = WatchDayProgramViewModel()
with
#ObservedObject var watchDayProgramVM = WatchDayProgramViewModel()
Figure it out. Just had to create another AnyCancellable variable to call objectWillChange publisher.
WatchDayProgramViewModel
class WatchDayProgramViewModel: ObservableObject {
#Published var workoutModel = WorkoutModel()
var cancellable: AnyCancellable?
init() {
cancellable = workoutModel.objectWillChange
.sink { _ in
self.objectWillChange.send()
}
}
}
While I have provided my answer, that worksaround with viewmodels, I would love to see/get advice on other alternatives.

run when a view redraws in SwiftUI

I have a view in SwiftUI, and I would like it to both redraw and to run a closure whenever a variable in my model changes. I am using this closure to update a state var I am storing in the view, which should be the previous value of the variable in my model before it changes
The following code simulates my situation:
let viewModel = ViewModel()
struct someView: View {
#observedObject var viewModel: ViewModel = viewModel
#State var previousSomeValue: CGFloat = 0
var body: some View {
Text("\(viewModel.model.someValue)")
}
}
class ViewModel: ObservableObject {
#Published var model = Model()
}
struct model {
var someValue: CGFloat = 0
}
With this setup, if someValue ever changes, someView redraws, however, I am unable to fire a closure.
//Solutions I have tried:
The main one was to attach onChangeOf(_ (T)->Void) to my view. With .onChangeOf( viewModel.model.someValue ) { _ in //do something } I was able to fire a closure whenever it changed however, by the time it ran, viewModel.model.someValue had already updated to the newValue, and I wasnt able to capture the old one. I read in the documentation that this is by design and that you must capture the thing you want to store the old value of, but I (to my knowledge) am only able to capture self, viewModel, but not viewModel.model.someValue.
.onChangeOf( viewModel.model.someValue ) { [self] newValue in //do something } //works but doesnt capture the var
.onChangeOf( viewModel.model.someValue ) { [viewModel] newValue in //do something } //works but doesnt capture the var
.onChangeOf( viewModel.model.someValue ) { [viewModel.model.someValue] newValue in //do something } //does not compile ( Expected 'weak', 'unowned', or no specifier in capture list )
I have also tried creating a binding in the view such as Binding { gameView.model.someValue } set: { _ in } and having the onChange observer this instead, but even when I capture self, when the closure is called, the old and new values are identicial.
This seems like a common thing to do (detecting external changes and firing a closure), how should I go about it?
If I correctly understood your needs then you should do this not in view but in view model, like
class ViewModel: ObservableObject {
var onModelChanged: (_ old: Model, _ new: Model) -> Void
#Published var model = Model() {
didSet {
onModelChanged(oldValue, model)
}
}
init(onModelChanged: #escaping (_ old: Model, _ new: Model) -> Void = {_, _ in}) {
self.onModelChanged = onModelChanged
}
}
so instantiating ViewModel you can provide a callback to observe values changed in model and have old and new values, like
#StateObject var viewModel = ViewModel() {
print("Old value: \($0.someValue)")
print("New value: \($1.someValue)")
}

Modify #Published variable from another class that is not declared in | SwiftUI

I want to put the logic of all my #Published in a model class, however when I try to separate it, it doesn't update. I recreated a little example:
The code below works, it increases every time the button is clicked:
struct ContentView: View {
#StateObject var myClass = MyClass()
var body: some View {
Button(action: {
myClass.doStuff(numb: 1)
}) {
Text("People: \(myClass.people)")
}
}
}
class MyClass: ObservableObject {
#Published var people: Int = 0
func doStuff(numb: Int) {
people += numb
}
}
However, once I split the logic and try to have my #Published in a separate class to have it more clean, it doesn't update, see below:
struct ContentView: View {
#StateObject var myClass = MyClass()
let modify = Modify()
var body: some View {
Button(action: {
modify.doStuff(numb: 1)
}) {
Text("People: \(myClass.people)")
}
}
}
class Modify {
var myClass = MyClass()
func doStuff(numb: Int) {
myClass.people += numb
}
}
class MyClass: ObservableObject {
#Published var people: Int = 0
}
I think it's because there are two different instances in the view right? Anyway, how can I separate the #Publish correctly have it updated?
Thanks
Your first form is absolutely fine! You may, though, consider your ContentView using a #ObservedObject instead a #StateObject.
Your second form is flawed, for several reasons:
don't move logic into a view
don't use class variables to keep "state".
The first statement is due to a sane design that keeps your models and views nicely separated.
The second statement is due to how SwiftUI works. If you need to have some "state" in your views, use #State where the value is a struct.
Using #State ensures, that it's value is bound to the actual life time of the "conceptual view", i.e. the thing we human perceive as the view. And this "conceptual view" (managed as some data object by SwiftUI) is distinct from the struct View, which is merely a means to describe how to create and modify this conceptual view - that is, struct view is rather a function that will be used to initially create the "conceptual view" and modify it. Once this is done, it gets destroyed, and gets recreated when the conceptual view needs to be modified. That also means, the life time of this struct is not bound to the life time of its "state". The state's life time is bound to the conceptual view, and thus has usually longer life time than the struct view, where the struct view can be created and destroyed several times.
Now, imagine what happens when you always execute let modify = Modify() whenever the (conceptual) view or its content view is modified and needs to be recalculated and rendered by creating a struct view and then - after it has been rendered - destroying it again.
Also, this "state" is considered private for the view, means it is considered an implementation detail for the view. If you want to exchange data from "outside" and "inside" use a #ObservedObject or a Binding.
The problem is that you have 2 separate instances of MyClass:
#StateObject var myClass = MyClass()
var myClass = MyClass()
You are updating the myClass in Modify, which you aren't receiving updates from. A way to fix this is by having one instance of MyClass, passed into Modify during initialization:
struct ContentView: View {
#StateObject var myClass: MyClass
let modify: Modify
init() {
let temp = MyClass()
_myClass = StateObject(wrappedValue: temp)
modify = Modify(myClass: temp)
}
var body: some View {
Button(action: {
modify.doStuff(numb: 1)
}) {
Text("People: \(myClass.people)")
}
}
}
class Modify {
let myClass: MyClass
init(myClass: MyClass) {
self.myClass = myClass
}
func doStuff(numb: Int) {
myClass.people += numb
}
}
Another method is to have a #Published property in Modify to observe the changes of MyClass:
struct ContentView: View {
#StateObject var modify = Modify()
var body: some View {
Button(action: {
modify.doStuff(numb: 1)
}) {
Text("People: \(modify.myClass.people)")
}
}
}
class Modify: ObservableObject {
#Published var myClass = MyClass()
private var anyCancellable: AnyCancellable?
init() {
anyCancellable = myClass.objectWillChange.sink { [weak self] _ in
self?.objectWillChange.send()
}
}
func doStuff(numb: Int) {
myClass.people += numb
}
}
you could try this approach using a singleton. Works well for me:
struct ContentView: View {
#StateObject var myClass = MyClass.shared // <--- here
let modify = Modify()
var body: some View {
Button(action: {
modify.doStuff(numb: 1)
}) {
Text("People: \(myClass.people)")
}
}
}
class Modify {
var myClass = MyClass.shared // <--- here
func doStuff(numb: Int) {
myClass.people += numb
}
}
class MyClass: ObservableObject {
#Published var people: Int = 0
static let shared = MyClass() // <--- here
}

Binding to a read-only property in SwiftUI

I have a model type which looks like this:
enum State {
case loading
case loaded([String])
case failed(Error)
var strings: [String]? {
switch self {
case .loaded(let strings): return strings
default: return nil
}
}
}
class MyApi: ObservableObject {
private(set) var state: State = .loading
func fetch() {
... some time later ...
self.state = .loaded(["Hello", "World"])
}
}
and I'm trying to use this to drive a SwiftUI View.
struct WordListView: View {
#EnvironmentObject var api: MyApi
var body: some View {
ZStack {
List($api.state.strings) {
Text($0)
}
}
}
}
It's about here that my assumptions fail. I'm trying to get a list of the strings to render in my List when they are loaded, but it won't compile.
The compiler error is Generic parameter 'Subject' could not be inferred, which after a bit of googling tells me that bindings are two-way, so won't work with both my private(set) and the var on the State enum being read-only.
This doesn't seem to make any sense - there is no way that the view should be able to tell the api whether or not it's loading, that definitely should be a one-way data flow!
I guess my question is either
Is there a way to get a one-way binding in SwiftUI - i.e. some of the UI will update based on a value it cannot change.
or
How should I have architected this code! It's very likely that I'm writing code in a style which doesn't work with SwiftUI, but all the tutorials I can see online neatly ignore things like loading / error states.
You don't actually need a binding for this.
An intuitive way to decide if you need a binding or not is to ask:
Does this view need to modify the passed value ?
In your case the answer is no. The List doesn't need to modify api.state (as opposed to a textfield or a slider for example), it just needs the current value of it at any given moment. That is what #State is for but since the state is not something that belongs to the view (remember, Apple says that each state must be private to the view) you're correctly using some form of an ObservableObject (through Environment).
The final missing piece is to mark any of your properties that should trigger an update with #Published, which is a convenience to fire objectWillChange signals and instruct any observing view to recalculate its body.
So, something like this will get things done:
class MyApi: ObservableObject {
#Published private(set) var state: State = .loading
func fetch() {
self.state = .loaded(["Hello", "World"])
}
}
struct WordListView: View {
#EnvironmentObject var api: MyApi
var body: some View {
ZStack {
List(api.state.strings ?? [], id: \.self) {
Text($0)
}
}
}
}
Not exactly the same problem as I had, but the following direction can help you possibly find a good result when bindings are done with only reads.
You can create a custom binding using a computed property.
I needed to do exactly this in order to show an alert only when one was passed into an overlay.
Code looks something along these lines :
struct AlertState {
var title: String
}
class AlertModel: ObservableObject {
// Pass a binding to an alert state that can be changed at
// any time.
#Published var alertState: AlertState? = nil
#Published var showAlert: Bool = false
init(alertState: AnyPublisher<AlertState?, Never>) {
alertState
.assign(to: &$alertState)
alertState
.map { $0 != nil }
.assign(to: &$showAlert)
}
}
struct AlertOverlay<Content: View>: View {
var content: Content
#ObservedObject var alertModel: AlertModel
init(
alertModel: AlertModel,
#ViewBuilder content: #escaping () -> Content
) {
self.alertModel = alertModel
self.content = content()
}
var body: some View {
ZStack {
content
.blur(radius: alertModel.showAlert
? UserInterfaceStandards.blurRadius
: 0)
}
.alert(isPresented: $alertModel.showAlert) {
guard let alertState = alertModel.alertState else {
return Alert(title: Text("Unexected internal error as occured."))
}
return Alert(title: Text(alertState.title))
}
}
}

Updating a #Published variable based on changes in an observed variable

I have an AppState that can be observed:
class AppState: ObservableObject {
private init() {}
static let shared = AppState()
#Published fileprivate(set) var isLoggedIn = false
}
A View Model should decide which view to show based on the state (isLoggedIn):
class HostViewModel: ObservableObject, Identifiable {
enum DisplayableContent {
case welcome
case navigationWrapper
}
#Published var containedView: DisplayableContent = AppState.shared.isLoggedIn ? .navigationWrapper : .welcome
}
In the end a HostView observes the containedView property and displays the correct view based on it.
My problem is that isLoggedIn is not being observed with the code above and I can't seem to figure out a way to do it. I'm quite sure that there is a simple way, but after 4 hours of trial & error I hope the community here can help me out.
Working solution:
After two weeks of working with Combine I have now reworked my previous solution again (see edit history) and this is the best I could come up with now. It's still not exactly what I had in mind, because contained is not subscriber and publisher at the same time, but I think the AnyCancellable is always needed. If anyone knows a way to achieve my vision, please still let me know.
class HostViewModel: ObservableObject, Identifiable {
#Published var contained: DisplayableContent
private var containedUpdater: AnyCancellable?
init() {
self.contained = .welcome
setupPipelines()
}
private func setupPipelines() {
self.containedUpdater = AppState.shared.$isLoggedIn
.map { $0 ? DisplayableContent.mainContent : .welcome }
.assign(to: \.contained, on: self)
}
}
extension HostViewModel {
enum DisplayableContent {
case welcome
case mainContent
}
}
DISCLAIMER:
It is not full solution to the problem, it won't trigger objectWillChange, so it's useless for ObservableObject. But it may be useful for some related problems.
Main idea is to create propertyWrapper that will update property value on change in linked Publisher:
#propertyWrapper
class Subscribed<Value, P: Publisher>: ObservableObject where P.Output == Value, P.Failure == Never {
private var watcher: AnyCancellable?
init(wrappedValue value: Value, _ publisher: P) {
self.wrappedValue = value
watcher = publisher.assign(to: \.wrappedValue, on: self)
}
#Published
private(set) var wrappedValue: Value {
willSet {
objectWillChange.send()
}
}
private(set) lazy var projectedValue = self.$wrappedValue
}
Usage:
class HostViewModel: ObservableObject, Identifiable {
enum DisplayableContent {
case welcome
case navigationWrapper
}
#Subscribed(AppState.shared.$isLoggedIn.map({ $0 ? DisplayableContent.navigationWrapper : .welcome }))
var contained: DisplayableContent = .welcome
// each time `AppState.shared.isLoggedIn` changes, `contained` will change it's value
// and there's no other way to change the value of `contained`
}
When you add an ObservedObject to a View, SwiftUI adds a receiver for the objectWillChange publisher and you need to do the same. As objectWillChange is sent before isLoggedIn changes it might be an idea to add a publisher that sends in its didSet. As you are interested in the initial value as well as changes a CurrentValueSubject<Bool, Never> is probably best. In your HostViewModel you then need to subscribe to AppState's new publisher and update containedView using the published value. Using assign can cause reference cycles so sink with a weak reference to self is best.
No code but it is very straight forward. The last trap to look out for is to save the returned value from sink to an AnyCancellable? otherwise your subscriber will disappear.
A generic solution for subscribing to changes of #Published variables in embedded ObservedObjects is to pass objectWillChange notifications to the parent object.
Example:
import Combine
class Parent: ObservableObject {
#Published
var child = Child()
var sink: AnyCancellable?
init() {
sink = child.objectWillChange.sink(receiveValue: objectWillChange.send)
}
}
class Child: ObservableObject {
#Published
var counter: Int = 0
func increase() {
counter += 1
}
}
Demo use with SwiftUI:
struct ContentView: View {
#ObservedObject
var parent = Parent()
var body: some View {
VStack(spacing: 50) {
Text( "\(parent.child.counter)")
Button( action: parent.child.increase) {
Text( "Increase")
}
}
}
}