Update Screen Only When Visible on iOS
Practical step-by-step guide to updating the screen on iOS only when visible
The Problem
In iOS development, presenting a new view controller from an existing parent view controller is a common practice. However, this can introduce challenges when the parent view controller needs to update its content while another view controller is being presented modally. If the parent view controller updates immediately while the modal is visible, the user may not see these updates, leading to a poor user experience and potential visual glitches. To ensure a seamless experience, it’s crucial to defer these updates until the parent view controller becomes visible again.
Consider a task list app as an example. When a user taps on a task row, a task details screen is presented. On this screen, the user can complete the task. Once completed, the task should be displayed at the bottom of the list. When the user returns to the task list, you want to animate moving the task to the bottom of the list. In this article we will explore how to achieve this behavior using RxSwift, ensuring updates occur only when the parent view controller is visible, providing a smooth and user-friendly experience.
The Solution
To handle this situation, we can use RxSwift's pausableBuffered
operator. This operator pauses the elements of the source observable sequence based on the latest element from a second observable sequence (the pauser). When the pauser emits true
, the source observable is paused. When it emits false
, the source observable resumes, and any buffered elements are emitted.
According to the ReactiveX documentation:
If you call the
pause
method of aPausableObservable
created with thepausableBuffered
operator, it will buffer any items emitted by the underlying source Observable until such time as you call itsresume
method, whereupon it will emit those buffered items and then continue to pass along any additional emitted items to its observers.
This is how it’s implemented in RxSwift:
public func pausableBuffered<Pauser>(
_ pauser: Pauser,
limit: Int? = 1,
flushOnCompleted: Bool = true,
flushOnError: Bool = true
) -> RxSwift.Observable<Self.Element> where Pauser : RxSwift.ObservableType, Pauser.Element == Bool
pauser: The observable sequence used to control pausing and resuming of the source observable sequence. When this emits
true
, the source sequence is paused. When it emitsfalse
, the source sequence resumes.limit: The maximum number of elements to buffer while the source sequence is paused. If
nil
, all elements are buffered without limit. The default is 1, which means only the last emitted element will be buffered.flushOnCompleted: If
true
, buffered elements are flushed (emitted) when the source observable completes. This ensures that any remaining buffered elements are delivered before the sequence ends. The default istrue
.flushOnError: If
true
, buffered elements are flushed when the source observable encounters an error. This ensures that any remaining buffered elements are delivered before the sequence errors out. The default istrue
.
In our example, we use the default limit, which is 1. This means that only the last emitted element will be buffered. This is particularly useful when you want to ensure that the most recent state is applied when the view becomes visible again, maintaining data consistency and providing a seamless user experience.
To implement the pauser, we can use the viewWillAppear
and viewDidDisappear
lifecycle methods of the parent view controller to manage the timing of updates.
Step 1: Add RxSwift, RxRelay and RxSwiftExt
First, ensure you have RxSwift, RxRelay and RxSwiftExt installed in your project. You can do this using CocoaPods, Carthage, or Swift Package Manager.
Step 2: Implement the View Controller
Create your view controller and set up the necessary properties, including a BehaviorRelay
to act as the pauser.
import UIKit
import RxSwift
import RxSwiftExt
import RxRelay
class ViewController: UIViewController {
private let pauseRelay = BehaviorRelay<Bool>(value: true)
private let disposeBag = DisposeBag()
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
pauseRelay.accept(false)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
pauseRelay.accept(true)
}
// Other properties and methods
}
Step 3: Implement the ViewModel
Define a ViewModel
class that contains a state observable. This observable will emit new data that needs to be reflected in the UI.
import UIKit
import RxSwift
class ViewModel {
struct State {
var title: String
var image: UIImage
}
let state: Observable<State>
// Other properties and methods
}
Step 3: Observe Updates
In your viewDidLoad
method, use the pausableBuffered
operator to manage the update flow. When updates are resumed, the last emitted value from the viewModel.state
observable will be applied, ensuring that the UI reflects the most recent state. This is critical for maintaining data consistency and user experience. This setup guarantees that the UI only updates when the view controller is visible, providing a seamless and glitch-free user experience.
class ViewController: UIViewController {
// Other properties and methods
private var viewModel: ViewModel!
override func viewDidLoad() {
super.viewDidLoad()
viewModel.state
.pausableBuffered(pauseRelay)
.observe(on: MainScheduler.instance)
.bind { [weak self] state in
self?.updateUI(with: state)
}
.disposed(by: disposeBag)
}
private func updateUI(with state: ViewModel.State) {
// Apply updates to your UI using the state object
// For example:
titleLabel.text = state.title
imageView.image = state.image
}
}
The viewDidLoad
method subscribes to the viewModel.state
observable using the pausableBuffered
operator. The pauseRelay
determines when the state updates should be paused or resumed. The observe(on: MainScheduler.instance)
ensures that the UI updates occur on the main thread, which is crucial for maintaining a responsive UI.
This setup guarantees that the UI only updates when the view controller is visible, providing a seamless and glitch-free user experience.
Conclusion
By leveraging RxSwift’s pausableBuffered
operator, you can easily manage deferred updates to your parent view controller. By following these steps, you can ensure that your iOS applications handle UI updates efficiently, maintaining responsiveness and a polished user experience. This technique is especially useful in scenarios where the user navigates between different view controllers, ensuring that updates are only applied when the relevant view controller is active and visible.
Thank you for reading until the end. If you have any questions, please feel free to write them in the comments.
If you enjoyed reading this article, please press the clap button 👏 . Your support encourages me to share more of my experiences and write more articles like this. Follow me here on medium for more updates!
Happy coding! 😊👨💻👩💻