Rendering child views without using ForEachStore #985
-
Hi, Recently we face an issue where we want to use a UIKit based paging library to display a list, however its API doesn't fit well into TCA world. The library puts each child view inside a public init(
currentPage: Binding<Int>,
navigationOrientation: UIPageViewController.NavigationOrientation = .horizontal,
transitionStyle: UIPageViewController.TransitionStyle = .scroll,
bounce: Bool = true,
wrap: Bool = false,
hasControl: Bool = true,
control: UIPageControl? = nil,
controlAlignment: Alignment = .bottom,
@PagesBuilder pages: () -> [AnyView]
) { ... } The To fix this, I kind of work around it by manually looping through child elements after reading struct GalleryState: Equatable {
var paintings: IdentifiedArrayOf<Painting>
}
enum GalleryAction: Equatable {
case painting(id: Painting.ID, action: PaintingAction)
}
struct GalleryEnvironment {}
let galleryReducer = Reducer<GalleryState, GalleryAction, GalleryEnvironment> { _,_,_ in
.none
}
struct GalleryView: View {
// @State var index: Int = 0
var store: Store<GalleryState, GalleryAction>
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text("My Art Collection")
.font(.system(size: 40, weight: .bold))
.padding([.horizontal, .top])
// Doesn't work using TCA's ForEachStore
Pages {
AnyView(ForEachStore(store.scope(state: \.paintings, action: GalleryAction.painting(id:action:))) {
PaintingView(store: $0)
})
}
// Loop through store's elements manually; works
Pages {
let scopedStore = store
.scope(state: \.paintings, action: GalleryAction.painting(id:action:))
let idsViewStore = ViewStore(
scopedStore.scope(state: { $0.ids })
)
Array(idsViewStore.state).map { id -> AnyView in
var element = ViewStore(scopedStore).state[id: id]!
return AnyView(PaintingView(store: scopedStore.scope(state: {
element = $0[id: id] ?? element
return element
}, action: {
(id, $0)
})))
}
}
Spacer()
}
}
}
enum PaintingAction: Equatable {}
struct PaintingEnvironment {}
let paintingReducer = Reducer<Painting, PaintingAction, PaintingEnvironment> { _,_,_ in
.none
}
private struct PaintingView: View {
var store: Store<Painting, PaintingAction>
var body: some View {
WithViewStore(store) { viewStore in
VStack {
Image(viewStore.image)
.resizable()
.scaledToFit()
VStack(alignment: .leading) {
Text(viewStore.title)
.font(.system(size: 30, weight: .bold))
Text(viewStore.author)
.foregroundColor(.secondary)
Text(viewStore.about)
.multilineTextAlignment(.leading)
.fixedSize(horizontal: false, vertical: true)
.padding(.top)
}
Button(action: {}) {
HStack {
Spacer()
Text("Buy for $\(viewStore.price)")
.fontWeight(.semibold)
.foregroundColor(.white)
.padding(.vertical)
Spacer()
}
.background(Color.blue)
.cornerRadius(10)
}
.padding(.top, 30)
Spacer()
}
.padding(.horizontal)
}
}
} Below is the screen recording of the preview app behaviour. Screen.Recording.2022-01-26.at.5.30.52.PM.mp4What I'd like to ask is whether what I did is correct or if there's a better way to consume a UI library that expects an array of child views in TCA e.g. @PagesBuilder pages: () -> [AnyView] Thanks, |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 1 reply
-
Hey @josephktcheung! I think your solution is totally reasonable! We're not aware of You are now aware of some of the messiness that lives inside Thanks for sharing your solution! |
Beta Was this translation helpful? Give feedback.
Hey @josephktcheung!
I think your solution is totally reasonable! We're not aware of
[AnyView]
being a widespread pattern in library design, but without a@ViewBuilder
interface your workaround would seem to be necessary here.You are now aware of some of the messiness that lives inside
ForEachStore
(like the internal caching of state, and force unwraps, which isn't great). Those workarounds are specifically there to account for behavior/bugs in SwiftUI'sForEach
view, though, so you may find they're not necessary.Thanks for sharing your solution!