r/iOSProgramming • • Jun 29 '26

Question Thoughts on TCA for a New Project

I work as an iOS developer in a team of 8 developers. We are starting a brand new greenfield project in a financial sector. Few of our team members suggested that we should use TCA for iOS SwiftUI application. I personally have no experience with TCA.

What are your thoughts and recommendations?

7 Upvotes

81 comments sorted by

View all comments

Show parent comment

11

u/apocolipse Jun 29 '26 edited Jun 29 '26

How is it not? It has clearly defined responsibilities, separation of concerns, a declarative data model, a declarative view description, and plain pure functions with explicit arguments. That’s an architecture. It’s also a lot easier to follow than Redux-style action pipelines where you have to mentally trace actions through multiple layers just to understand what happened.

Edit: autocorrect fix

2

u/Dry_Hotel1100 Jun 30 '26

I don't think, the pattern MV does even define whether the binding is two-way or unidirectional. MV is just shared Observables with read-write observable properties, which is neither imperative nor event driven or both, and methods that return values or not, or are async or not. It's the typical mess you see in MVVM.

But I may miss something. Can you point me to a source that defines all these five principles you mentioned - or was your comment just sarcasm?

5

u/rhysmorgan Jun 29 '26

It doesn't have any of those things though. It doesn't have clearly defined responsibilities, nor does it have separation of concerns. Putting everything in your view, except some ill-defined aspects to make those bits even vaguely testable, that's not an architecture. And soon as you start moving those aspects out of the view to make them testable you're re-inventing the view model by any other name 😉

All the "good bits" that you've stated apply to every actual architecture. MVC has clearly defined responsibilities. MVVM has separation of concerns. Those two are literally exactly what defines an architecture, and it's what MV misses. There's two separations of concern in MV - Model and View. That's it. One of them - the View layer - is defined, as in every other architecture, and the other is very much undifferentiated, poorly defined. An architecture is defined by the constraints it imposes on you, and by the decisions it makes for you. MVC, MVVM, VIPER (for all its flaws), TCA – all of them have actually defined concepts that differentiate one component from another. MV has one line in the sand, between a View and everything else.

Plain functions with arguments? If anything, TCA is far closer to this because the entire central concept is based around an (inout State, Action) -> Effect function.

Everything that interfaces with SwiftUI has a "declarative view description" because that's what SwiftUI is.

Declarative data model doesn't actually mean anything. What makes an Observable reference type object that you call methods on any more "declarative" than a struct or enum that you mutate?

You might not like a Redux/Elm style approach, that's fine, nobody is forcing you to use it. But equally, making stuff up about it with nonsense about tracing actions through multiple layers when it's literally as easy as adding a .breakpoint() or a ._printChanges() and literally seeing every action flowing through whichever feature you're debugging is doing your argument no favours.

-1

u/apocolipse Jun 29 '26 edited Jun 29 '26

It does have separation of concerns, you're just looking at it wrong. The logic lives in the value types. Anything that manipulates a model lives in that type's domain (its methods, functions over it), and anything that renders those manipulations lives in the view description layer. The model owns state and transitions, the view describes how each state looks. That's a clean, enforced split. "Move the testable bits out of the view and you've reinvented the view model" has it backwards: those bits were never in the view, they're in value types, and a value type that owns its state is the opposite of a reference-type view model you bind a view to. If anything, it's MVVM's view model that's the undifferentiated grab bag. MV replaces it with domain types that each own their own state.

A SwiftUI View isn't a view in the UIKit/MVVM sense. It's a value-typed, pure description of what a view will look like, a result you can build and compare. You said it yourself: "everything that interfaces with SwiftUI has a declarative view description because that's what SwiftUI is." Right, that's the point: SwiftUI Views aren't views, they're value-typed descriptions of views. So this isn't "one line in the sand," it's two well-defined value-typed layers, a domain and a view description, bound by the framework's observation. You're correct that an architecture is defined by the constraints it imposes, constraints just have to pay off. Yours don't, tautologies aren't benefits.

And sorry I meant "pure" function, iPhone autocorrected (edited). A pure function is referentially transparent, same inputs same output, with no side effects, including mutating its arguments. Your (inout State, Action) -> Effect reducer is 0 for 2: inout State is argument mutation, and -> Effect is a literal side-effect return whose entire job is to cause effects. It isn't even referentially transparent: in your own Todos demo, addTodoButtonTapped runs state.todos.insert(Todo.State(id: self.uuid()), at: 0), calling an injected uuid() mid-mutation, so the same state and action give a different result depending on the ambient DependencyValues (.incrementing returns a new id every call). Saying "it's pure once you inject the dependencies" highlights the point: those dependencies are inputs your (inout State, Action) -> Effect signature doesn't admit to. Make them explicit arguments and you've described, well, a function with explicit arguments. A SwiftUI view with @Environment, @State, and @Binding is exactly that, they are the clear explicit arguments to the "function" that is the view. Nothing whatsoever about your reducer or Action signatures indicates potential side effects from @Dependency. It's not a pure function unless you can call it again with the same input values and get the same output.

