Notifications when an ObservedObject is changed - swift

I have a graphical user interface made of several SwiftUI components (lets call them subviews).
These subviews communicate with each other with the help of ObservableObject / ObservedObject.
When changes are made to one view, the other view is automatically notified and updates itself.
However, one of the subviews is not a SwiftUI view but a SpriteKit SKScene.
Here, too, I would like to react to the changes in an observed value.
But I do not have a view that updates automatically. I want to make adjustments to the sprites, depending on the observed value.
How can I be notified of changes in the value?
Can I call a method as soon as the value of the ObservedObject changes?

It is easy to observe changes in ObservedObject from UIKit or SKScene. The example below is from UIViewController, change this for the equivalent to SKScene:
import Combine
class Object: ObservableObject { 
#Published var value: Int = 10
#Published var anotherValue: String = "Hello"
}
class MyViewController: UIViewController { 
let observedObject = Object()
var cancellableBag = Set<AnyCancellable>()
override func viewDidLoad() { 
super.viewDidLoad()
// REACT TO ANY #PUBLISHED VARIABLE
observedObject.objectWillChange.sink { 
// Do what you want here
}.store(in: &cancellableBag)
// REACT ONLY TO ONE SPECIFIED #PUBLISHED VARIABLE
observedObject.$value.sink { value in
// Do what you want here
}.store(in: &cancellableBag)
}
}
Note that #Published is a propertyWrapper with projectedValue. The projectedValue in this case is the Publisher, that emits events every time the wrappedValue will be changed, so in order to access to the projectedValue it is necessary to call $value instead of value.

Related

How/Where to assign CoreData entity's variable to structs's #State variable in SwiftUI?

