simonbs,
@simonbs@mastodon.social avatar

Can anyone think of a design pattern that makes it easy and safe to keep the state of child objects in sync with their parent? I.e. when a property is updated on a parent object, the new value should be forwarded to its children.

The naive approach is to override didSet on the parent's property and forward the new value to the children but it’s easy to forget to update the implementation of didSet when new children are introduced.

iankeen,
@iankeen@mastodon.social avatar

@simonbs I'm not super sure what you're going for. it feels like if you have duplicate state you may need to model it a different way?

Anyway, here's one idea that I think gets the boilerplate down a bit.

I don't love the force casts, but they should be safe.

https://gist.github.com/IanKeen/f2bc44b645d0f502b6bcc296a7b6495d

simonbs,
@simonbs@mastodon.social avatar

@iankeen Interesting approach. I’m unsure how this would work if the children depended on multiple different properties from parent as shown in this example: https://mastodon.social/@simonbs/111347005888446356

iankeen,
@iankeen@mastodon.social avatar

@simonbs I kinda just looks like you want @.State and @.Binding 🤔😂

simonbs,
@simonbs@mastodon.social avatar

@iankeen I’m not writing a SwiftUI app 😞

iankeen,
@iankeen@mastodon.social avatar

@simonbs Conceptually though you could write your own version 😋

Hugo_say,
@Hugo_say@mastodon.social avatar

@simonbs I may have missed something in your different solutions but wouldn’t a Reference class that stores only one property at a time be a solution ?