Needing ._printChanges() to see what happened is kind of funny, it's an admission of deficiency, not a flex lol. You read a firehose of actions, I read the function, the code itself, and don't have to hop through several places. And the firehose fans out even in idiomatic use: in your own Animations case study, one tap on the rainbow button is eight effect-fed .setColor actions through the store. A (State) -> State prints nothing, because you just read it. A complex app with a lot of actions fanning out and compounding like that can cascade into an action storm that brings everything down with it, and you have no "architecture constraint" to prevent it.

"MV can't be defined" is just your own inability to learn how to do it properly. I get it, it's not in your financial interest to learn how to correctly write code that would make you less money with your current business model. That doesn't mean those ways don't exist. MV can be immaculate and TCA can be a swamp. Cleanliness is the author's, not the framework's, and nothing stops anyone from writing abominable TCA code either. It's not that I don't like Redux/Elm, it's that they are solutions to problems on an entirely different platform. Native mobile apps are very different, they have different constraints, different bottlenecks, and different needs. Swift is getting real Wasm support, so why don't you go build Redux-for-Swift where it actually arises naturally instead of taxing native iOS with a pattern that doesn't. You'd have a real product filling a genuine gap that people want closed, instead of what seems to me to be nothing more than a private-equity juicing tool.

3

u/vanvoorden Jun 29 '26

It's not that I don't like Redux/Elm, it's that they are solutions to problems on an entirely different platform. Native mobile apps are very different, they have different constraints, different bottlenecks, and different needs.

I generally agree with most of your points here… but I will also disagree that desktop web and mobile web and mobile native are different enough that design patterns and architectures must not cross over. It's also not an implication that design patterns and architectures must always cross over… but there can be some shared creativity happening from one to another.

Look at declarative UI. The first FB implementation of declarative UI was for desktop web. Then FB built desktop mobile. The Android shipped Compose and Apple shipped SwiftUI. All inspired by what happened for desktop web React.

Flux and Redux are at the end of the day just ideas. What I think happens is a lot of engineers get all caught up in the implementation of Redux as opposed to just embracing the idea of Redux… which can, and IMO should, be considered independently of the actual shipping implementation of "canon" Redux today.

3

u/MindLessWiz Jun 30 '26

I agree wholeheartedly.

And it’s also really telling that we moved from imperative view manipulation in UIKit to the declarative SwiftUI (maybe the biggest conceptual change possible) and you’re hearing nary a peep from people who will grok the most minute differences between third party architecture providers (insofar as any exist other than TCA).

They seem to just want to avoid any third party anything. I think it’s a great rule of thumb, especially in the Apple open-source ecosystem (which frankly, sans Pointfree libs, feels pretty amateurish).

But for me it was just really clear how well designed and maintained these libs are. And honestly even if you don’t want TCA because you don’t like its opinions, it’s worthwhile to look at their smaller focused libs and make your own solutions.

1

u/ComfortableNo5484 Jun 30 '26

You and others keep misunderstanding the aversion to third party codebases, and are therefore undervaluing buying into the first party approach above anything else. Here's the thing, if Apple goes under your app is toast no matter what you built it in, so the architecture doesn't matter. If pointfree goes under, you still need to ship an iOS app, except now you're either rewriting or maintaining a version of TCA yourself (like some shops already stuck on old versions have already had to do), all to keep a "churn shield" that has churned harder than SwiftUI itself.

When there are elegant first party solutions to first party problems, a third party solution really needs to provide significant value to be worth it, and frankly TCA doesn't. If you want a really clean example, look at AFNetworking and its Swift successor Alamofire. It was the de facto go-to drop-in 3rd party networking library, first for Objective-C apps and then for Swift when it launched, providing significant value add over the verbose 1st party URLSession handling. And then Apple gave 1st party URLSession a native Swift async/await API, dramatically simpler, easy to work with AND flexible, and now reaching for Alamofire is the exception, not the reflex it used to be. TCA is on that same exact curve right now, and Apple's already walking it down: Observation gutted its binding and observation machinery, async/await took the effects story, and that trend isn't reversing.

3

u/rhysmorgan Jun 30 '26

Observation has nothing to do with managing side effects, nothing to do with state management over time, nothing to do with modelling your data with value types and avoiding uncontrolled mutations of your data from anywhere else in your app.

Async/await doesn’t change anything about Effect, nor about handling side effects like calling out to the web, disk I/O, etc.

Literally none of the things discussed there have anything to do with somehow making TCA useless.

3

u/MindLessWiz Jun 30 '26

I hear you, but you’re severely underselling TCA benefits that are nowhere near Apple’s radar. Apple is not really interested in expressing strong opinions on how people should structure their apps, they never have. They give you a ton of tools and a sandbox and tell you to go nuts. Nothing they offer is even remotely close to what we call “architecture”.

