SwiftUI - ObservableObject created multiple times - navigationview

I have created an ObservableObject in a View.
#ObservedObject var selectionModel = FilterSelectionModel()
I put a breakpoint inside the FilterSelectionModel's init function and it is called multiple times. Because this View is part of a NavigationLink, I understand that it gets created then and along with it, the selectionModel. When I navigate to the View, the selectionModel is created again.
In this same View I have a "sub View" where I pass the selectionModel as an EnvironmentObject so the sub-view can change it.
AddFilterScreen().environmentObject(self.selectionModel)
When the sub view is dismissed, the selectionModel is once more created and the changes made to it have disappeared.
Interesting Note: At the very top level is a NavigationView. IF I add
.navigationViewStyle(StackNavigationViewStyle())
to this NavigationView, my selectionModel's changes disappear. BUT if I do not add the navigationStyle, the selectionModel's changes made in the sub view remain!! (But I don't want a split nav view, I want a stacked nav view)
In both cases - with or without the navigationStyle, the selectionModel is created multiple times. I can't wrap my head around how any of this is supposed to work reliably.

Latest SwiftUI updates have brought solution to this problem. (iOS 14 onwards)
#StateObject is what we should use instead of #ObservedObject, but only where that object is created and not everywhere in the sub-views where we are passing the same object.
For eg-
class User: ObservableObject {
var name = "mohit"
}
struct ContentView: View {
#StateObject var user = User()
var body: some View {
VStack {
Text("name: \(user.name)")
NameCount(user: self.user)
}
}
}
struct NameCount: View {
#ObservedObject var user
var body: some View {
Text("count: \(user.name.count)")
}
}
In the above example, only the view responsible (ContentView) for creating that object is annotating the User object with #StateObject and all other views (NameCount) that share the object is using #ObservedObject.
By this approach whenever your parent view(ContentView) is re-created, the User object will not be re-created and it will persist its #State, while your child views just observing to the same User object doesn't have to care about its re-creation.

You can instantiate the observable object in the init method, in this way you will be able to hold its value or the value won't disappear.
Instantiate this way in the view file.
#ObservedObject var selectionModel : FilterSelectionModel
init() {
selectionModel = FilterSelectionModel(value : "value to be saved from disappearing")
}
Instantiate this way in the viewModel file.
class FilterSelectionModel : ObservableObject {
#Published var value : String
init(value : String) {
self.value = value
}
}
This is a workaround that I found, but still, the init method is called multiple times and I didn't get any success with this issue.
In order to stop multiple initializing of the ViewModels as the view is declared in the Navigation View and SwiftUI uses struct which is a value type, so eventually these are initialized before the view is presented, therefore you can convert that view into a LazyView, so that it will only be initialized once the view is about to be presented or shown.
// Use this to delay instantiation when using `NavigationLink`, etc...
struct LazyView<Content: View>: View {
var content: () -> Content
var body: some View {
self.content()
}
}
You can call it like this...
NavigationLink(destination: LazyView { ViewTobePresented() })

Related

Access fragment view in parent activity

I have an activity which displays multiple fragments depending on which one is selected.
I also have a button in this activity and I want to obtain a value from a specific fragment when this button is clicked.
How can I obtain this value?
I tried to get the view I wanted from the fragment from the activity as the code shows below but I can understand that it doesn't work since the fragment is still to be created.
onOffButton.setOnClickListener {
if (onOffButton.text.contains("ON")) {
onOffButton.text = "TURN OFF"
var hoursPicker = findViewById<NumberPicker>(R.id.hoursPicker)
}
}
The short version is you shouldn't do this, there are all kinds of complications (especially when you're trying to access the Fragment's Views).
It's even more complicated if the Fragment might not even be added to the UI at all! If it's not there, what value are you supposed to use? If you want to somehow create the Fragment just so it exists, and so you can read the value from its text box, then that's a sign the value really needs to be stored somewhere else, so you don't need the Fragment if you want to access it.
The easiest, recommended, and modern way to share data like this is with a ViewModel:
class MyViewModel : ViewModel() {
// setting a default value here!
var currentHour: Int = 0
}
class MyActivity : AppCompatActivity() {
val model: MyViewModel by viewModels()
fun onCreate(...) {
...
onOffButton.setOnClickListener {
// access the data in the ViewModel
val currentHour = model.currentHour
}
}
}
class MyFragment : Fragment() {
// using activityViewModels so we get the parent Activity's copy of the VM,
// so we're all sharing the same object and seeing the same data
val model: MyViewModel by activityViewModels()
fun onViewCreated(...) {
...
hoursPicker.setOnValueChangeListener { _, _, newValue ->
// update the VM
model.currentHour = newValue
}
}
}
So basically, you have this ViewModel object owned by the Activity and visible to its Fragments. The VM outlives all of those components, so you don't lose data while an Activity is being destroyed on rotation, or when a Fragment isn't added to the UI, etc.
The VM is the source of data, everything else just reads from it, or updates it when something changes (like when the Fragment updates the variable when its number picker's value changes). This way, the Activity doesn't need to go "ask" the Fragment for info - it's stored in a central location, in the VM
This is the most basic way to use a ViewModel - you can start using LiveData and Flow objects to make different UI components observe data and react to changes too. For example, your button in your Activity could change some enabled state in the VM, and the Fragment (if it's added) will see that change and can do things like make the number picker visible or invisible.
It's way easier to coordinate this stuff with a ViewModel, so if you don't already know how to use them, I'd recommend learning it!

Is it possible to prevent property changes of an object from triggering didSet{} in Swift/Obj-c

I have an object bound to a controller as shown below:
// ViewController
#objc dynamic var objectX: SomeClass {
didSet {
print("something")
}
}
override func viewDidLoad() {
self.bind(NSBindingName(rawValue: "objectX"),
to: controller,
withKeyPath: "selectedObject",
options: nil)
}
An I expect didSet{} to be called if the controller has a different object selected. However if one of the properties of objectX gets changed then didSet{} is also getting called.
Surely this shouldn't be occurring - and if this is expected then is there any way to prevent property changes on the object itself from triggering the didSet{} or is it necessary to check in the didSet{} whether the object is a different one to the old one?
This is a bit odd because when I test in a Playground changing a property on the object does not trigger the didSet{} callback.

Why are variables declared after root automatically added to UI in TornadoFx?

I have stumbled upon a behaviour in TornadoFx that doesn't seem to be mentioned anywhere (I have searched a lot) and that I'm wondering about.
If I define a view like this with the TornadoFx builders for the labels:
class ExampleView: View() {
override
val root = vbox{ label("first label") }
val secondLabel = label("second label")
}
The result is:
That is, the mere definition of secondLabel automatically adds it to the rootof the scene.
However, if I place this definition BEFORE the definition of root...
class ExampleView: View() {
val secondLabel = Label("second label")
override
val root = vbox{ label("first label") }
}
... or if I use the JavaFx Labelclass instead of the TornadoFx builder ...
class ExampleView: View() {
override
val root = vbox{ label("first label") }
val secondLabel = Label("second label")
}
... then it works as I expect:
Of course, I can simply define all variables in the view before I define the rootelement but I'm still curious why this behaviour exists; perhaps I am missing some general design rule or setting.
The builders in TornadoFX automatically attach themselves to the current parent in the scope they are called in. Therefore, if you call a builder function on the View itself, the generated ui component is automatically added to the root of that View. That's what you're seeing.
If you really have a valid use case for creating a ui component outside of the hierarchy it should be housed in, you shouldn't call a builder function, but instead instantiate the element with it's constructor, like you did with Label(). However, the use cases for such behavior are slim to none.
Best practice is to store value properties in the view or a view model and bind the property to the ui element using the builders. You then manipulate the value property when needed, and the change will automatically update in the ui. Therefore, you very very seldom have a need to access a specific ui element at a later stage. Example:
val myProperty = SimpleStringProperty("Hello world")
override val root = hbox {
label(myProperty)
}
When you want to change the label value, you just update the property. (The property should be in an injected view model in a real world application).
If you really need to have a reference to the ui element, you should declare the ui property first, then assign to it when you actually build the ui element. Define the ui property using the singleAssign() delegate to make sure you only assign to it once.
var myLabel: Label by singleAssign()
override val root = hbox {
label("My label) {
myLabel = this
}
}
I want to stress again that this is very rarely needed, and if you feel you need it you should look to restructure your ui code to be more data driven.
Another technique to avoid storing references to ui elements is to leverage the EventBus to listen for events. There are plenty of examples of this out there.

Create an arbitrary view

So, I have a case in which I need to have N rows in form of: Label TextView/Checkbox. Maybe I will have to have more than those two views, so I want to be able to support anything that is TornadoFx View.
I've created an interface that has one method that returns TornadoFx View and it looks like this:
interface ValueContainer {
fun getView() : View
}
One of the implementations looks like this:
class BooleanValueContainer(val checked: Boolean) : ValueContainer {
val valueProperty = SimpleBooleanProperty(checked)
override fun getView(): View {
return (object : View() {
override val root = checkbox {
bind(valueProperty)
}
})
}
}
Now, when I try to use it inside init block, it doesn't show in the layout. root is GridPane and parameters is a list of objects that have name and reference to ValueContainer implementation (BooleanValueContainer or other one which I haven't shown):
init {
with(root) {
parameters.map {
row(it.name) {
it.parameterContainer.getView()
}
}
}
}
I'm stuck here for quite a while and I've tried anything I could find but nothing really worked except putting textview or checkbox block instead of getView() call, but then I would have to have logic on what view should I show inside this class which represents a view and I don't want that.
The reason this is not working for you is that you simply call parameterContainer.getView() but you don't add the View to the row. I think what's confusing you is that for builders you can just say label() for example, and it's added to the current Node in the builder tree. In your case, you just say Label() (just create an instance of Label, not call the label builder), which would create a new Label, but not add it to the children list of the current Node. To solve your problem, do:
this += it.parameterContainer.getView()
This will add the View to the row.
Apart from this, I don't quite see the point of the ValueContainer. What does it solve to put a View inside this container object? I suspect this as well might be due to a misunderstanding and I'd like to understand why you feel that you need this construct.

how to pass data between UICollectionViewLayout and UIViewController [duplicate]

This question already has answers here:
Passing data between view controllers
(45 answers)
Closed 7 years ago.
I need to create a custom layout based on http call to an api. Based on result from api in loop i will populate an array from CollectionView
In CustomLayout I need to use this array in order to draw correctly details in each row.
In collectionView I use reload data after the data was added to array
Thank YOu
code:
class epgViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource{
var events = [Event]()
#IBOutlet weak var collectionView: UICollectionView!
//another initializations
. . .
//====== load Events ===============
func get_events() {
//http api call
//as result in loop i append data to Events()
self.events.append(newEvent)
//reload collection view data
self.collectionView.reloadData()
}
}
//////////
/// LAyout
////////
class customCollectionViewLayout: UICollectionViewLayout {
//prepare layout
override func prepareLayout() {
for section in 0...collectionView!.numberOfSections()-1 {
how to access array from collectionView
Events[i] ????
}
}
}
I'm not entirely sure I understand your situation, but I'll provide two methods for communication within your application.
Using NSNotificationCenter
Add an observer In the viewDidLoad of the controller that needs an event fired:
NSNotificationCenter.defaultCenter().addObserver(self, selector: "remoteRefresh:", name:"remoteRefreshID", object: nil)
In that same controller add an observer with a name the same as our selector, in the above example "remoteRefresh".
func remoteRefresh(notification: NSNotification) {
// do stuff
}
Then in your other controller can post messages and your remoteRefresh action will run:
NSNotificationCenter.defaultCenter().postNotificationName("remoteRefreshID", object: nil)
for more details I have a two part tutorial on NSNotificationCenter
Protocol / Delegate pattern
The second is the Protocol / Delegate pattern which is a bit more involved but I have a written tutorial here:
protocol / delegate pattern
Another Option
Here's an option if you're still having issues. You could declare a global struct to hold your data, paste this into a playground to understand.
struct SessionData {
static var events = [String]()
}
class classOne {
func addValues() {
SessionData.events.append("a message")
SessionData.events.append("another message")
}
}
class classTwo {
func getValues() -> [String] {
return SessionData.events
}
}
classOne().addValues()
classTwo().getValues()