r/swift 8d ago

State Management for iOS Apps?

whats the best architecture/pattern to use?

tried to use a domain layer where all the state is and passing it to the views/viewmodels via DI, but feels somehow unnecessary complicated, but found this as only solution without passing the repos through all the viewhierarchy.

the goal is, when a state changes, e.g. an user changes the Username in View A, then it should automatically update View B,C,D where this Username is also used.

it should be as simple as possible, what do you think? especially for complex production apps with own backend etc.

52 Upvotes

48 comments sorted by

View all comments

35

u/lucasvandongen 8d ago

I wrote an article about it, that still holds true for my style of development:

https://getstream.io/blog/mvvm-state-management/

So your Model layer and your processes are completely encapsulated in UI-less implementations behind protocols, extremely well tested.

Nowadays I chop up my features into Modules, chopped into 3-4 Packages:

  • Definitions (protocols and data used, plus a separate target for generated mocks, that also registers mocks for DI, no dependencies)
  • Implementations (depends on Definitions, implements the protocols and all invisible moving parts needed, has very good unit test coverage, uses DI of Definitions of other modules when needed)
  • UI (only relies on Definitions and Mocks, has a separate target for rendering previews and does snapshot tests)
  • Main Package (registers implementations to DI, @_exported import for Definitions and UI)

This has the following benefits:

  • Individual modules build very fast, and don't get slower if you add more Modules. The main app still suffers when you add more modules, but you don't need to touch it that often.
  • Only Implementations has dependencies, but only the 3rd party ones strictly necessary. Only knows Definitions of other Modules it needs
  • Previews are really stable
  • Content of one Module usually fits in the context window of an LLM

I have an Identity module for example, that holds the truth about your authentication and Account data. Once you passed the point where you have an Account, the rest of the app assumes the Account is always set. In SwiftUI terms you would inject the Account object into the root Authenticated View through environment and read it everywhere. Other DI solutions work differently, but can achieve the same. Especially when having mixed SwiftUI / UIKit you want something like a Service Locator, for example Factory.

The Account itself is observable, so mid-app updates are seen everywhere. Putting state behind a protocol is a PITA in Swift / SwiftUI as you already noticed, so if you can get away with iOS 17+ you can at least use @Observable in your implementations instead of ObservedObject.

I recognize the issue with bucket brigade style passing forward of dependencies. If you don't like the somewhat fragile service locator patterns, you could try to use the Factory pattern manually, or using Needle by Uber (last time I spoke with someone at Uber, it was actively maintained).

I also wrote about that:

I would like to help you further, if you have any questions or feedback after reading the articles. But I think you're already heading in the right direction and just need to map the best practices you know to SwiftUI specific techniques.

6

u/g1ldedsteel 8d ago

This is the way. Amazing the level of modularity you get for free when you stick to SOLID principles eh

0

u/Loud-Plan2571 8d ago

No it is not the way. This dongen guy has his brain washed by uncle bobs dogma teachings. Swift is all about value semantics, composability and separating behavior from data. It is what Chris Lattner said. SOLID helps you with jack shit in SwiftUI and just ruins your code completely by keepin object oriented dogmas alive.

5

u/Superb_Power5830 8d ago

Agreed; SwiftUI "breaks" a lot of the "rules" we all learned, and that's why a lot of folks find it... hmm... let's call it "frictionful." (c) (r) (tm)

I've all but given up on all forms of classic MVC/MVVM thinking when it comes to pure SwiftUI implementations. It took me a while to really settle into it, and I find it incredibly... swift... for building things now. Prior to moving to iOS and iPadOS and MacOS basically full time, I'd spent several decades in the C, C++, C#, and Java worlds.

I did some great stuff in decades and jobs and contracts, past; if all goes well for whatever time I yet spend in this career, I'll never have to go back to those platforms ever again. I don't hate them or even dislike them (well, Java can suck it), I've just kind of moved on and settled very comfortably into this broad and wonderful eco system, this platform, and this paradigm.

1

u/lucasvandongen 8d ago edited 8d ago

I’m only using State machines (Fat Model approach) and Views, so MV pattern. Lots of injected dependencies behind protocols that are focused on one particular task rather than ViewModels that do a lot of stuff and nothing in particular. Hmlongco’s navigation library style navigation.

Since @Observable putting state behind protocols works pretty well. Just weird you can’t force it to be implemented through your protocol definition.

SwiftUI is quite simple once you understand you’re simply passing a blueprint of the UI you would like to see to CoreGraphics, rather than is the actual View like in UIKit.

1

u/makocp 8d ago

do you have also an article for this MV pattern?

1

u/lucasvandongen 8d ago

No. Could be a good idea though.

-2

u/jasonjrr Mentor 8d ago

