Exit Coordinator in Coodinator Pattern in swift - swift

i am a beginner at Coordinator Pattern,
so i am trying to create a simple login screen
Load app -> Login screen -> Main Screen
i read an article by Hacking with Swift about Coordinator Pattern and still a bit confuse on how to exit an coordinator.
So how do i exit a coordinator?
From what i know, the parent coordinator are consist of the Login Coordinator and Main Screen Coordinator , in order to access the Main Screen Coordinator, you need to clear the child coordinator in the parent coordinator and then add the Main Screen Coordinator. is it correct?
So basically what i want to do is go back to the parent Coordinator and then setup the Min View Coordinator as the parent coordinator child
Thanks in advance
Parent Coordinator :
class AppCoordinator:NSObject,CoordinatorExtras {
private(set) var childCoordinators = [Coordinator]()
private let window:UIWindow
init(win:UIWindow){
self.window = win
}
func start() {
let navigationControler = UINavigationController()
let loginCoordinator = LoginCoordinator(navigationController: navigationControler)
childCoordinators.append(loginCoordinator)
loginCoordinator.parentCoordinator = self
loginCoordinator.start()
navigationControler.navigationBar.barStyle = .blackTranslucent
window.rootViewController = navigationControler
window.makeKeyAndVisible()
}
func userIsValid(_ child:Coordinator?) {
childDidFinnish(child)
let navigationControler = UINavigationController()
let tabBarCoordinator = TabBarCoordinator(navigationController: navigationControler)
childCoordinators.append(tabBarCoordinator)
tabBarCoordinator.start()
}
func childDidFinnish(_ child:Coordinator?) {
for (idx,coordinator) in childCoordinators.enumerated() {
if coordinator === child {
childCoordinators.remove(at: idx)
}
}
}
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
guard let fromVC = navigationController.transitionCoordinator?.viewController(forKey: .from) else { return }
if navigationController.viewControllers.contains(fromVC) {
return
}
if let homeVC = fromVC as? LoginViewController {
childDidFinnish(homeVC.coordinator)
}
}
}
The childDidFinish function is based on Paul's but i have no idea on how to use it
So this is how i tried to implement it
Login Cordinator:
final class LoginCoordinator:NSObject, Coordinator {
weak var parentCoordinator:AppCoordinator?
private(set) var childCoordinators: [Coordinator] = []
private let navigationController : UINavigationController
private let loginVC = LoginViewController.instantiate()
init (navigationController:UINavigationController) {
self.navigationController = navigationController
}
func start() {
navigationController.delegate = self
loginVC.coordinator = self
navigationController.isNavigationBarHidden = true
navigationController.pushViewController(loginVC, animated: true)
}
func gotoLogin() {
navigationController.popToRootViewController(animated: true)
}
func gotoRegister(){
let registerVC = RegisterViewController.instantiate()
registerVC.coordinator = self
navigationController.pushViewController(registerVC, animated: true)
}
func gotoHome() {
let tabBarCoordinator = TabBarCoordinator(navigationController: navigationController)
childCoordinators.append(tabBarCoordinator)
tabBarCoordinator.start()
}
func gotoTerdaftar() {
let terdaftarVC = ListKorperasiViewController()
terdaftarVC.coordinator = self
navigationController.pushViewController(terdaftarVC, animated: true)
}
func popView() {
navigationController.popViewController(animated: true)
}
//Check if user is login or not
func userIsLogin() {
parentCoordinator?.userIsValid(self)
}
}

you can add a closure to your child coordinator
var finish: (() -> Void)?
And associate a value to this closure in your parent coordinator.
let loginCoordinator = LoginCoordinator(navigationController: navigationControler)
childCoordinators.append(loginCoordinator)
loginCoordinator.finish = { do something like remove coordinator from childCoordinators and start a new coordinator }
loginCoordinator.start()

Related

How to listen for data change with #Published variable then reload tableView

