Comments (7)
My raw reimplementation of binding works perfectly instead of SwiftUI one's
struct XBinding<Value> {
var wrappedValue: Value {
get { return getValue() }
nonmutating set { setValue(newValue) }
}
private let getValue: () -> Value
private let setValue: (Value) -> Void
init(getValue: @escaping () -> Value, setValue: @escaping (Value) -> Void) {
self.getValue = getValue
self.setValue = setValue
}
var projectedValue: Self { self }
}
This means that somewhere under the hood SwiftUI binding retains the value twice
from clean-architecture-swiftui.
@State var loadable: Loadable<Int> = .notRequested
var body: some View {
VStack {
Button("action") {
let cancelBag: CancelBag = .init()
let binding = Binding(get: {
return loadable // captures the cancelBag
}, set: { value in
loadable = value
})
binding.wrappedValue.setIsLoading(cancelBag: cancelBag)
print(binding.wrappedValue)
let publisher = Just<Int>(1).delay(for: .seconds(5), scheduler: RunLoop.main)
publisher
.handleEvents(receiveCancel: {
print("canceled")
})
.sink(receiveValue: { [binding] value in
print("value received")
print(binding)
$loadable2.wrappedValue = .loaded(value)
})
.store(in: cancelBag)
}
Button("cancel") {
loadable = .isLoading(last: nil, cancelBag: .init())
}
}
}
If u press button action several times it would not cancel previous requests
from clean-architecture-swiftui.
Hey! I believe if you remove the binding from your sample code, the reference cycle should go away. It doesn't seem to be used for any state management in your code, just store the cancelBag in the view's loadable
variable.
Oh, I see you're referring to a similar structure in the codebase of the project. Thanks for the heads up, this might actually be an issue. I'll have a look when I get time!
from clean-architecture-swiftui.
@nalexn Thanks for your response!
It is an initial way how we update the interactor so binding is required. I've tried to hardcode the binging's get
to .notRequsted
and the problem goes away. But in this case, there we have a limitation for the getter, and pay attention to reading a value from it in interactors.
from clean-architecture-swiftui.
I have found a similar problem while writing directly to appState. The root cause differs, so even could suggest a fix for it.
So, the problem is following lines of code
extension Store {
subscript<T>(keyPath: WritableKeyPath<Output, T>) -> T where T: Equatable {
get { value[keyPath: keyPath] }
set {
var value = self.value
if value[keyPath: keyPath] != newValue {
value[keyPath: keyPath] = newValue
self.value = value
}
}
}
Imagine u have some loadable in appState and it equals to isLoading(_, _)
. If u will try to call appState[\.someloadable].setIsLoading(CancelBag())
it would do nothing. The If
operator in a subscript above will not pass value to change cause Loadable
's Eqautable
implementation ignores it
extension Loadable: Equatable where T: Equatable {
public static func == (lhs: Loadable<T>, rhs: Loadable<T>) -> Bool {
let result: Bool
switch (lhs, rhs) {
case (.notRequested, .notRequested):
result = true
case (.isLoading(let lhsV, _), .isLoading(let rhsV, _)):
result = lhsV == rhsV // the problem is here
case (.loaded(let lhsV), .loaded(let rhsV)):
result = lhsV == rhsV
case (.failed(let lhsE), .failed(let rhsE)):
result = lhsE.localizedDescription == rhsE.localizedDescription
default:
result = false
}
return result
}
}
if the last value is equal, the old operation will never be canceled. Cause isLoading(last, oldCancelBag) == isLoading(last, newCancelBag)
return true.
A suggestion is to compare the cancelBag as well in Equitable implementation by reference (===). Comparing by usual equal == will not help.
However, this approach will brake the tests, cause it XCTAssertsEqual will check for cancelBag as well. But this solutions could be solved by custom asserts
from clean-architecture-swiftui.
Hi @nalexn!
Any updates?
After a few days of brainstorming, I've found only some workarounds, but not a proper fix. So, maybe you have some ideas, cause this is going to be a significant issue.
from clean-architecture-swiftui.
Hey - sorry for a slow response. Today I reviewed the existing code, so here are my thoughts.
First of, I think your last suggestion for comparing the cancelbags in Loadable: Equatable
is valid, I'll update the code. Some tests fail indeed.
Now about the original concern about cancelBag being retained. In RxSwift we're relying on the disposeBag's deallocation for canceling the subscriptions, so retaining it anywhere except in the owning object is indeed a problem leading to subscription leaking.
A peculiarity of SwiftUI is that almost everything is a struct, so you cannot use the same approach as with RxSwift due to the absence of "owning objects" limiting the lifetime of the subscriptions.
CancelBags
are built differently than DisposeBags
. They do cancel subscriptions upon deallocation, but they also offer the method cancel()
, allowing you to release subscriptions while the bag is still retained.
Loadable enum also offers cancelLoading
helper method that calls cancel()
if it currently holds a cancelBag.
So, to the solution: if you have an event in your app when you want to stop all running requests (for example, when the user logs out) - you can call cancelLoading
on all Loadables you have in the AppState. Alternatively, just reset the AppState to some default state, so all retained cancelBags should get deallocated (except if you still show a view that retains cancelbag through binging - then manual cancelLoading
should kill the requests for sure)
from clean-architecture-swiftui.
Related Issues (20)
- Typo: TextField spelling HOT 1
- Cannot connect to REST API HOT 4
- How to save related CoreData objects? HOT 1
- Something wrong with @Builder and @resultBuilder !!! HOT 1
- How to get value from LoadableSubject? HOT 1
- Why does the '.onReceive(routingUpdate)' perform twice? HOT 1
- Project does not build in Xcode 13.2; Internal error: missingPackageDescriptionModule HOT 2
- Question: why `case .isLoading` is not called even we got `.isLoading` on didSet? HOT 1
- Find by AccessibilityIdentifier HOT 3
- Reseting route state HOT 5
- app crash after select some countires HOT 1
- Would be cool to have the same using SwiftUI Lifecycle
- safearea is not correct HOT 2
- How do I add pagination?
- Question regarding Value: Decodable and Closures
- How do i add interceptor? HOT 1
- Few questions
- Decouple `Respository` logic HOT 1
- DIContainer conforming EnvironmentKey - can't find its usage HOT 2
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from clean-architecture-swiftui.