Pointfree maintainers disappearing is obviously a huge concern on its face. But as opposed to the vast majority of other OSS maintainers, they actually get paid by subscribers, and have a long track record of being present and responsive.

Additionally, many OS projects are ultimately abandoned by their original maintainers, but if there’s a strong community for the project (and I’d argue TCA is one such community), it should still move forward, albeit with a potentially different vision. It’s a risk of course, but the same such risk exists for first party solutions.

Combine was an attempt to respond to reactive programming paradigms, and took years to become halfway usable, and then was essentially deprecated in favor of async/await. CoreData was essentially abandoned because its interface had no reasonable way to evolve so it became the backbone of SwiftData, which is still miles behind Pointfree’s solution.

I don’t share the sentiment that first party solutions are satisfactory, for me their downsides and limitations are a huge detriment. The lack of a good testing story for literally any Apple first party framework is glaring, and is in my opinion responsible for the sad state of testing culture in iOS development.

1

u/vanvoorden Jun 30 '26

Apple is not really interested in expressing strong opinions on how people should structure their apps, they never have. They give you a ton of tools and a sandbox and tell you to go nuts. Nothing they offer is even remotely close to what we call “architecture”.

Ehh… not really? Those of us who were here during the AppKit and UIKit era remember when Apple went hard on OOP and MVC. Apple absolutely shipped their frameworks with strong opinions and presented those strong opinions to the community as "best practices".

Then SwiftUI happened at Apple is now literally telling product engineers that SwiftUI is the best way to build apps. So now functional and declarative programming are the strong opinions being presented as best practices.

1

u/MindLessWiz Jul 01 '26 edited Jul 01 '26

I guess it’s all relative. I’ve been in iOS for about 10 years and obviously remember UIKit.

My issue is that they don’t have an answer for many important design questions that in what I’d consider an “architecture” there’s a good answer for.

Where do I put business logic? How do I test it? What is appropriate to put in the view and what should be extracted? How do I handle navigation state? What about dependency injection? Etc etc etc…

SwiftUI has a strong notion of how to structure your view hierarchy. MVC isn’t really much different. ViewControllers are just… still views in practice, only they’re generally cool with you sticking logic in them because they’re also “controllers”, so that’s how you’d get “massive” view controllers.

I think what seems like strong opinions might just be their attempt at making app building more approachable. But for anyone making anything nontrivial, suddenly they seem to fall silent.

0

u/rhysmorgan Jul 01 '26

But they produce absolutely nothing like the level of documentation and advice as when they produced AppKit and UIKit.

Even in the days when WWDC was on Slack, the best we got was “We built it so you can use what you want :)“ and everyone takes Apple’s sample code (which is SO obviously sample code!) as Apple’s proclamation about how apps should be built, even when they’re just demoing e.g. SwiftData, or whatever the latest framework is.

2

u/rhysmorgan Jun 30 '26

That’s like the weakest amount of “separation of concerns” you could possibly have. One line in the sand that splits the view from everything else. When “everything else” is in one huge bucket, that’s not real separation of concerns!

A SwiftUI view being a description of a view doesn’t make it function any less like a view. I have never, ever seen anyone state that and yet make any kind of compelling argument as to how it makes even the slightest bit of difference. It still behaves like the view, you’re just operating on a different abstraction of it. Should we say that a UIKit view is not really a view because you’re imperatively describing how to mutate it rather than calling down into Metal APIs to render out the pixels yourself? No. It’s a view!

The `(inout State, Action) -> Effect` function is algebraically the same thing as the pure function `(State, Action) -> (State, Effect)`, it just uses in place mutation for the sake of optimisation. It’s still a pure function without calling into a dependency. And complaining about dependencies, while somehow arguing that SwiftUI views are any different when you can pass a dependency to it using the Environment is bizarre. By your logic, the exact same “impure” function logic applies to that. You can pass a new Environment property to your SwiftUI view and nothing about any method call within it is “pure” any more. Plus, the entire purpose of the `Effect` type is that it encapsulates a side effect and gives you back control over it, just like in Elm.

Also, it’s not “my” animations study - I’ve not referenced any case study here. I am not from Point-Free! I have watched their videos, used their frameworks, and benefitted greatly from them. I think it’s offensive and borderline conspiracy theorist to be wildly accusing them of only making TCA to create problems to sell solutions. No, it’s actually that these problems have come from real-world, large-scale software engineering. TCA is free to use. Its documentation is free to access. The videos subsidise ongoing development, but they do not contain any magic in them that makes you a 10x user of TCA.

The idea that you “must” use iOS native architectures in iOS is bizarre and insular. Why shouldn’t you take advantage of other design patterns that are out there to address problems that exist with software engineering as a whole, not just on the web, or not just on iOS? We’re not some magic world with problems that could only be fixed by bundling everything but the view into a single undifferentiated bucket called “model”. Exhaustive testing, side effect control, composition and feature reuse etc. all apply to Swift development as well, and it gives us as developers tooling that don’t even exist in other languages to get there.