The most difficult task I face is to know the correct terminology to search for. I'm used to SwiftUI for an easy way to build an app in the fastest time possible. With this project I have to use UIKit and for this specific task.
Inside a view controller I created a tableView:
private let tableView: UITableView = {
let table = UITableView()
table.register(ProfileCell.self, forCellReuseIdentifier: ProfileCell.identifier)
return table
}()
Later I reload the data inside viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
Task {
do {
try await viewModel.getProfiles()
// Here I reload the table when data comes in
self.tableView.reloadData()
} catch {
print(error)
}
}
view.addSubview(tableView)
tableView.delegate = self
tableView.dataSource = self
}
So what is viewModel? In SwiftUI I'm used to having this inside a view struct:
#ObservedObject var viewModel = ProfilesViewModel()
..and that's what I have inside my view controller. I've searched for:
observedobject in uitableview
uitableview reload data on data change
..and more but noting useful for me to "pick up the pieces" with.
In same controller, I'm showMyViewControllerInACustomizedSheet which now uses UIHostingController:
private func showMyViewControllerInACustomizedSheet() {
// A SwiftUI view along with viewModel being passed in
let view = ProfilesMenu(viewModel: viewModel)
let viewControllerToPresent = UIHostingController(rootView: view)
if let sheet = viewControllerToPresent.sheetPresentationController {
sheet.detents = [.medium(), .large()]
sheet.largestUndimmedDetentIdentifier = .medium
sheet.prefersScrollingExpandsWhenScrolledToEdge = false
sheet.prefersEdgeAttachedInCompactHeight = true
sheet.widthFollowsPreferredContentSizeWhenEdgeAttached = true
}
present(viewControllerToPresent, animated: true, completion: nil)
}
For the ProfilesViewModel:
class ProfilesViewModel: ObservableObject {
// ProfilesResponse is omitted
#Published var profiles = [ProfilesResponse]()
public func getProfiles(endpoint: String? = nil) async throws -> Void {
// After getting the data, I set the profiles variable
self.profiles = [..]
}
}
Whenever I call try await viewModel.getProfiles(endpoint: "..."), from ProfileMenu, I'd like to reload the tableView. What additional setup is required?
In the comments, Vadian mentioned "Combine" where I did a Google search and found this. What works, for a basic demonstaration:
[..]
import Combine
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
private let viewModel = ProfilesViewModel()
private var cancellable: AnyCancellable?
override func viewDidLoad() {
super.viewDidLoad()
Task {
do {
try await viewModel.getProfiles()
// Remove this
// self.tableView.reloadData()
} catch {
print(error)
}
}
view.addSubview(tableView)
tableView.delegate = self
tableView.dataSource = self
// Add this
cancellable = viewModel.objectWillChange.sink(receiveValue: { [weak self] in
self?.render()
})
}
// Also add this
private func render() {
// TODO: Implement failures...
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
...
}
objectWillChange was the key to my problem.

How to pass chain view controller presenter with observable

I'm new in the RxSwift development and I've an issue while presentation a view controller.
My MainViewController is just a table view and I would like to present detail when I tap on a item of the list.
My DetailViewController is modally presented and needs a ViewModel as input parameter.
I would like to avoid to dismiss the DetailViewController, I think that the responsability of dismiss belongs to the one who presented the view controller, i.e the dismiss should happen in the MainViewController.
Here is my current code
DetailsViewController
class DetailsViewController: UIViewController {
#IBOutlet weak private var doneButton: Button!
#IBOutlet weak private var label: Label!
let viewModel: DetailsViewModel
private let bag = DisposeBag()
var onComplete: Driver<Void> {
doneButton.rx.tap.take(1).asDriver(onErrorJustReturn: ())
}
override func viewDidLoad() {
super.viewDidLoad()
setup()
bind()
}
private func bind() {
let ouput = viewModel.bind()
ouput.id.drive(idLabel.rx.text)
.disposed(by: bag)
}
}
DetailsViewModel
class DetailsViewModel {
struct Output {
let id: Driver<String>
}
let item: Observable<Item>
init(with vehicle: Observable<Item>) {
self.item = item
}
func bind() -> Output {
let id = item
.map { $0.id }
.asDriver(onErrorJustReturn: "Unknown")
return Output(id: id)
}
}
MainViewController
class MainViewController: UIViewController {
#IBOutlet weak private var tableView: TableView!
private var bag = DisposeBag()
private let viewModel: MainViewModel
private var detailsViewController: DetailsViewController?
override func viewDidLoad(_ animated: Bool) {
super.viewDidLoad(animated)
bind()
}
private func bind() {
let input = MainViewModel.Input(
selectedItem: tableView.rx.modelSelected(Item.self).asObservable()
)
let output = viewModel.bind(input: input)
showItem(output.selectedItem)
}
private func showItem(_ item: Observable<Item>) {
let viewModel = DetailsViewModel(with: vehicle)
detailsViewController = DetailsController(with: viewModel)
item.flatMapFirst { [weak self] item -> Observable<Void> in
guard let self = self,
let detailsViewController = self.detailsViewController else {
return Observable<Void>.never()
}
self.present(detailsViewController, animated: true)
return detailsViewController.onComplete.asObservable()
}
.subscribe(onNext: { [weak self] in
self?.detailsViewController?.dismiss(animated: true)
self?.detailsViewController? = nil
})
.disposed(by: bag)
}
}
MainViewModel
class MainViewModel {
struct Input {
let selectedItem: Observable<Item>
}
struct Output {
let selectedItem: Observable<Item>
}
func bind(input: Input) -> Output {
let selectedItem = input.selectedItem
.throttle(.milliseconds(500),
latest: false,
scheduler: MainScheduler.instance)
.asObservable()
return Output(selectedItem: selectedItem)
}
}
My issue is on showItem of MainViewController.
I still to think that having the DetailsViewController input as an Observable isn't working but from what I understand from Rx, we should use Observable as much as possible.
Having Item instead of Observable<Item> as input could let me use this kind of code:
item.flatMapFirst { item -> Observable<Void> in
guard let self = self else {
return Observable<Void>.never()
}
let viewModel = DetailsViewModel(with: item)
self.detailsViewController = DetailsViewController(with: viewModel)
guard let detailsViewController = self.detailsViewController else {
return Observable<Void>.never()
}
present(detailsViewController, animated: true)
return detailsViewController
}
.subscribe(onNext: { [weak self] in
self?.detailsViewController?.dismiss(animated: true)
self?.detailsViewController = nil
})
.disposed(by: bag)
What is the right way to do this?
Thanks
You should not "use Observable as much as possible." If an object is only going to ever have to deal with a single item, then just pass the item. For example if a label is only ever going to display "Hello World" then just assign the string to the label's text property. Don't bother wrapping it in a just and binding it to the label's rx.text.
Your second option is much closer to what you should have. It's a fine idea.
You might find my CLE library interesting. It takes care of the issue you are trying to handle here.

