Update Screen Only When Visible on iOS

Practical step-by-step guide to updating the screen on iOS only when visible

Ruslan Dzhafarov
5 min readMay 26, 2024
Design by Salman Rahman (Behance)

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 a PausableObservable created with the pausableBufferedoperator, it will buffer any items emitted by the underlying source Observable until such time as you call its resume 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 emits false, 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 is true.

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 is true.

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! 😊👨‍💻👩‍💻

--

--

Ruslan Dzhafarov

Senior iOS Developer since 2013. Sharing expert insights, best practices, and practical solutions for common development challenges