Replies: 2 comments 3 replies
-
I was able to implement a workaround for now, but would love to know whether there is a better approach. One thing that occurred to me is it would be nice if a My solution// From somewhere in UIKit
let store = StoreOf<MyFeature>(
initialState: .init(),
reducer: MyFeature()
)
let rootView = MyFeatureView(store: store)
let host = MyHostingController(rootView: rootView)
self.navigationController?.pushViewController(host, animated: true)
// Subclass of UIHostingController
class MyHostingController<Content>: UIHostingController<Content> where Content: View {
// Deinit corresponds with when the view is no longer conceptually in the hierarchy
// rather than just being offscreen.
deinit {
if let tearDownableView = self.rootView as? any TearDownableView {
tearDownableView.tearDown()
}
}
}
// View protocol exposing a store capable of handling a `tearDown` action
public protocol TearDownableView: View {
associatedtype State
associatedtype Action: TearDownableAction
var store: Store<State, Action> { get }
}
// Action protocol requiring presence of a `tearDown` action
public protocol TearDownableAction: Equatable {
static var tearDown: Self { get }
}
// Convenience method for sending the `tearDown` action to the store of a `View` conforming to `TearDownableView`
public extension TearDownableView {
func tearDown() {
ViewStore(store.stateless).send(.tearDown)
}
}
//
public struct MyFeature: ReducerProtocol {
public struct State: Equatable {
public var hasAppeared = false
}
public enum Action: TearDownableAction {
case onAppear
case onNotificationReceived(Notification)
case tearDown
}
@Dependency(\.notificationCenterClient) var notificationCenterClient
public var body: some ReducerProtocol<State, Action> {
Reduce { state, action in
struct NotificationCenterClientId: Hashable {}
switch action {
case .onAppear:
guard !state.hasAppeared else { return .none }
state.hasAppeared = true
return Effect.run { send in
let notifications = self.notificationCenterClient.observe([.someEvent])
for await notification in notifications {
await sent(.onNotificationReceived(notification))
}
}.cancellable(id: NotificationCenterClientId())
case .onNotificationReceived(notification):
// Handle notification
return .none
case .tearDown:
return EffectTask.cancel(id: NotificationCenterClientId())
}
}
}
}
public struct MyFeatureView: TearDownableView {
public let store: StoreOf<MyFeature>
public init(store: StoreOf<MyFeature>) {
self.store = store
}
public var body: some View {
WithViewStore(self.store) { viewStore in
Text("My Feature View")
.onAppear { viewStore.send(.onAppear) }
}
}
} |
Beta Was this translation helpful? Give feedback.
-
Hi @rzulkoski, as you noticed, Luckily you can make one yourself quite easily: @available(iOS 15.0, *)
extension View {
func onFirstAppear(perform: @escaping () async -> Void) -> some View {
self.modifier(OnFirstAppear(perform: perform))
}
}
@available(iOS 15.0, *)
private struct OnFirstAppear: ViewModifier {
let perform: () async -> Void
@State private var didAppear = false
func body(content: Content) -> some View {
content.task {
if !self.didAppear {
self.didAppear = true
await self.perform()
}
}
}
} If you send an action from that view modifier it should work as you expect. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
I have a screen set up with a couple of clients with long living effects for observing notification center and network status. I originally hooked them up as part of the
.task
view modifier to directly tie it to the view's lifecycle. However, I noticed that when I switched tabs to a different view then the original view in question no longer appeared to be observing those events. When navigating back to the original view it was in it's original state prior to switching.I then decided to revert back to using the
.onAppear
view modifier while guarding against setting up observation more than once, but now I am realizing that nothing ever cancels those long living effects. This means that pushing and popping the screen multiple times will result in the old stores continuing to perform work (and in my case make network calls!) even though the view is long gone by then. I also can't tie it to.onDisappear
or I'll be in the same situation I was in with.task
.Do you have any guidance on the best strategy for tying the lifecycle of these long-living effects to when the view is intended to be part of the hierarchy but isn't actually on screen?
Edit:
It may also be worth mentioning that this store is not at the root of the application, and instead is using a
UIHostingController
that is then pushed onto aUINavigationController
within aUITabBarController
.Also using TCA version 0.43.0.
Beta Was this translation helpful? Give feedback.
All reactions