I use #State variables (title, description) to communicate with the other view (UITextView). That view has a binding string variable (text).
I want to use specific CoreData entity (Item) to update my #State variables, so the text in UITextView is updated when view appears (so UITextView does not appear blank). As far as I understand, I should somehow assign that CoreData entity variable to my #State variable at first and, when I get updates from UITextView on my #State variables, I should save context to my CoreData entity. I use init() for initial assign and I don't get any errors until I get to live preview debugging. I get this error (as a debugger message):
Failed to call designated initializer on NSManagedObject class 'Item'
Also, the text is missing on the preview.
Maybe there's the other way to do what I want?
Here's my code:
Main view
struct DetailView: View {
#ObservedObject var item: Item = Item()
#State private var title = ""
#State private var description = ""
init (item: Note)
{
self.item = item
self.title = item.title!
...
TextView(text: self.$title...
...
TextView(text: self.$details...
...
TextView
struct TextView: UIViewRepresentable {
#Binding var text: String
...
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text
...
P.S. when I directly pass (ignoring existence of my #State variables) in CoreData entity's property (e.g. self.item.title) as an argument for UITextView displaying text actually works but I don't know how to save the changes from UITextView to CoreData entity directly
There are a few ways of doing this.
There is the #FetchedRequest way for which there are many tutorials out there.
https://www.hackingwithswift.com/quick-start/swiftui/what-is-the-fetchrequest-property-wrapper
With that method you would pass the users object that you are editing.
Also, you can use an FetchedResultsController.
https://www.youtube.com/watch?v=-U-4Zon6dbE
This way you can have an #ObservableObject with the CoreData objects
https://developer.apple.com/documentation/coredata/nsfetchedresultscontroller
A ManagedObject or CoreData object already conforms to ObservableObject
https://developer.apple.com/documentation/coredata/nsmanagedobject
You don't need to put it in an #State you just need to pass the Fetched Object from CoreData.
Also, when the user is done with the object you need to save the managedObjectContext.

Sharing ViewModel between SwiftUI components

I'd like to implement a navigator/router for an architecture implemented with SwiftUI and Combine. In a few words the View will share viewModel with Router. When the View triggers a change on the viewModel the Router should navigate to a new sheet.
This is a version of my code where I'm directly passing the viewModel from View to Router. Is there anything wrong? My biggest doubt is that since I'm using #ObservedObject on both the Router and the View, two different instances of the viewModel are created.
VIEW MODEL
class BootViewModel:ObservableObject{
#Published var presentSignIn = false
}
VIEW
struct BootView: View {
#ObservedObject var viewModel:BootViewModel
var navigator:BootNavigator<BootView>? = nil
init(viewModel:BootViewModel) {
self.viewModel = viewModel
self.navigator = BootNavigator(view: self, viewModel: viewModel)
self.navigator.setSubscriptions()
}
var body: some View {
VStack{
Text("Hello")
Button("Button"){
self.viewModel.presentSignIn.toggle()
}
}
}
}
NAVIGATOR
class BootNavigator<T:View>{
var view:T? = nil
#ObservedObject var viewModel:BootViewModel
init(view:T, viewModel:BootViewModel) {
self.view = view
self.viewModel = viewModel
}
func setSubscriptions(){
subscribe(onSigninPressed: $viewModel.presentSignIn)
}
func subscribe(onSigninPressed : Binding<Bool>){
_ = view.sheet(isPresented: $viewModel.presentSignIn){
SignInView()
}
}
}
Why the SignInView is never presented?
Without taking into account the fact that using a router with swiftUI is not needed in general(I'm mostly doing an exercise)... is there anything wrong with this implementation?
This
view.sheet(isPresented: $viewModel.presentSignIn){
SignInView()
MUST be somewhere in body (directly or via computed property or func) but inside body's ViewBuilder
Some notes I have to point out here:
ValueType
There is a difference between an UIView and a SwiftUI View. All SwiftUI Views are value type! So they get copied when you pass them around. Be aware of that.
Single instance
If you want a single instance like a regular navigator for your entire app, you can use singleton pattern. But there is a better approach in SwiftUI universe called #Environment objects. You can take advantage of that.
Trigger a view refresh
To refresh the view (including presenting something), you must code inside the var body. But it can be directly written on indirectly through a function or etc.

Passing data back to controllers when struct model modified

I have a series of View Controllers which pass a struct model object down the chain.
If a user modifies the value of a property on the model, I update the view controller's model instance, and now I need to inform the parent view controllers that this object's value has changed.
Previously I would have used classes over structs for my model object and so I wouldn't have this issue as the object would have been directly written to.
But since structs are pass by value, I have to update the state on other view controllers. I have been using a singleton Manager object to handle state changes through a call to updateModel(). Is there a better way?
I have used something similar to this; keep a reference to the neighbouring view controller (with care to avoid a reference cycle) and a property observer on the struct property to update it when it changes.
This could also be updated prior to presenting a new view controller or before a segue, depending on your needs.
class myViewController: UIViewController {
// Your struct
var model: MyStruct? {
didSet {
if let pvc = previousVC {
pvc.model = model
}
}
}
// Keep a reference to the previous view controller on your stack
var previousVC: UIViewController?
override viewDidLoad() {
super.viewDidLoad()
self.model = MyStruct()
}
}

Why/when are uninitialized non-optional values allowed in Swift for EnvironmentObjects?

In Swift, this crashes at runtime:
class EmptyData: BindableObject {
let didChange = PassthroughSubject<EmptyData, Never>()
}
struct RandomView : View {
#EnvironmentObject var emptyData: EmptyData
#EnvironmentObject var emptyData2: EmptyData
var body: some View {
Text("Hello World!")
}
}
and in the SceneDelegate.swift:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let window = UIWindow(frame: UIScreen.main.bounds)
// The emptyData variables are not initialized as seen below
window.rootViewController = UIHostingController(rootView: RandomView())
self.window = window
window.makeKeyAndVisible()
}
Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
Fixing the problem isn't that hard, but rather strange:
window.rootViewController = UIHostingController(rootView: RandomView().environmentObject(EmptyData()))
So what's happening here? I pass EmptyData() and SwiftUI decides that both emptyData and emptyData2 should be initialized with the same object reference? I can pass also other environmentobjects that do not even exists as variables in the RandomView instance:
window.rootViewController = UIHostingController(rootView: RandomView().environmentObject(EmptyData()).environmentObject(SomeData()))
And SwiftUI just happily run, although SomeData() isn't used anywhere in the instance of RandomView() and should trigger a compile time error in my opinion.
Why are uninitialized values permitted at compile time without initializing them when initializing the object and why are we free to pass environment instances without doing anything with them? Looks a bit like Javascript to me, I loved the strong static safe typing in Swift... I don't see right away why the member-wise initializer just generates an initializer which takes the environment variables as it's parameter.
The EnvironmentObject property delegate has an init() method taking no parameters, and that provides an implicit initialization for the wrapped properties
#EnvironmentObject var emptyData: EmptyData
#EnvironmentObject var emptyData2: EmptyData
(this is explained in the Modern Swift API Design video roughly at 28:10). So that is why these (non-optional) properties do not need an (explicit) initial value.
The documentation also states that EnvironmentObject is (emphasis added)
... a dynamic view property that uses a bindable object supplied by an ancestor view to invalidate the current view whenever the bindable object changes.
You must set a model object on an ancestor view by calling its environmentObject(_:) method.
So this is how I understand it:
If a matching bindable object (in your case: an instance of EmptyData) is found in the environment of the current view or one of its ancestors then the properties are initialized to this object.
If no matching bindable object if found in an ancestor view then the program terminates with a runtime error.
Environment objects can be used in all, some, or none of the views in the view hierarchy. (See Data Flow Through SwiftUI at 29:20.) Therefore it is not an error to provide an environment object (in your case: an instance of SomeData) which is not used in RandomView.
What is #EnvironmentObject?
A linked View property that reads a BindableObject supplied by an
ancestor
So, the environment prop can be supplied to children from the ancestor, not necessarily it should come from its immediate parent.
With that, take a look at the below snippet, since RandomViewGrandParent injects the required Env objects into the environment, RandomViewParent doesn't have to do anything if the children of RandomViewParent needs same Env obj. RandomViewParent can just initiate view without passing the env obj again.
class EmptyData: BindableObject {
let didChange = PassthroughSubject<EmptyData, Never>()
}
struct RandomViewGrandParent : View {
var body: some View {
RandomViewParent().environmentObject(EmptyData())
}
}
struct RandomViewParent : View {
#EnvironmentObject var emptyData: EmptyData
#EnvironmentObject var emptyData2: EmptyData
var body: some View {
RandomView()
}
}
struct RandomView : View {
#EnvironmentObject var emptyData: EmptyData
#EnvironmentObject var emptyData2: EmptyData
var body: some View {
Text("Hello World!")
}
}
And to ans your another question -
I pass EmptyData() and SwiftUI decides that both emptyData and
emptyData2 should be initialized with the same object reference?
That's because EnvironmentObject conforms to BindableObject and BindableObject's didChange is a Publisher, so I believe it thinks both emptyData and emptyData2 wants to subscribe to the same events/values hence uses the same ref for both.

Swift: How to link Touch Bar controls to main window controls

I'm new to Swift/macOS dev, plenty of dev experience otherwise though. Just trying to make something rudimentary.
Here's my app storyboard:
I'm trying to get:
the Touch Bar slider to change when the slider on the main window changes
vice versa
update the Touch Bar Label button with the Int value of the slider.
Q) How do I achieve this?
Note: The main window slider control is wired up and working when I manipulate it e.g.
#IBOutlet weak var mySlider: NSSlider!
#IBAction func mySlider_Changed(_ sender: NSSlider) {
//... stuff happens here.
}
You'll want your view controller to have some explicit model/state of what the value of these sliders have. e.g.
class ViewController : NSViewController {
var value: Double
}
Then you can connect the sliders and textfield to update or display this value.
Approach 1: Target/Action/SetValue
This follows the use of explicit IBActions that you had started. In response to that action, we'll pull the doubleValue from the slider and update the ViewController's model from that:
#IBAction func sliderValueChanged(_ sender: NSSlider) {
value = sender.doubleValue
}
The second piece is updating everything to reflect that new value. With Swift, we can just use the didSet observer on the ViewController's value property to know when it changes and update all of the controls, e.g:
#IBOutlet weak var touchBarSlider: NSSlider!
#IBOutlet weak var windowSlider: NSSlider!
#IBOutlet weak var windowTextField: NSTextField!
var value: Double {
didSet {
touchBarSlider.doubleValue = value
windowSlider.doubleValue = value
windowTextField.doubleValue = value
}
}
And that's it. You can add a number formatter to the textfield so it nicely displays the value, which you can do in Interface Builder or programmatically. And any other time you change the value, all of the controls will still get updated since they are updated in the didSet observer instead of just the slider action methods.
Approach 2: Bindings
Bindings can eliminate a lot of this boiler plate code when it comes to connecting model data to your views.
With bindings you can get rid of the outlets and the action methods, and have the only thing left in the view controller be:
class ViewController: NSViewController {
#objc dynamic var value: Double
}
The #objc dynamic makes the property be KVO compliant, which is required when using bindings.
The other piece is establishing bindings from the controls to our ViewController's value property. For all of the controls this is done by through the bindings inspector pane, binding the 'Value' of the control to the View Controller's value key path:
And that's it. Again, you could add a number formatter to the textfield, and any other changes to the value property will still update your controls since it will trigger the bindings to it. (you can also still use the didSet observer for value to make other changes that you can't do with bindings)