RxSwift concurrency problems with coordinator - ios

What I want to do:
Present VC1
When VC1 is dismissed, present VC2
Problem:
When VC1 is dismissed, VC2 does not present
Dirty Fix:
Put milisecond delay. It fixes the problem, but want to know why it happens
Explanation: I get viewDidDissapear event when VC1 dismisses so I can present VC2
If you need more details, please ask.
Code:
class ViewModel {
let coordinator = Coordinator()
struct Input {
let itemSelected: Driver<IndexPath>
}
struct Output {
let presentVC1: Driver<Void>
let presentVC2: Driver<Void>
}
func transform(input: Input) -> Output {
let navigateToVC1 = input.itemSelected
.flatMap { [coordinator] in
return coordinator.transition(to: Scene.VC1)
}
let navigateToVC2 = navigateToVC1
.delay(.milliseconds(1))
.flatMap { [coordinator] in
return coordinator.transition(to: Scene.VC2)
}
return Output(presentVC1: presentVC1, presentVC2: presentVC2)
}
Coordinator code:
func transition(to scene: TargetScene) -> Driver<Void> {
let subject = PublishSubject<Void>()
switch scene.transition {
case let .present(viewController):
_ = viewController.rx
.sentMessage(#selector(UIViewController.viewDidDisappear(_:)))
.map { _ in }
.bind(to:subject)
currentViewController.present(viewController, animated: true)
return subject
.take(1)
.asDriverOnErrorJustComplete()
}

The viewDidDisappear method is called before the view controller is fully dismissed. You should not try to present the second view controller until the callback of dismiss is called.
Wherever you are dismissing your view controller, use the below instead and don't present the next view controller until after the observable emits a next event.
extension Reactive where Base: UIViewController {
func dismiss(animated: Bool) -> Observable<Void> {
Observable.create { [base] observer in
base.dismiss(animated: animated) {
observer.onNext(())
observer.onCompleted()
}
return Disposables.create()
}
}
}
I suggest you consider using my Cause-Logic-Effect architecture which contains everything you need to properly handle view controller presentation and dismissal.
https://github.com/danielt1263/CLE-Architecture-Tools
A portion of the interface is below:
/**
Presents a scene onto the top view controller of the presentation stack. The scene will be dismissed when either the action observable completes/errors or is disposed.
- Parameters:
- animated: Pass `true` to animate the presentation; otherwise, pass `false`.
- sourceView: If the scene will be presented in a popover controller, this is the view that will serve as the focus.
- scene: A factory function for creating the Scene.
- Returns: The Scene's output action `Observable`.
*/
func presentScene<Action>(animated: Bool, overSourceView sourceView: UIView? = nil, scene: #escaping () -> Scene<Action>) -> Observable<Action>
extension NSObjectProtocol where Self : UIViewController {
/**
Create a scene from an already existing view controller.
- Parameter connect: A function describing how the view controller should be connected and returning an Observable that emits any data the scene needs to communicate to its parent.
- Returns: A Scene containing the view controller and return value of the connect function.
Example:
`let exampleScene = ExampleViewController().scene { $0.connect() }`
*/
func scene<Action>(_ connect: (Self) -> Observable<Action>) -> Scene<Action>
}
struct Scene<Action> {
let controller: UIViewController
let action: Observable<Action>
}
The connect function is your view model, when its Observable completes or its subscriber disposes, the view controller will automatically dismiss.
The presentScene function is your coordinator. It handles the actual presenting and dismissing of scenes. When you dismiss and present a new scene, it will properly handle waiting until the previous view controller is dismissed before presenting the next one.

Related

present a ViewController from a Swift class derived from an NSObject?

This project was written in Objective C and a bridging header and Swift files were added so they can be used at run time. When the app starts, Initializer called in Mock API client is printed in the debugger. Is it possible to present a ViewController from the initializer?
Xcode Error:
Value of type 'MockApiClient' has no member 'present'
//MockApiclient.Swift
import Foundation
class MockApiClient: NSObject
{
override init ()
{
print("Initializer called in Mock API client")
if isLevelOneCompleted == false
{
print("It's false")
let yourVC = ViewController()
self.present(yourVC, animated: true, completion: nil)
} else
{
print("It's true")
}
}
var isLevelOneCompleted = false
#objc func executeRequest()
{
print("The execute request has been called")
isLevelOneCompleted = true
if isLevelOneCompleted {
print("It's true")
} else {
//do this
}
}
}
Update - ViewController.m
// prints "The execute request has been called" from the debugger window
- (void)viewDidLoad {
[super viewDidLoad];
MockApiClient *client = [MockApiClient new];
[client executeRequest];
}
You can't call present(_:animated:completion) because it is a method of UIViewController, not NSObject.
Why not pass a viewController reference to the MockApiClient to present on instead like so. Be sure to check Leaks or Allocations on instruments to avoid the client retaining the controller.
class MockApiClient: NSObject {
var referencedViewController: UIViewController?
override init() {
let presentableViewController = ViewController()
referencedViewController.present(presentableViewController, animated: true, completion: nil)
}
deinit {
referencedViewController = nil
}
}
let apiClient = MockApiClient()
apiClient.referencedViewController = // The view controller you want to present on
Assuming you're using UIKit, you'll have to present the view controller from the nearest available attached view controller. If you know for certain that no other view controllers would currently be presented then you can safely present from the root view controller:
UIApplication.shared.keyWindow?.rootViewController?.present(someViewController, animated: true, completion: nil)
This concept of attached and unattached/detached view controllers is never officially explained but the infamous UIKit warning of presenting view controllers on detached view controllers is real. And the workaround is finding the nearest available attached view controller, which at first (when nothing is currently being presented) is the root view controller (of the window). To then present an additional view controller (while one is currently being presented), you'd have to present from that presented view controller or its nearest parent view controller if it has children (i.e. if you presented a navigation view controller).
If you subclass UIViewController, you can add this functionality into it to make life easier:
class CustomViewController: UIViewController {
var nearestAvailablePresenter: UIViewController {
if appDelegate.rootViewController.presentedViewController == nil {
return appDelegate.rootViewController
} else if let parent = parent {
return parent
} else {
return self
}
}
}
Then when you wish to present, you can simply do it through this computed property:
nearestAvailablePresenter.present(someViewController, animated: true, completion: nil)

RxSwift | Initialize and push UIViewController in closure

For example we have three UIViewControllers:
A, B, C.
We pushing B from A.
In B we calling some API:
func getProduct(productNumber: String) {
someService.rxGetProduct(productNumber: productNumber)
.asObservable()
.trackActivity(loading)
.subscribe(onNext: { [weak self] product in
guard let `self` = self else { return }
let cViewModel: CViewModel = .init()
let cViewController: CViewController = .init(viewModel: cViewModel)
self.navigationController?.pushViewController(cViewController, animated: true)
}, onError: { [weak self] error in
// error handling
}).disposed(by: disposeBag)
}
In method above, we are getting some product model and pushing C view controller in closure.
The problem is, when we are popping back from C view controller to B view controller – C view controller is not deinitializing. C view controller is deinitializing when we are popping back from B view controller to A view controller.
What I am doing wrong?
You might be creating the retain cycle within trackactivity or whatever loading is
consider using take(1) and asSingle() in case your observable is not intended to complete.

How to create a UITabBarController and connect its tabs with coordinators?

I've recently been inspired to learn MVVM-C (C for Coordinators) and I'm now trying to rewrite my current project to use it, but I'm struggling to figure out how to create a tab bar controller, where each tab has it's own coordinator.
I have a LoginCoordinator that, once the user logs in, creates a TabCoordinator that should create 3 coordinators, each coordinating a tab.
The most obvious solution would be to have the coordinator's start() functions to return the view controllers that they create, so I guess it would look kinda like this:
class TabCoordinator {
func start() {
let fooCoordinator = FooCoordinator(...)
let fooVC = fooCoordinator.start()
// create other coordinators/VCs
tabBarController.viewControllers = [fooVC, ...]
}
}
class FooCoordinator {
func start() -> UIViewController {
let fooVC = FooViewController(...)
// ...
return fooVC
}
}
I'm not sure if I'm on the right track or if there's a better way, but having the start() functions return something feels wrong somehow...
I feel that it is usually the job of the parent coordinator to present a child coordinator's controller so my start() function is func start(presentationHander: #escaping (UIViewController) -> ()). The start() function for your TabCoordinator would look something like this:
class TabCoordinator : Coordinator {
func start(presentationHander: #escaping (UIViewController) -> ()) {
let tabBarController = UITabBarController()
var tabBarControllers = [UIViewController]()
let fooCoordinator = FooCoordinator()
fooCoordinator.start { (controller) in
tabBarControllers.append(controller)
}
tabBarController.viewControllers = tabBarControllers
presentationHander(tabBarController)
}
}
class FooCoordinator : Coordinator {
func start(presentationHander: #escaping (UIViewController) -> ()) {
let fooVC = FooViewController.makeFromStoryboard()
presentationHander(settingVC)
}
}
FooCoordinator is responsible for initializing its own dependencies but allows TabCoordinator to present its controller.
I find this gives me more flexibility when reusing controllers. The parent coordinator can choose to present as modal or push the controller onto a UINavigationController or into a UITabBarController. The child coordinator shouldn't really care about the context into which it is added.
As for start() returning a controller or accepting a closure or even having the coordinator expose the controller via a property I'm not sure it matters much. I chose a closure because I wanted control over exactly when during the start() function the controller's view would be added to the view hierarchy.

Dismissing a modal view but keeping the data

I'm trying to dismiss a modal view and return back to the view controller that was "sent" from, while keeping the data that was entered in the modal view. If I understand correctly I need to use delegates/protocols for this but I'm having a lot of trouble understanding how to actually implement it in this situation.
Basically a user can call a modal view to enter some information in text fields, and when they hit save this function is called:
func handleSave() {
guard let newProductUrl = NSURL(string: urlTextField.text!) else {
print("error getting text from product url field")
return
}
guard let newProductName = self.nameTextField.text else {
print("error getting text from product name field")
return
}
guard let newProductImage = self.logoTextField.text else {
print("error getting text from product logo field")
return
}
// Call save function in view controller to save new product to core data
self.productController?.save(name: newProductName, url: newProductUrl as URL, image: newProductImage)
// Present reloaded view controller with new product added
let cc = UINavigationController()
let pController = ProductController()
productController = pController
cc.viewControllers = [pController]
present(cc, animated: true, completion: nil)
}
Which calls the self.productController?.save function to save the newly entered values into core data, and reloads the productController table view with the new product.
However the issue I'm running into, is that the productController table view is dynamically set depending on some other factors, so I just want to dismiss the modal view once the user has entered the data, and return back to the page the modal view was called from.
EDIT: attempt at understanding how to implement the delegate -
ProductController is the parent class that the user gets to the modal view from:
protocol ProductControllerDelegate: class {
func getData(sender: ProductController)
}
class ProductController: UITableViewController, NSFetchedResultsControllerDelegate, WKNavigationDelegate {
override func viewDidLoad() {
super.viewDidLoad()
weak var delegate:ProductControllerDelegate?
}
func getData(sender: ProductController) {
}
And AddProductController is the modally presented controller where the user enters in the data then handleSave is called and I want to dismiss and return to the ProductController tableview it was called from:
class AddProductController: UIViewController, ProductControllerDelegate {
override func viewDidDisappear(_ animated: Bool) {
// error on this line
getData(sender: productController)
}
If the sole purpose of your protocol is to return the final state of the view controller its usually easier and clearer to use an unwind segue instead of a protocol.
Steps:
1) In the parent VC you make a #IBAction unwind(segue: UIStoryboardSegue) method
2) In the storyboard of the presented ViewController you control drag from either the control you want to trigger the exit or from the yellow view controller itself(if performing the segue in code) on to the orange exit icon.
your code should look like:
#IBAction func unwind(segue: UIStoryboardSegue) {
if let source = segue.source as? MyModalViewController {
mydata = source.data
source.dismiss(animated: true, completion: nil)
}
}
see apple documentation
Edit here is the hacky way to trigger and unwind from code without storyboard; I do not endorse doing this:
guard let navigationController = navigationController,
let presenter = navigationController.viewControllers[navigationController.viewControllers.count - 2] as? MyParentViewController else {
return
}
presenter.unwind(UIStoryboardSegue(identifier: String(describing: self), source: self, destination: presenter))
Basically you need to create a delegate into this modal view.
Let's say you have ParentViewController which creates this Modal View Controller. ParentViewController must implement the delegate method, let´s say retrieveData(someData).
On the Modal View Controller, you can use the method viewWillDisappear() to trigger the delegate method which the data you want to pass to the parent:
delegate.retrieveData(someData)
If you have issues understanding how to implement a delegate you can check this link

Pushing Navigation Controller From Other Class in Swift

I have a Data Manager class that handles some JSON activities. When one action is completed, I want to push to the Navigation controller.
The FirstViewController calls a function on the DataManager Class
DataManager Class
class func extract_json(data:NSData) -> Bool {
//Success
FirstViewController().pushToValueView()
}
FirstViewController Class
func pushToValueView() {
println("Push to Value View")
navigationController?.pushViewController(ValueViewController(), animated: true)
}
In this example, println() is called but no push occurs. If I call pushToValueView() within FirstViewController e.g. self.pushToValueView() (For debug purposes), a push does occur.
Try calling this function on an existing instance of FirstViewController.
I think that in this example you try to push view controller to FirstViewController which is deallocated after exiting the method scope. That is why view controller doesn't appear
Unwrap the optional. Edit: You shouldn't call your vc from a model class btw, thats bad MVC, I'd use an observers rather and get notified when the closure is complete
if let nav = self.navigationController {
println("Push to Value View")
nav.pushViewController(ValueViewController(), animated: true)
}
You can modify your extract_json function so that it accepts a closure to be executed once the data has been extracted. The closure will contain the code to push the new view controller and it will execute in the context of the calling rather than called object:
In DataManager.swift -
func extractJSON(data:NSData, completion:() -> Void) {
//Do whatever
completion()
}
In FirstViewController.swift -
func getDataAndSegue() {
extractJSON(someData, completion:{
navigationController?.pushViewController(ValueViewController(), animated: true)
}
)
}

Resources