Why is my UIViewController not showing up in my popup card?

I wanted to create a pop up for one of my UIViewController and found this repo on GitHub.
It is working fine with my InfoViewController which only has 4 UILabels (I think this might be the problem that it is not showing up when you use reusable cells)
But somehow it is not working with my StructureNavigationListViewController and I do not know why.
I call the didTapCategory method in my MainViewController where the StructureNavigationController should pop up but I only see the dimming view (which is weird cause the tap recognizer and pan gestures are working fine but no content is showing up)
In my MainViewController I set up the popup like before:
#IBAction func didTapCategory(_ sender: UIBarButtonItem) {
let popupContent = StructureNavigationListViewController.create()
let cardpopUp = SBCardPopupViewController(contentViewController: popupContent)
cardpopUp.show(onViewController: self)
}
In my StructureNavigationListViewController I set up the table view and the pop up:
public var popupViewController: SBCardPopupViewController?
public var allowsTapToDismissPopupCard: Bool = true
public var allowsSwipeToDismissPopupCard: Bool = true
static func create() -> UIViewController {
let sb = UIStoryboard(name: "Main", bundle: nil)
let vc = sb.instantiateViewController(withIdentifier: "StructureNavigationListViewController") as! StructureNavigationListViewController
return vc
}
#IBOutlet var tableView: UITableView!
var structures = Variable<[Structure]>([])
public var treeSource: StructureTreeSource?
let disposeBag = DisposeBag()
var depthDictionary : [String : Int] = [:]
public override func viewDidLoad() {
structures.asObservable()
.bind(to:tableView.rx.items) {(tableView, row, structure) in
let cell = tableView.dequeueReusableCell(withIdentifier: "StructureNavigationCell", for: IndexPath(row: row, section: 0)) as! StructureNavigationCell
cell.structureLabel.text = structure.name
cell.spacingViewWidthConstraint.constant = 20 * CGFloat(self.depthDictionary[structure.id]!)
return cell
}.disposed(by:disposeBag)
_ = tableView.rx.modelSelected(Structure.self).subscribe(onNext: { structure in
let storyBoard = UIStoryboard(name:"Main", bundle:nil)
let plansViewCtrl = storyBoard.instantiateViewController(withIdentifier: "PlansViewController2") as! PlansViewController2
self.treeSource?.select(structure)
plansViewCtrl.treeSource = self.treeSource
plansViewCtrl.navigationItem.title = structure.name
self.show(plansViewCtrl, sender: self)
if let mainVC = self.parent as? ProjectOverViewTabController2 {
mainVC.addChildView(viewController: plansViewCtrl, in: mainVC.scrollView)
}
})
showList()
}
func showList() {
if treeSource == nil {
treeSource = StructureTreeSource(projectId:GlobalState.selectedProjectId!)
}
//The following piece of code achieves the correct order of structures and their substructures.
//It is extremely bad designed and rather expensive with lots of structures and should
//therefore be refactored!
if let strctrs = getStructures() {
var sortedStructures : [Structure] = []
while(sortedStructures.count != strctrs.count) {
for strct in strctrs {
if let _ = sortedStructures.index(of: strct) {
continue
} else {
depthDictionary[strct.id] = getDepthOfNode(structure: strct, depth: 1)
if let structures = getStructures() {
if let parent = structures.first(where: {$0.id == strct.parentId}) {
if let index = sortedStructures.index(of: parent) {
sortedStructures.insert(strct, at: index+1)
}
} else {
sortedStructures.insert(strct, at: 0)
}
}
}
}
}
structures.value = sortedStructures
tableView.reloadData()
}
}
func getDepthOfNode(structure: Structure, depth: Int) -> Int {
if(structure.parentId == nil || structure.parentId == "") {
return depth
} else {
if let structures = getStructures() {
if let parent = structures.first(where: {$0.id == structure.parentId}) {
return getDepthOfNode(structure: parent, depth: depth + 1)
}
}
}
return -1
}
private func getStructures() -> Results<Structure>? {
do {
if let projectId = GlobalState.selectedProjectId {
return try Structure.db.by(projectId: projectId)
}
} catch { Log.db.error(error: error) }
return nil
}
}
Lot of code here. Sorry..
Is it because I call the create() method after the viewDidLoad() dequeues the cells?
It's hard to tell what is the problem, since you left no information about where didTapCategory is supposed to be called, but maybe it has something to do with your modelSelected subscription being prematurely released?
Edit:
As posted here: https://stackoverflow.com/a/28896452/11851832 if your custom cell is built with Interface Builder then you should register the Nib, not the class:
tableView.registerNib(UINib(nibName: "CustomCell", bundle: nil), forCellReuseIdentifier: "CustomCellIdentifier")