If you want multiple states to be grouped you can use @dynamicMemberLookup to make the solution more ergonomic (as presented in @sundell’s https://www.swiftbysundell.com/tips/combining-dynamic-member-lookup-with-key-paths/)

image/jpeg

simonbs,
@simonbs@mastodon.social avatar

@Hugo_say Yes, that’s certainly an option too. It’s a bit along the same lines as what I’m doing here but I call the properties “store” instead of “reference”. https://mastodon.social/@simonbs/111347005888446356

One caveat with storing instances of Reference<Value> on Parent is that it provides for a slightly awkward API once these properties become public.

I didn’t think of taking advantage of dynamic member lookup. That’s an interesting idea. I haven’t found a meaningful way to take advantage of it though.

simonbs,
@simonbs@mastodon.social avatar

@Hugo_say I suppose dynamic member lookup could solve the slightly awkward public API that Reference<Value> would introduce. In the example I linked to I use a property wrapper to achieve something similar. I didn’t include the implementation of that property wrapper in my code snippet though.

kylebshr,
@kylebshr@mastodon.social avatar

@simonbs do you still need to be able to set them individually on children?

simonbs,
@simonbs@mastodon.social avatar

@kylebshr I don’t think that’ll be necessary, no.

kylebshr,
@kylebshr@mastodon.social avatar

@simonbs so it's just a getter of the parent's property? I might have it be a closure instead, and pass in { [unowned self] self.isEnabled }. Or do you need a didSet or something too?

simonbs,
@simonbs@mastodon.social avatar

@kylebshr I didn’t think of this until you asked but I will need a didSet or similar.

kylebshr,
@kylebshr@mastodon.social avatar

@simonbs I feel like some sort of (observable) model that the parent owns & passes in makes the most sense

simonbs,
@simonbs@mastodon.social avatar

@kylebshr I think that’s what I’ll end up doing. I have a proposal in this post: https://mastodon.social/@simonbs/111347005888446356

As mentioned in the post I’m still struggling with a few drawbacks. Now, I’m not handling observations in that proposal but I’ll need to figure that out too.

Alexbbrown, (edited )
@Alexbbrown@hachyderm.io avatar

@simonbs I would be asking myself if the property was really part of the child view models!

For example, if it's value is independent of the value of the rest of the child model or has a wildly different timeline for creation and updates, I would put it in a separate data object to pass to the child view

simonbs,
@simonbs@mastodon.social avatar

@Alexbbrown I hear you. You’re thinking something like this, right? https://mastodon.social/@simonbs/111346961141324058

Alexbbrown,
@Alexbbrown@hachyderm.io avatar

@simonbs in your example you inject part of the parent model into the type of the child models

this is required if the child model does computation combining values from the parent and child model

But in the cases where they're independent, I would pass the extra parent values alongside the child value

struct ParentView {
@StateObject var parentModel: Parent
var body: some View {
Child1View(parent.child1, parent.commonBools)
Child2View(parent.child2, parent.commonBools)

simonbs,
@simonbs@mastodon.social avatar

@Alexbbrown I see. I’m leaning towards this being different in SwiftUI and UIKit. I’m using UIKit because I’m using UITextInput.

mlysons,
@mlysons@iosdev.space avatar

@simonbs Could the didSet use reflection, iterating over properties conforming to a protocol and setting the relevant value?

simonbs,
@simonbs@mastodon.social avatar

@mlysons Hmm... I suppose so but it sounds a bit dangerous 😄

mlysons,
@mlysons@iosdev.space avatar

@simonbs Live dangerously, you know you want to 🤪

dzamir,
@dzamir@mastodon.uno avatar

@simonbs Every child extends an interface “isEnabled" containing the property.

The parent has an array of “isEnabled” and iterates over them.

simonbs,
@simonbs@mastodon.social avatar

@dzamir I've thought about this but that's not much different from what I'm doing when forwarding the value through didSet. Whether I need to remember to add a new child to didSet or to an array is the same -- it's easy to forget.

dzamir,
@dzamir@mastodon.uno avatar

@simonbs but the parent only have one responsibility: to track their children.
You add them to the array in a centralised method and then do all kinds of operations always from the array.

For example, a View can create all the subviews that it wants, but if it doesn't call addSubview() the view will never be handled as a child.

simonbs,
@simonbs@mastodon.social avatar

Furthermore, this approach means that the properties on the children may be out of sync from the beginning as shown in this code snippet.

adriano,
@adriano@lile.cl avatar

@simonbs What language is this?

Also, it's a bit distracting to read 'children' object when you're compositing them instead of inheriting them. Furthermore, can't you pass the variable as a parameter in the constructor, or refer to it with a parent.isEnabled statement or the like?

simonbs,
@simonbs@mastodon.social avatar

@adriano It's Swift. If I pass the parameter in the constructor then I need to find a way to update the value once it changes on the parent. Would you then pass an instance of parent into the children? That complicates unit testing.

adriano,
@adriano@lile.cl avatar

@simonbs Person who just read Head First Design Patterns "this sounds like the observer pattern"

raphaelmor,

@simonbs looks like the flyweight pattern might do? Embed the state in an instance that is shared by all the children (and parent)

simonbs,
@simonbs@mastodon.social avatar

@raphaelmor That's something like this, right? https://mastodon.social/@simonbs/111346734354148654

That's the best I've come up with too but I feel like it gets difficult to maintain as the state grows. Especially once not all children need to know about the same state.

raphaelmor,

@simonbs that’s what I had in mind. To simplify a bit, you could use the parent as the “store” and some protocols so that diffeeent children can have access to different part of the state

simonbs,
@simonbs@mastodon.social avatar

@raphaelmor Right. So I'd essentially pass the children a reference to the parent, right?

I'd love this if I could somehow avoid that reference being optional in the child. I don't see any way of doing that since I'd need the reference in the child to be weak to avoid a retain cycle. Also, I wouldn't be able to set the value until after the parent has been created which also means it needs to be an optional AFAIK.

raphaelmor,

@simonbs yeah :/ I can’t think of any obvious ideal solution.

In your case what would the child return if they are created before the parent? They need to maintain their own state but sync it if they’re attached to a parent?

simonbs,
@simonbs@mastodon.social avatar

@raphaelmor It's not important to me that the children are able to maintain their own state as long as they can be unit tested without too much hassle. It is, however, important that the state of the child reflects the state of the parent if it's ever updated.

jaylyerly,
@jaylyerly@mastodon.social avatar

@simonbs @raphaelmor If it makes sense that the children never exist without the parent, you might look into an unowned reference from the child to the parent. AFAIK, unowned exists to fix this problem of reference cycles in two tightly coupled classes like this.

https://docs.swift.org/swift-book/documentation/the-swift-programming-language/automaticreferencecounting/#Unowned-References

simonbs,
@simonbs@mastodon.social avatar

@jaylyerly @raphaelmor I don't think of using unowned. That's a good idea. Thanks! I'll explore how that can work.

simonbs,
@simonbs@mastodon.social avatar

We could introduce a store that holds the value of isEnabled and make sure we reference the same store across the parent and all its children. This solves the issue but becomes difficult to maintain as more properties need to be in sync.

Like, how would we handle it if multiple properties needed to be kept in sync but not all children needed all the properties? A single store would leak information to children that don't need the properties and multiple stores become difficult to manage.

ross,
@ross@okla.social avatar

@simonbs You could make a property wrapper that does that. Similar to SwiftUI’s Binding.

matadan,
@matadan@mastodon.social avatar

@simonbs You could keep the enabled state in a fileprivate var that you use a computed property to access.

simonbs,
@simonbs@mastodon.social avatar

@matadan That could work in this particular example but in a real-world codebase my types will not be living in the same file.

juri,
@juri@mastodon.social avatar

@simonbs Pointfree’s composable architecture has stores with a scope operator which allows you to whittle down child stores to have just bits they need

simonbs,
@simonbs@mastodon.social avatar

This could work. Now I have three children that depends on the state stored in three different properties on an instance of Parent.

The state is kept in sync by passing a store into the children. That store just happens to be the parent itself. Storing the reference as unowned avoids a retain cycle.

Thanks to @raphaelmor and @jaylyerly for the idea!

Only downside I see is that the children are declared with lazy var where they could (and should?) really be declared with let.

HanBrolo,
@HanBrolo@techhub.social avatar

@simonbs @raphaelmor @jaylyerly Why not send them as properties to the children?

simonbs,
@simonbs@mastodon.social avatar

@HanBrolo I'm not entirely sure I know what you're thinking of but I think I've answered that question here: https://mastodon.social/@simonbs/111346689257000120

simonbs,
@simonbs@mastodon.social avatar

I can avoid the lazy vars by extracting the state into a separate type and initializing the children in the initializer of Parent. This code is safe and everything I want to be immutable is immutable. It's just very verbose 😞

lextar,
@lextar@social.blach.io avatar

@simonbs how will the Parent/StateStore notify the children when a property, e.g. the selectedRange changes? Does the child observe the StateStore somehow?

simonbs,
@simonbs@mastodon.social avatar

@lextar I haven’t tackled that part yet but I’m thinking I can introduce the Observer pattern somewhere here. Possibly all stores, like IsEnabledStore, will need to conform to a protocol that enables observation.

It’s a good point and definitely something I need to address but I’ve been thinking that’s easier to solve than my current challenge. I could be wrong though.

simonbs,
@simonbs@mastodon.social avatar

I can introduce a property wrapper to get rid of the trivial getters and setters but it still feels like there's room for improvement here.

For once, Parent and StateStore needs the same properties. Secondly, the declaration and initialization of the childA, childB, and childC properties is verbose.

I wonder if it's possible to get fancy with property wrappers and/or macros to solve this 🤔

everyplace,
@everyplace@mastodon.social avatar

@simonbs
Oh I hadn’t seen @PROXY@mastodon.social before. Interesting …

selsoe,
@selsoe@mastodon.social avatar

@simonbs why do you need to duplicate the state in the parent and children though? The way I see it, you should have one object holding the state and you pass that from parent to children. Use that to access state. That’s your source of truth.

simonbs,
@simonbs@mastodon.social avatar

@selsoe That’s what I’m doing in this example. StateStore is the source of truth. I need to access the state in two places because Parent is my public interface. The children contains logic that uses the state. This is for a framework where Parent represents the public interface and children represent internal logic.

selsoe,
@selsoe@mastodon.social avatar

@simonbs you could make statesStore public as well? Or parts of it. I think state duplication (even if you are just trampolining properties) often lead to clutter and confusion about responsibility.

simonbs,
@simonbs@mastodon.social avatar

@selsoe I could do that but then the interfaces between my parent and children a no longer explicit about which part of the state store the children use. As the state store grows I fear it could cause confusion that every child can randomly access any property in the state store.

I’m also thinking that passing in a state store that conforms to well-defined interfaces makes my unit tests cleaner.

Kowfm,
@Kowfm@ruby.social avatar

@simonbs why don’t you use an array of “children”. If they have common properties then they aren’t special enough to be “named” in this parent object.

simonbs,
@simonbs@mastodon.social avatar

@Kowfm Right, but once the children share more state from the parent and not all children shares the same state then I think the array will fall short.

simonnickel,
@simonnickel@mastodon.social avatar

@simonbs isn’t that the perfect example use case for Combine?

Each property gets a publisher on the parent and the children get initialized with only ones they need.

simonbs,
@simonbs@mastodon.social avatar

@simonnickel I'm actually using Combine to solve this now and I'm revisiting this because I want to move away from Combine. I'm afraid it's already an outdated framework 🙊

rhysmorgan,
@rhysmorgan@mastodon.social avatar

@simonbs @simonnickel Surely it’s not gonna go anywhere, though? It’s just a different means of representing streams of data over time than async/await and observability, and one which might be the better solution for some use cases.

simonbs,
@simonbs@mastodon.social avatar

@rhysmorgan @simonnickel Probably but I’m concerned that Apple released the framework and have given it little to no love since then. It seems that language features will eventually rake over the responsibility of Combine, if they haven’t already. That makes it me concerned to build on top of it.

simonnickel,
@simonnickel@mastodon.social avatar

@simonbs @rhysmorgan I also just use it as little as possible, but as long it’s still the best fitting tool 🤷‍♂️

Just make sure to be able to replace it in the future.

  • All
  • Subscribed
  • Moderated
  • Favorites
  • random
  • kavyap
  • rosin
  • modclub
  • DreamBathrooms
  • InstantRegret
  • magazineikmin
  • khanakhh
  • osvaldo12
  • tacticalgear
  • Youngstown
  • everett
  • slotface
  • ngwrru68w68
  • thenastyranch
  • JUstTest
  • mdbf
  • tester
  • GTA5RPClips
  • provamag3
  • Leos
  • Durango
  • ethstaker
  • cisconetworking
  • normalnudes
  • megavids
  • cubers
  • anitta
  • lostlight
  • All magazines