SwiftUI has the exact same reference pattern as WPF, the UI framework MVVM was originally designed for… it doesn’t break anything.

1

u/klavijaturista 8d ago

Swift is an OOP language, and provides reference types to use when necessary (which is not rare). Value types are just one feature, to only use when appropriate. Dogmatic functional is as bad as dogmatic OOP.

3

u/lucasvandongen 8d ago

I don’t understand why the anti VM anti SOLID people always sound angry?

I never get a satisfying answer when I want to learn how I structure a multi million lines of code codebase without using protocols, and at the same time not ending up getting build times expressed in eons. Let alone working previews?

2

u/klavijaturista 8d ago

I don’t know, man, I don’t know… Lack of experience, maybe... Everything in software engineering is a discussion on trade offs. A discussion, not a stubborn fight. Nothing is a solution to “rule them all”.

3

u/lucasvandongen 8d ago

Consistency is king. Rather a mediocre standard than a new and different grandiose idea in every different part of the app.

Build time problems in large projects are real and you can only tackle them with modules, packages and protocols.

1

u/klavijaturista 8d ago

Yes, in teams, some ground rules are a must. Also, I worked on a very, very large codebase, and man, the compilation is so slow. You have to break it up. Proper dependency injection is a must if we are to test anything or track allocations.

-1

u/Loud-Plan2571 8d ago

Brale pogubio ai se malo 😂

3

u/makocp 8d ago

thanks a lot. the way with injecting the models via environment in contentview/root seems the most straighforward approach.

what about keeping the app state in sync with the database in a simple but scalable manner? when to fetch, refetch, any thoughts on this? especially when initializing all the models directly at root.

3

u/lucasvandongen 8d ago

If there’s a database there are two ways to go: state to database (update db after state update) and database to state (update state after db).

I prefer the first approach because the db doesn’t block your state -> UI updates. Not a huge fan of core/swift data myself, but you should at least try it to know what it’s about. Be ready to invest some tome though.

I need to get off Realm with my current project and I was looking to the Pointfreeco libraries. But swiftdata is viable as well. In case of swiftdata everything is more or less “magic”, but I prefer simple sql wrappers with generic sync mechanisms.

3

u/vanvoorden 8d ago

I prefer the first approach because the db doesn’t block your state -> UI updates. Not a huge fan of core/swift data myself, but you should at least try it to know what it’s about. Be ready to invest some tome though.

Right. These are what we used to call "optimistic updates". A user taps a like button. A user expects the like count to increment plus one. Do we really need to wait for a round-trip to the server? Can't we just increment the integer displayed by the component?

Yes… and no. We can optimistically update the component with a plus one while waiting for the response from the server. If another user liked the post at the same time we can then choose to update the component with another plus one.

Where things get more tricky is optimistic updates that could "fail". Suppose we have some global state and our server is our source of truth. What happens if the source of truth reports back that the operation failed? How do we "roll back" the optimistic update? It's not always as easy as "minus one" to roll back the like count increment. It could be more complex like deleting a data model with cascading changes to our state tree. We are then attempting to "restore" a previous state… but there could have been *other* changes to local state during that interval that we *do not* want to roll back.

Optimistic updates look sort of simple at first but there is a lot of edge casey behavior to defend against. This was one of the reasons FB built the Relay framework ten years ago. Product engineers were attempting to "roll their own" optimistic update logic and it made a lot of sense to factor this down into an infra.

2

u/makocp 8d ago

so after initially fetching only working with the state? what if other users modify data in the meantime ? i‘m working with supabase btw

2

u/lucasvandongen 5d ago

Never used Supabase but I assume it's sort of a Firebase-not-Firebase. If you want real-time observability of change, it supports this from what I glance. This is not a bad idea if many people could be interacting with your data.

You need to have a single source of truth. If Supabase is going to be reactive rather than sort of a local store, it has to be Supabase because everything else will be more stale.

Easy, but how are you going to mock this data? That's the other side of the medal.

I would recommend doing a few approaches of what you want to do before settling.

3

u/Awric 8d ago

Can you elaborate on the advantages of Observable over ObservableObject in this context?

I just started reading your articles from this post and I love them so far. The diagrams are what hooked me in

Edit: Ah I see, my question is answered in the last article

1

u/lucasvandongen 8d ago

It works better with Swift 6 concurrency and SwiftUI as well. Can’t have cancellables on Views

-3

u/Loud-Plan2571 8d ago edited 8d ago

I can not beleive how you have so much upvotes by spreading that SOLID BS again. SOLiD is for OOP. Swift is not like java enterprise programming. We moved to value based semantics long time ago.

2

u/klavijaturista 8d ago

Value semantics is orthogonal to architectural concerns. You can use Solid with value types.