Why delegate method is not called?

I am trying to notify ChatViewController that a chat was deleted in MessagesViewController using a protocol, but the delegate method implemented in ChatViewController is never called.
In the navigationController hierarchy ChatViewController is on top of MessagesViewController.
protocol MessagesViewControllerDelegate:class {
func chatWasDeletedFromDatabase(chatUID: String)
}
class MessagesViewController: UITableViewController {
weak var delegate: MessagesViewControllerDelegate?
func observeChatRemoved() {
print("it is gonna be called")
//inform ChatViewController that a chat was deleted.
self.delegate?.chatWasDeletedFromDatabase(chatUID: chat.chatUID)
print("was called here") //prints as expected
}
}
class ChatViewController: JSQMessagesViewController {
var messagesVC: MessagesViewController?
override func viewDidLoad() {
super.viewDidLoad()
messagesVC = storyboard?.instantiateViewController(withIdentifier: "MessagesViewController") as! MessagesViewController
messagesVC?.delegate = self
}
}
extension ChatViewController: MessagesViewControllerDelegate {
func chatWasDeletedFromDatabase(chatUID: String) {
print("chatWasDeletedFromDatabase called") //never prints out
if self.chatSelected.chatUID == chatUID {
//popToRootViewController
}
}
It seems
weak var delegate: MessagesViewControllerDelegate?
is nil you have to set it to the ChatViewController presented instance what ever how you present it
let chat = ///
self.delegate = chat
self.navigationController?.pushViewController(chat,animated:true)
Also do
chat.messagesVC = self
as this
messagesVC = storyboard?.instantiateViewController(withIdentifier: "MessagesViewController") as! MessagesViewController
messagesVC?.delegate = self
isn't the currently presented messagesVC , so comment the above 2 lines

Shared container with assembly - how to pass same objects to Coordinator and Controller

Everytime we resolve a protocol/class then we get a new object. My coordinator needs a view model and my controller needs the same view model.
internal class LoginFactory: Assembly {
func assemble(container: Container) {
let delegate = UIApplication.shared.delegate as? AppDelegate
container.register(LSViewModel.self) { res in
return LSViewModel()
}
container.register(LSCoordinator.self, factory: { res in
let cord = LSCoordinator(window: (delegate?.window)!)
cord.viewModel = res.resolve(LSViewModel.self)
return cord
})
container.register(LSViewController.self) { res in
let cont = StoryboardScene.Login.lsViewController.instantiate()
cont.viewModel = res.resolve(LSCoordinator.self)?.viewModel
return cont
}
}
}
The coordinator goes like
internal final class LSCoordinator: BaseCoordinator<Void>, Coordinator, HasDisposeBag {
private let window: UIWindow
weak var navigationController: UINavigationController!
//LSViewModel implements LSViewModelType
var viewModel: LSViewModelType!
init(window: UIWindow) {
self.window = window
}
func start() -> Observable<Void> {
let lslViewController: LSViewController = DependencyManager.getResolver().resolve(LSViewController.self)!
navigationController = UINavigationController(rootViewController: lsController)
viewModel.outputs.doneTapped
.subscribe(onNext: { [weak self] in self?.showLoginTypes() }).disposed(by: disposeBag)
window.rootViewController = navigationController
window.makeKeyAndVisible()
return .never()
}
func showLoginTypes() {
print(“blah blah”)
}
}
The problem is, when I am trying to inject viewModel in my lsViewController then a different instance of lsViewModel is created. As a result the Rx bindings are not working and the print statement is not executed. Is there any way to pass the same view model to both coordinator and controller?