While learning more and more about TCA, I'm perplexed on a problem where my Button view disappears on every state update that's outside of the ShareLinkButtonView
. I figured the reason was due to the view recomputing/reinitializing every time an outside state updates and re-renders the entire view again.
The below code sends an action to Firebase to initialize and ultimately retrieve the ideaSnapshot
object to be used for later. The button view should only be visible/usable when the object has been retrieved.
How do I get the RootStore's state to stay the way it is, i.e. not be recomputed unnecessarily? I'm using @ObservableState and I thought that would resolve it, but I don't think it's doing what I think it is?
ShareLinkButtonView:
public struct ShareLinkButtonView: View {
@State private var showShareSheet: Bool = false
let store: StoreOf<RootStore> = Store(initialState: RootStore.State()) {
RootStore()
}
let eventId: String
public init(eventId: String) {
self.eventId = eventId
}
public var body: some View {
WithPerceptionTracking {
ZStack {
if let ideaSnapshot = store.ideaSnapshot {
Button {
Haptics.shared.play(.soft)
self.showShareSheet.toggle()
} label: {
Image.Custom("share-box-arrow")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 30, height: 30)
}
}
}
.onAppear {
store.send(.initializeSnapshot(eventId))
}
.sheet(isPresented: $showShareSheet) {
ShareSheet(activityItems: ["Hello world!"])
.presentationDetents([.fraction(0.65)])
}
}
}
}
RootStore
@Reducer
struct RootStore {
// MARK: Root state
@ObservableState
struct State {
var ideaSnapshot: IdeaSnapshotModel?
}
// MARK: Root action
enum Action {
case initializeSnapshot(String)
case getSnapshotByEventId(String)
case getSnapshotByEventIdResponse(IdeaSnapshotModel?)
case ideaSnapshot(IdeaSnapshotModel)
}
// MARK: Dependencies
@Dependency(\.firebaseClient) var firebaseClient
// MARK: Root reducer
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .ideaSnapshot:
return .none
case .initializeSnapshot(let eventId):
return handleInitializeSnapshot(state: &state, eventId)
case .getSnapshotByEventId(let eventId):
return handleGetSnapshotByEventId(state: &state, eventId)
case .getSnapshotByEventIdResponse(let ideaSnapshot):
return handleGetSnapshotByEventIdResponse(state: &state, ideaSnapshot)
}
}
}
// MARK: Action handlers
func handleInitializeSnapshot(state: inout State, _ eventId: String) -> Effect<Action> {
return .run { send in
let isSuccessful = try await self.firebaseClient.initializeIdeaSnapshot(eventId)
if isSuccessful {
// Retrieve idea snapshot
await send(.getSnapshotByEventId(eventId))
} else {
// TODO: Add retry logic here
}
}
}
func handleGetSnapshotByEventId(state: inout State, _ eventId: String) -> Effect<Action> {
return .run { send in
let ideaSnapshot = try await self.firebaseClient.getIdeaSnapshotByEventId(eventId)
await send(.getSnapshotByEventIdResponse(ideaSnapshot))
}
}
func handleGetSnapshotByEventIdResponse(state: inout State, _ ideaSnapshot: IdeaSnapshotModel?) -> Effect<Action> {
state.ideaSnapshot = ideaSnapshot
return .none
}
}