-
I'm currently working on a stock market iPad/iPhone app. I mention stock market specifically because I feel like I have a problem that I just can't seem to wrap my head around. I'm new to Composable Architecture, but not necessarily new to SwiftUI. I've been thinking about this for days, and I'm at the point where I think I may actually be overthinking it. The issue is that I have 2 recurring API calls that fetch current market hours as well as a list of stock quotes. Currently, I use the API as a service dependency, and in my Root level state, I have a couple actions with a timer that simply fetch market data (hours and quotes) every 3 seconds. From the root level, I think store all this information in an array of quotes and market hours in the RootState. Because it is also an iPad app, the way the layout looks is that both the Detail sidebar and the Content views show quote information, much like the Apple Stock app (think left table view has list of users Watchlist with stock information updating, and the Content view shows the stock details, along with the quote information + much more). Even when a user selects a Stock in the Detail view, the Detail view remains active and hence the remain stock quotes should also be updated for the stocks the user hasn't selected. So for today, I simply use the Shared State example in the Case-Studies to pass down this quote information at every child view of the root view that requires it. Basically using computed variables for child states, and copying the quotes and market hours down the child states that need them. This seems like a ton of boilerplate, and as my app grows and this basic level stock quote information is needed in more and more places, its seems incredible redundant to use computed variables in this way. Is there a better way? I've seen some suggestions of using Environment to share state throughout multiple parts of the app, but I haven't found any good examples of this. The other problem that I have now run into, is that because these quotes are being fetched more frequently, SwiftUI is updating many many views in the hierarchy each time quotes are updated. I'm playing around with "observe" and trying to get everything scoped correctly, but I haven't quite figured out the right balance yet. I may just be overthinking this, or maybe I haven't designed my various states correctly just yet (as I'm new to Composable Architecture), but any pointers you may be able to share would be greatly appreciated. |
Beta Was this translation helpful? Give feedback.
Replies: 4 comments 14 replies
-
Here's a pattern I often use to coordinate child domains with shared state without having to pass it down every level. With Just an example of the idea, not at all tested. /// This client relays data from root to any child
struct FeatureClient {
var updateValue: (Value) await -> Void
var observeValue: () -> AsyncStream<Value>
}
struct RootFeature: ReducerProtocol {
struct State {
}
enum Action {
case task
}
@Dependency(\.stocksClient) var stocksClient
@Dependency(\.featureClient) var featureClient
func reduce() -> Effect {
switch action {
case .task:
return .run { _ in
// observe stocks and relay to FeatureClient
for await value in stocksClient.observeStocks() {
await self.featureClient.updateValue(value)
}
}
}
}
}
struct ChildFeature: ReducerProtocol {
struct State {
var value: Value?
}
enum Action {
case task
case valueResponse(Value)
}
@Dependency(\.featureClient) var featureClient
func reduce() -> Effect {
switch action {
case .valueResponse(let value):
state.value = value
return .none
case .task:
return .run { send in
// Child observes shared state
for await value in featureClient.observeValue() {
await send(.valueResponse(value))
}
}
}
}
} |
Beta Was this translation helpful? Give feedback.
-
That is really clever. I'm curious if this would still be accepted within the bounds of the Composable Architecture. I wouldn't see why not given that the environment now holds the single source of truth, but I'm curious if there is an opinion on this. It seems to even have the added benefit that the child who observes could transform the quote or market information into their own state instead of having to take everything. I will definitely try this out. Appreciate you taking the time to respond. |
Beta Was this translation helpful? Give feedback.
-
Hmm, this is interesting. I've been thinking through a similar problem but with the added condition that the remote state is mutable from API calls made by the app. Continuing with the stocks example, say you could create bundles of stocks and give the bundle a name. And then you could perform basic CRUD like list all your bundles, augment them, delete them, change the name etc. So some small child feature might let you change the name of you bundle of stocks. That change would be made through an API call in a small feature client. But then you'd want the global-level environment client to "pull" down the list of stock bundles so that anywhere in the app you view stock bundles the name change would be reflected. Is there some way expand @rcarver 's solution to work this way? Like maybe the global client environment acts as a repository and all feature clients are kind of carved off of this bigger client? And how to make the global client know that a feature client has made an API call that changes the remote state? |
Beta Was this translation helpful? Give feedback.
-
Just to give everyone a quick update, based on @rcarver's answer I was able to get something working by doing the following. First I created a
Then I setup my dependencies:
Then in my RootState reducer:
Not shown here but in my reducer I also have a case which updates the I've then been able to do this through multiple layers of subviews and it has worked very nicely. I still need to figure out when/how I should cancel my subscriptions, but I think that is relatively minor. Thanks @rcarver for the suggestion, so far this works pretty well. Hopefully others find this useful as well. |
Beta Was this translation helpful? Give feedback.
Here's a pattern I often use to coordinate child domains with shared state without having to pass it down every level. With
@Dependency
, creating and sharing clients is lightweight so I'll just make a client supporting a given feature.Just an example of the idea, not at all tested.