SwiftUI ObservedObject is re-initiated everytime - ios

I have a simple class to store some data in my SwiftUI app:
final class UserData: ObservableObject {
#Published var id = 1
#Published var name = "Name"
#Published var description = "Initial text"
}
I also defined it as EnvironmentObject in the main app struct:
ContentView(document: file.$document)
.environmentObject(UserData())
In my Content View, I embedded a UIKit Text View:
EditorTextView(document: $document.text)
Where EditorTextView is a UITextView that is applied through UIViewRepresentable.
Now, what I'm trying to do is to update UserData within EditorTextView, like, storing some user input into UserData.description. So I defined the class inside my Coordinator as follows (the code is just for example):
class Coordinator: NSObject, UITextViewDelegate {
#ObservedObject var userData = UserData()
...
// Somewhere in the code, I update UserData when user taps Enter:
func textViewDidChange(_ textView: UITextView) {
if (textView.text.last == "\n") {
userData.description = "My New Text"
}
}
My problem is:
Although it is updated with "My New Text" (as debugger shows), the value of userData.description is re-initiated again with the value "Initial text". It's like if UserData class is created everytime.
I tried to use #StateObject instead of #ObservedObject, but it doesn't make a difference.

"It's like if UserData class is created everytime."
It is created everytime, right here:
class Coordinator: NSObject, UITextViewDelegate {
#ObservedObject var userData = UserData() // <--- here
Try this instead, and pass the UserData in from outside:
class Coordinator: NSObject, UITextViewDelegate {
#ObservedObject var userData: UserData
Typically you would also do this in your main app struct:
#StateObject var userData = UserData()
...
ContentView(document: file.$document).environmentObject(userData)

Related

SwiftUI not updating with Bluetooth events that change object variables

I’m playing around with some Bluetooth objects and having issues getting SwiftUI to update the view.
Essentially I have a CBCentralManagerDelegate object (btManager) that has a published array of CBPeripheralDelegate objects (btPeripheral) that store their state in variables that get updated when the subscribed Bluetooth messages get processed. However the SwiftUI display (a row in a List) doesn’t update immediately but usually when something else happens to trigger an update (e.g. a different button press).
Im guessing it’s to do with the asynchronous way the peripheral’s delegate methods get called, but despite both classes being ObservableObjects and the array in the manager being #Published it doesn’t update the display straight away.
Any tips would be welcome!
Difficult to show all the code, but the gist of it is:
class BTDataManager: NSObject, ObservableObject, CBCentralManagerDelegate {
#Published var peripherals: [BTPeripheral] = []
private var centralManager: CBCentralManager!
// CBCentralManagerDelegate methods
}
class BTPeripheral: NSObject, ObservableObject, Identifiable, CBPeripheralDelegate {
var manager: BTDataManager?
var peripheral: CBPeripheral?
var RSSI: Int?
var data = "Test"
var id = UUID()
// CBPeripheralDelegate methods
}
So for example when a peripheral delegate method is called and the data var is updated, the SwiftUI view doesn't immediately update.
My ListView is:
struct PeripheralList: View {
#EnvironmentObject var modelData: BTDataManager
var body: some View {
List{
ForEach (modelData.peripherals){ peripheral in
PeripheralDetailRow(peripheral: peripheral)
.environmentObject(modelData)
}
}
}
}
struct PeripheralDetailRow: View {
#EnvironmentObject var modelData: BTDataManager
var peripheral: BTPeripheral
var index: Int {
modelData.peripherals.firstIndex(where: { $0.id == peripheral.id })!
}
var body: some View {
VStack {
Text(peripheral.id.uuidString.suffix(5))
Text(modelData.peripherals[index].data)
.font(caption)
}
}
When a peripheral's delegate function is fired and updates the data var, then the view doesn't update until something else triggers a refresh.
Ok, after typing this out I think I have solved it.
var data becomes #Published var data
In the DetailRow, I have #EnvironmentObject var peripheral: BTPeripheral and I don't need the modelData or index stuff.
In the List View I use .environmentObject(peripheral) to pass it through.
Seems to work now, although I'm not 100% that this is the correct way.

How do you edit an ObservableObject’s properties in SwiftUI from another class?

I’m looking for the proper pattern and syntax to address my goal of having an ObservableObject instance that I can share amongst multiple views, but while keeping logic associated with it contained to another class. I’m looking to do this to allow different ‘controller’ classes to manipulate the properties of the state without the view needing to know which controller is acting on it (injected).
Here is a simplification that illustrates the issue:
import SwiftUI
class State: ObservableObject {
#Published var text = "foo"
}
class Controller {
var state : State
init(_ state: State) {
self.state = state
}
func changeState() {
state.text = "bar"
}
}
struct ContentView: View {
#StateObject var state = State()
var controller: Controller!
init() {
controller = Controller(state)
}
var body: some View {
VStack {
Text(controller.state.text) // always shows 'foo'
Button("Press Me") {
print(controller.state.text) // prints 'foo'
controller.changeState()
print(controller.state.text) // prints 'bar'
}
}
}
}
I know that I can use my ObservableObject directly and manipulate its properties such that the UI is updated in response, but in my case, doing so prevents me from having different ‘controller’ instances depending on what needs to happen. Please advise with the best way to accomplish this type of scenario in SwiftUI
To make SwiftUI view follow state updates, your controller needs to be ObservableObject.
SwiftUI view will update when objectWillChange is triggered - it's done automatically for properties annotated with Published, but you can trigger it manually too.
Using same publisher of your state, you can sync two observable objects, for example like this:
class Controller: ObservableObject {
let state: State
private var cancellable: AnyCancellable?
init(_ state: State) {
self.state = state
cancellable = state.objectWillChange.sink {
self.objectWillChange.send()
}
}
func changeState() {
state.text = "bar"
}
}
struct ContentView: View {
#StateObject var controller = Controller(State())

SwiftUI + MVVM + DI

I've spent a while researching this and found lots of examples of SwiftUI, MVVM and DI separately but none combined, so I'm assuming I've misunderstood something.
I have a new SwiftUI app and aiming to use the above.
I have the following
A DependencyContainer
protocol ViewControllerFactory {
func makeFirstViewController() -> First_ViewModel
func makeSecondViewController() -> Second_ViewModel
}
class DependencyContainer : ObservableObject {
let database = AppDatabase()
}
extension DependencyContainer: ViewControllerFactory {
func makeFirstViewController() -> First_ViewModel {
return First_ViewModel(appDatabase: database)
}
func makeSecondViewController() -> Second_ViewModel {
return Second_ViewModel()
}
}
In my app entry point I have:
#main
struct MyApp: App {
var body: some Scene {
let container = DependencyContainer()
WindowGroup {
First_View()
.environment(\.container, container)
}
}
}
private struct Container: EnvironmentKey {
static let defaultValue = DependencyContainer()
}
extension EnvironmentValues {
var container: DependencyContainer {
get { self[Container.self] }
set { self[Container.self] = newValue }
}
}
Now I run into problems
How do I use the container in the views?
struct First_View: View{
#Environment(\.container) var container
#ObservedObject private var firstViewModel : First_ViewModel
init(){
_firstViewModel = container.makeFirstViewController()
}
Gives error
"Cannot assign value of type 'First_ViewModel' to type 'ObservedObject<First_ViewModel>'"
If I inject the container like below,
struct First_View: View {
#ObservedObject private var firstViewModel : First_ViewModel
init (container : DependencyContainer){
_firstViewModel = container.makeFirstViewController()
It works but not how I thought it should work
The #Environment() property wrapper type relies on a defined set of key names. You can create your own (this blog post from Use Your Loaf is a good description of how to do this). This is designed for value types, though.
However, as you’ve already declared DependencyContainer as an ObservableObject you can use the #EnvironmentObject property wrapper:
struct MyApp: App {
#StateObject var container = DependencyContainer()
var body: some View {
WindowGroup {
First_View()
.environmentObject(container)
}
}
}
Then, in any child view where you need access, you use the #EnvironmentObject wrapper in your declaration. Note that you have to include the class type; you can have multiple environment objects defined, but a maximum of one of each class.
struct First_View: View {
#EnvironmentObject var container: DependencyContainer
// ...
}
Note also that #EnvironmentObject assumes that it will be able to find an instance of DependencyContainer - if it can’t find one, it will crash. When you’re defining one at the app level that shouldn’t be a problem. However, in your SwiftUI previews you will need to specify a suitable instance or the preview subsystem may crash.
So if you had a static method that prepared a preview-suitable version of your dependencies, you could write:
struct First_View_Previews: PreviewProvider {
static var previews: some View {
First_View()
.environmentObject(DependencyContainer.previewInstance)
}
}

Subscribing to value changes inside child objects of environment view models (view not being re-rendered when this happens)

I have a view model that is parent to other children view models. That is:
public class ViewModel: ObservableObject {
#Published var nav = NavigationViewModel()
#Published var screen = ScreenViewModel()
The other children view model, such as nav and screen, all serve a specific purpose. For example, nav’s responsibility is to keep track of the current screen:
class NavigationViewModel: ObservableObject {
// MARK: Publishers
#Published var currentScreen: Screen = .Timeline
}
The ViewModel is instantiated in the App struct:
#main
struct Appy_WeatherApp: App {
// MARK: Global
var viewModel = ViewModel()
// MARK: -
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(viewModel)
}
}
}
And I declare an #EnvironmentObject for it on any view that needs access to it:
#EnvironmentObject var viewModel: ViewModel
Any view referencing a non-object property of ViewModel that is being #Published whose value changes will result in the view to be re-rendered as expected. However, if the currentScreen #Published property of the NavigationViewModel changes, for example, then the view is not being re-rendered.
I know I can make it work if I separate NavigationViewModel from ViewModel, instantiate it at the app level and use it as its own environment object in any views that access any of its published properties.
My question is whether the above workaround is actually the correct way to handle this, and/or is there any way for views to be subscribed to value changes of properties inside child objects of environment objects? Or is there another way that I’ve not considered that’s the recommended approach for what I’m trying to achieve through fragmentation of view model responsibilities?
There are several ways to achieve this.
Option 1
Using Combine.
import Combine
public class ViewModel: ObservableObject {
#Published var nav = NavigationViewModel()
var anyCancellable: AnyCancellable?
init() {
anyCancellable = nav.objectWillChange.sink { _ in
self.objectWillChange.send()
}
}
}
You basically just listen to whenever your navigationViewModel publishes changes. If so, you tell your views that your ViewModel has changes aswell.
Option 2
I suppose due to the name NavigationViewModel, that you would use it quite often inside other view models?
If that's the case, I would go for a singleton pattern, like so:
class NavigationViewModel: ObservableObject {
static let shared = NavigationViewModel()
private init() {}
#Published var currentScreen: Screen = .Timeline
}
Inside your ViewModel:
public class ViewModel: ObservableObject {
var nav: NavigationViewModel { NavigationViewModel.shared }
}
You can of course also call it inside any View:
struct ContentView: View {
#StateObject var navigationModel = NavigationModel.shared
}
You might have to call objectWillChange.send() after changing publishers.
#Published var currentScreen: Screen = .Timeline {
didSet {
objectWillChange.send()
}
}

Swiftui - How do I initialize an observedObject using an environmentobject as a parameter?

I'm not sure if this is an antipattern in this brave new SwiftUI world we live in, but essentially I have an #EnvironmentObject with some basic user information saved in it that my views can call.
I also have an #ObservedObject that owns some data required for this view.
When the view appears, I want to use that #EnvironmentObject to initialize the #ObservedObject:
struct MyCoolView: View {
#EnvironmentObject userData: UserData
#ObservedObject var viewObject: ViewObject = ViewObject(id: self.userData.UID)
var body: some View {
Text("\(self.viewObject.myCoolProperty)")
}
}
Unfortunately I can't call self on the environment variable until after initialization:
"Cannot use instance member 'userData' within property initializer; property initializers run before 'self' is available."
I can see a few possible routes forward, but they all feel like hacks. How should I approach this?
Here is the approach (the simplest IMO):
struct MyCoolView: View {
#EnvironmentObject var userData: UserData
var body: some View {
MyCoolInternalView(ViewObject(id: self.userData.UID))
}
}
struct MyCoolInternalView: View {
#EnvironmentObject var userData: UserData
#ObservedObject var viewObject: ViewObject
init(_ viewObject: ViewObject) {
self.viewObject = viewObject
}
var body: some View {
Text("\(self.viewObject.myCoolProperty)")
}
}

Resources