class ViewModel {
...
func state(with bindings: #escaping (Driver<State>) -> Signal<Event>) -> Driver<State> {
Driver.system(
initialState: .initial,
reduce: State.reduce(state:event:),
feedback:
bindings,
react(request: { $0.startLoading }, effects: { _ in
self.fetchFavoriteRepositoriesUseCase.execute()
.asObservable()
.observe(on: self.scheduler)
.map(self.repositoriesToRepositoryViewModelsMapper.map(input:))
.map { repositories in .loaded(repositories) }
.asSignal { error in
.just(.failed(error.localizedDescription))
}
}))
}
...
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let initialTrigger = BehaviorRelay<Void>(value: ())
let trigger = Observable.merge(initialTrigger.asObservable(), refreshRelay.asObservable())
let uiBindings: (Driver<FavoriteRepositoriesViewModel.State>) -> Signal<FavoriteRepositoriesViewModel.Event> = bind(self) { me, state in
let subscriptions = [
state.drive(onNext: { state in
switch state {
case .initial:
print("Initial")
case .loading:
print("Loading")
case .failed:
print("Failed")
case .loaded:
print("Loaded")
}
})
]
let events: [Signal<FavoriteRepositoriesViewModel.Event>] = [
trigger.map {
.load
}
.asSignal(onErrorSignalWith: .empty())
]
return Bindings(subscriptions: subscriptions, events: events)
}
viewModel.state(with: uiBindings)
.drive()
.disposed(by: disposeBag)
}
}
I'm trying to grasp my head around why the react method from RxFeedback does NOT create a memory leak in this case. It has the effects closure as one of its arguments which is an #escaping closure and I'm not weakifying it, but capturing self strongly in it to call the use case. I assume it has nothing to do with RxFeedback but my knowledge of ARC and memory management.
To test the deallocation of the ViewController I'm just popping it from a NavigationController.
I would appreciate a detailed explanation on why this code is NOT creating a retain cycle. Thanks in advance!
There is no retain cycle. However, your view controller is holding several references (both direct and indirect) to your view model.
So for example, your view controller has a viewModel property. It's also holding a disposeBag which is retaining a disposable, which retains an Observable that retains the closure in your view model, which retains the view model.
The only time the strong capture of self is an issue is if the disposable is also being retained by the same object that is being captured. In this case, the view model is "self" but the view controller is the one retaining the disposable (through its dispose bag.)
Related
In the documentation for assign it says the following...
The Subscribers/Assign instance created by this operator maintains a
strong reference to object, and sets it to nil when the upstream
publisher completes (either normally or with an error).
In the ViewModifier below the assign method in subscribeToKeyboardChanges() refers to self but self is a struct here so there's no way it can create a strong reference
Why doesn't the subscription in subscribeToKeyboardChanges() get immediately deallocated?
What is the actually happening here behind the scenes?
struct KeyboardHandler: ViewModifier {
#State private var keyboardHeight: CGFloat = 0
func body(content: Content) -> some View {
content
.padding(.bottom, self.keyboardHeight)
.animation(.default)
.onAppear(perform: subscribeToKeyboardChanges)
}
private let keyboardWillShow = NotificationCenter.default
.publisher(for: UIResponder.keyboardWillShowNotification)
.compactMap { $0.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect }
.map { $0.height }
private let keyboardWillHide = NotificationCenter.default
.publisher(for: UIResponder.keyboardWillHideNotification)
.map { _ in CGFloat.zero }
private func subscribeToKeyboardChanges() {
_ = Publishers.Merge(keyboardWillShow, keyboardWillHide)
.subscribe(on: DispatchQueue.main)
.assign(to: \.self.keyboardHeight, on: self)
}
}
I believe you`re referring to the wrong function description. Here is right one:
assign(to:on:)
Assigns a publisher’s output to a property of an object.
...
Return Value
An AnyCancellable instance. Call cancel() on this instance when you no
longer want the publisher to automatically assign the property.
Deinitializing this instance will also cancel automatic assignment.
So in your subscribeToKeyboardChanges example code it's expected that subscription will be canceled after function finishes. You have to keep strong reference to AnyCancellable return from assign to keep subscription in memory.
EDIT:
It appears that in this line assign copies self and holds it in the memory until cancel() call.
.assign(to: \.self.keyboardHeight, on: self)
Therefore View that uses KeyboardHandler view modifier will never be deallocated with subscription and will eventually bloat the memory during navigation. For example, here is the screenshot after 3 navigations see 3 instances of KeyboardHandler still in the memory.
I'm using MVVM + ReactiveCocoa.
My code works as expected. Except ViewModel object had stayed in 'Debug Memory Graph' when I removed ViewController from parent.
ViewController deinited, unlike ViewModel.
Here is how looks button action in ViewModel:
var changeStatus: Action<Book.Status, Void, NoError> {
return Action<Book.Status, Void, NoError> { status -> SignalProducer<Void, NoError> in
return SignalProducer<Void, NoError> { [weak self] observer, error in
if let strongSelf = self {
strongSelf.status.value = status
observer.sendCompleted()
}
}
}
}
Assigning action to button in ViewController:
reading.reactive.pressed = CocoaAction(viewModel.changeStatus, input: .reading)
reading button also stays in memory.
When I commented assigning action to button ViewModel successfully deinited. Thus, I came to the conclusion that this is the problem. Is it possible this line keeps strong reference?
Dear community, Is there a way to add button action with ReactiveCocoa so that the ViewModel object will be deleted on time?
Try this. By lazy defining your action, you make sure that its closure won't be retained.
lazy var changeStatus: Action<Book.Status, Void, NoError> = {
return Action<Book.Status, Void, NoError> { status -> SignalProducer<Void, NoError> in
return SignalProducer<Void, NoError> { [weak self] observer, error in
if let strongSelf = self {
strongSelf.status.value = status
observer.sendCompleted()
}
}
}
}()
What I have:
a NSManagedObject that sets a dynamic property to true when it's deleted from CoreData
override func prepareForDeletion() {
super.prepareForDeletion()
hasBeenDeleted = true
}
And within a view, I observe this NSManagedObject with the new Observe pattern of Swift 4
// I added this to observe the OBSERVED deletion to avoid a crash similar to:
// "User was deallocated while key value observers were still registered with it."
private var userDeletionObserver: NSKeyValueObservation?
private func observeUserDeletion() {
userDeletionObserver = user?.observe(\.hasBeenDeleted, changeHandler: { [weak self] (currentUser, _) in
if currentUser.hasBeenDeleted {
self?.removeUserObservers()
}
})
}
private func removeUserObservers() {
userDeletionObserver = nil
userObserver = nil
}
private var userObserver: NSKeyValueObservation?
private var user: CurrentUser? {
willSet {
// I remove all observers in willSet to also cover the case where we try to set user=nil, I think it's safer this way.
removeUserObservers()
}
didSet {
guard let user = user else { return }
// I start observing the NSManagedObject for Deletion
observeUserDeletion()
// I finally start observing the object property
userObserver = user.observe(\.settings, changeHandler: { [weak self] (currentUser, _) in
guard !currentUser.hasBeenDeleted else { return }
self?.updateUI()
})
}
}
So now, here come one observation and the question:
Observation: Even if I don't do the observeUserDeletion thing, the app seems to work and seems to be stable so maybe it's not necessary but as I had another crash related to the observe() pattern I try to be over careful.
Question details: Do I really need to care about the OBSERVED object becoming nil at any time while being observed or is the new Swift 4 observe pattern automatically removes the observers when the OBSERVED object is 'nilled'?
My view models are fundamentally flawed because those that use a driver will complete when an error is returned and resubscribing cannot be automated.
An example is my PickerViewModel, the interface of which is:
// MARK: Picker View Modelling
/**
Configures a picker view.
*/
public protocol PickerViewModelling {
/// The titles of the items to be displayed in the picker view.
var titles: Driver<[String]> { get }
/// The currently selected item.
var selectedItem: Driver<String?> { get }
/**
Allows for the fetching of the specific item at the given index.
- Parameter index: The index at which the desired item can be found.
- Returns: The item at the given index. `nil` if the index is invalid.
*/
func item(atIndex index: Int) -> String?
/**
To be called when the user selects an item.
- Parameter index: The index of the selected item.
*/
func selectItem(at index: Int)
}
An example of the Driver issue can be found within my CountryPickerViewModel:
init(client: APIClient, location: LocationService) {
selectedItem = selectedItemVariable.asDriver().map { $0?.name }
let isLoadingVariable = Variable(false)
let countryFetch = location.user
.startWith(nil)
.do(onNext: { _ in isLoadingVariable.value = true })
.flatMap { coordinate -> Observable<ItemsResponse<Country>> in
let url = try client.url(for: RootFetchEndpoint.countries(coordinate))
return Country.fetch(with: url, apiClient: client)
}
.do(onNext: { _ in isLoadingVariable.value = false },
onError: { _ in isLoadingVariable.value = false })
isEmpty = countryFetch.catchError { _ in countryFetch }.map { $0.items.count == 0 }.asDriver(onErrorJustReturn: true)
isLoading = isLoadingVariable.asDriver()
titles = countryFetch
.map { [weak self] response -> [String] in
guard let `self` = self else { return [] }
self.countries = response.items
return response.items.map { $0.name }
}
.asDriver(onErrorJustReturn: [])
}
}
The titles drive the UIPickerView, but when the countryFetch fails with an error, the subscription completes and the fetch cannot be retried manually.
If I attempt to catchError, it is unclear what observable I could return which could be retried later when the user has restored their internet connection.
Any justReturn error handling (asDriver(onErrorJustReturn:), catchError(justReturn:)) will obviously complete as soon as they return a value, and are useless for this issue.
I need to be able to attempt the fetch, fail, and then display a Retry button which will call refresh() on the view model and try again. How do I keep the subscription open?
If the answer requires a restructure of my view model because what I am trying to do is not possible or clean, I would be willing to hear the better solution.
Regarding ViewModel structuring when using RxSwift, during intensive work on a quite big project I've figured out 2 rules that help keeping solution scalable and maintainable:
Avoid any UI-related code in your viewModel. It includes RxCocoa extensions and drivers. ViewModel should focus specifically on business logic. Drivers are meant to be used to drive UI, so leave them for ViewControllers :)
Try to avoid Variables and Subjects if possible. AKA try to make everything "flowing". Function into function, into function and so on and, eventually, in UI. Of course, sometimes you need to convert non-rx events into rx ones (like user input) - for such situations subjects are OK. But be afraid of subjects overuse - otherwise your project will become hard to maintain and scale in no time.
Regarding your particular problem. So it is always a bit tricky when you want retry functionality. Here is a good discussion with RxSwift author on this topic.
First way. In your example, you setup your observables on init, I also like to do so. In this case, you need to accept the fact that you DO NOT expect a sequence that can fail because of error. You DO expect sequence that can emit either result-with-titles or result-with-error. For this, in RxSwift we have .materialize() combinator.
In ViewModel:
// in init
titles = _reloadTitlesSubject.asObservable() // _reloadTitlesSubject is a BehaviorSubject<Void>
.flatMap { _ in
return countryFetch
.map { [weak self] response -> [String] in
guard let `self` = self else { return [] }
self.countries = response.items
return response.items.map { $0.name }
}
.materialize() // it IS important to be inside flatMap
}
// outside init
func reloadTitles() {
_reloadTitlesSubject.onNext(())
}
In ViewController:
viewModel.titles
.asDriver(onErrorDriveWith: .empty())
.drive(onNext: [weak self] { titlesEvent in
if let titles = titlesEvent.element {
// update UI with
}
else if let error = titlesEvent.error {
// handle error
}
})
.disposed(by: bag)
retryButton.rx.tap.asDriver()
.drive(onNext: { [weak self] in
self?.viewModel.reloadTitles()
})
.disposed(by: bag)
Second way is basically what CloackedEddy suggests in his answer. But can be simplified even more to avoid Variables. In this approach you should NOT setup your observable sequence in viewModel's init, but rather return it anew each time:
// in ViewController
yourButton.rx.tap.asDriver()
.startWith(())
.flatMap { [weak self] _ in
guard let `self` = self else { return .empty() }
return self.viewModel.fetchRequest()
.asDriver(onErrorRecover: { error -> Driver<[String]> in
// Handle error.
return .empty()
})
}
.drive(onNext: { [weak self] in
// update UI
})
.disposed(by: disposeBag)
I would shift some responsibilities to the view controller.
One approach would be to have the view model produce an Observable which as a side effect updates the view model properties. In the following code example, the view controller remains in charge of the view bindings, as well as triggering the refresh in viewDidLoad() and via a button tap.
class ViewModel {
let results: Variable<[String]> = Variable([])
let lastFetchError: Variable<Error?> = Variable(nil)
func fetchRequest() -> Observable<[String]> {
return yourNetworkRequest
.do(onNext: { self.results.value = $0 },
onError: { self.lastFetchError.value = $0 })
}
}
class ViewController: UIViewController {
let viewModel = ViewModel()
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
viewModel.results
.asDriver()
.drive(onNext: { yourLabel.text = $0 /* .reduce(...) */ })
.disposed(by: disposeBag)
viewModel.lastFetchError
.asDriver()
.drive(onNext: { yourButton.isHidden = $0 == nil })
.disposed(by: disposeBag)
yourButton.rx.tap
.subscribe(onNext: { [weak self] in
self?.refresh()
})
.disposed(by: disposeBag)
// initial attempt
refresh()
}
func refresh() {
// trigger the request
viewModel.fetchRequest()
.subscribe()
.disposed(by: disposeBag)
}
}
All answers are good, but i want to mentioned about CleanArchitectureRxSwift. This framework really help me to find the way how rx can be applied to my code. The part about "backend" mobile programming (request, parsers, etc) can be omitted, but work with viewModel/viewController has really interesting things.
I'm toying around with a small Swift application. In it the user can create as many MainWindow instances as he wants by clicking on "New" in the application's menu.
The application delegate holds an array typed to MainWindowController. The windows are watched for the NSWindowWillCloseNotification in order to remove the controller from the MainWindowController array.
The question now is, if the removal of the observer is done correctly – I fear there might be a cyclic reference to observer, but I don't know how to test for that:
class ApplicationDelegate: NSObject, NSApplicationDelegate {
private let notificationCenter = NSNotificationCenter.defaultCenter()
private var mainWindowControllers = [MainWindowController]()
func newWindow() {
let mainWindowController = MainWindowController()
let window = mainWindowController.window
var observer: AnyObject?
observer = notificationCenter.addObserverForName(NSWindowWillCloseNotification,
object: window,
queue: nil) { (_) in
// remove the controller from self.mainWindowControllers
self.mainWindowControllers = self.mainWindowControllers.filter() {
$0 !== mainWindowController
}
// remove the observer for this mainWindowController.window
self.notificationCenter.removeObserver(observer!)
}
mainWindowControllers.append(mainWindowController)
}
}
In general you should always specify that self is unowned in blocks registered with NSNotificationCenter. This will keep the block from having a strong reference to self. You would do this with a capture list in front of the parameter list of your closure:
{ [unowned self] (_) in
// Block content
}