using TabBarController in MVVM-C, coordinator for Tab Bar - swift

I am using MVVM with coordinator pattern for navigation. Navigation is made with pushViewController. Tab Bar Controller is created in TabBarCoordinator class.
class TabBarCoordinator: NSObject, BaseCoordinator {
var rootViewController: UIViewController {
return tabBarController
}
var tabBarController: UITabBarController
var window: UIWindow
private let appDependency: AppDependency
var navigationController: NavigationController
private let navigationObserver: NavigationObserver
var childCoordinators: [BaseCoordinator] = []
var onDidFinish: (() -> Void)?
var topController: UIViewController {
if let lastChild = topCoordinator {
return lastChild.topController
}
var controller: UIViewController = navigationController
while let presentedController = controller.presentedViewController {
controller = presentedController
}
return controller
}
func start() {
let homeCoordinator = HomeCoordinator(navigationObserver: navigationObserver,
navigationController: navigationController,
appDependency: appDependency)
add(child: homeCoordinator)
homeCoordinator.start()
let ordersListCoordinator = OrdersListCoordinator(navigationController: navigationController,
navigationObserver: navigationObserver,
appDependency: appDependency,
filterStatus: nil)
add(child: ordersListCoordinator)
ordersListCoordinator.start()
tabBarController.navigationController?.navigationBar.isTranslucent = true
let homeVC = homeCoordinator.topController
homeVC.tabBarItem = UITabBarItem(title: "home", image: nil, selectedImage: nil)
let ordersListVC = ordersListCoordinator.topController
ordersListVC.tabBarItem = UITabBarItem(title: "orders", image: nil, selectedImage: nil)
let controllers = [homeVC, ordersListVC]
tabBarController.viewControllers = controllers.map { UINavigationController(rootViewController: $0) }
window.rootViewController = tabBarController
window.makeKeyAndVisible()
}
required init(window: UIWindow,
navigationObserver: NavigationObserver,
navigationController: NavigationController,
appDependency: AppDependency
) {
tabBarController = UITabBarController()
self.window = window
self.navigationController = navigationController
self.navigationObserver = navigationObserver
self.appDependency = appDependency
}
}
Here the code of func start() for HomeCoordinator class:
func start() {
guard let user = appDependency.userDataStore.authorizedUserInfo?.user else { return }
let viewModel = HomeViewModel(user: user)
viewModel.delegate = self
let homeViewController = HomeViewController(viewModel: viewModel)
viewController = homeViewController
navigationController.viewControllers = [homeViewController]
}
Tab bar Screenshot
Problems with tab bar:
isTranslucent doesn't hide navBar from TabbarController
the screen is black
views of tabBarItems are not shown, but you can still tap them and screens would change
the second vc is just black screen
Any ideas what's wrong with my TabBarCoordinator class? Thank you in advance

Related

Dismiss SwiftUI View Embedded in UINavigationController

I am experiencing an issue when deep linking into a certain SwiftUI view from a link. the openTakeVC is what is called when deep linked. Currently it was to be embedded in a UINavigationController in order to work, if I try just presenting the UIHostingController I get a crash with this error:
Thread 1: "Application tried to present modally a view controller <_TtGC7SwiftUI19UIHostingControllerV8uSTADIUM8TakeView_: 0x14680a000> that has a parent view controller <UINavigationController: 0x1461af000>."
The dismiss functionality works perfectly fine if not embedded in a UINavigationController but I am only able to deep link that view using a UINavigationController.
Is there a fix for this error or a way to dismiss a UIHostingController embedded in a UINavigationController?
func openTakeVC(take: TakeOBJ) {
DispatchQueue.main.async {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
if let _ = appDelegate.window?.rootViewController as? BannedViewController { return }
//let vc = TakeSingleViewController(nibName: "TakeSingleView", bundle: nil, take: take)
let vc = UIHostingController(rootView: TakeView(take: take))
let nav = UINavigationController(rootViewController: vc)
nav.modalPresentationStyle = .fullScreen
nav.setNavigationBarHidden(true, animated: false)
appDelegate.window?.rootViewController?.present(vc, animated: true, completion: nil)
UserDefaults.removeURLToContinue()
}
}
in TakeView
#Environment(\.presentationMode) var presentationMode
Button {
UIImpactFeedbackGenerator(style: .light).impactOccurred()
presentationMode.wrappedValue.dismiss()
} label: {
Image(systemName: "xmark")
}
}
What about creating the hostingcontroller as a separate vc and dismissing it from there?
TakeSingleVC:
final class TakeSingleVC: UIViewController {
var viewModel: TakeViewModel
var subscriptions = Set<AnyCancellable>()
init(viewModel: TakeViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
let childView = UIHostingController(rootView: TakeView(viewModel: viewModel))
addChild(childView)
childView.view.frame = view.bounds
view.addSubview(childView.view)
childView.didMove(toParent: self)
viewModel.dismissSheet
.sink { isDismissed in
if isDismissed {
childView.dismiss(animated: true)
}
}.store(in: &subscriptions)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Dismissing in TakeView
viewModel.dismissSheet.send(true)
TakeViewModel:
final class TakeViewModel: ObservableObject {
// UIKit
var dismissSheet = CurrentValueSubject<Bool, Never>(false)
}
and then change your presentation to
let vc = TakeSingleVC(viewModel: viewModel)
let nav = UINavigationController(rootViewController: vc)

Conditionally show either a Window or the Menu bar view SwiftUI macOS

I'm creating an app where it simply lives in the menu bar, however I'd like a full-sized normal window to pop up if the user is not logged in, I have made a little pop over window which is sufficient for my main app to go into:
The code I have used to achieve this:
class AppDelegate: NSObject, NSApplicationDelegate{
var statusItem: NSStatusItem?
var popOver = NSPopover()
func applicationDidFinishLaunching(_ notification: Notification) {
let menuView = MenuView().environmentObject(Authentication())
popOver.behavior = .transient
popOver.animates = true
popOver.contentViewController = NSViewController()
popOver.contentViewController?.view = NSHostingView(rootView: menuView)
popOver.contentViewController?.view.window?.makeKey()
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
if let MenuButton = statusItem?.button{
MenuButton.image = NSImage(systemSymbolName: "gearshape.fill", accessibilityDescription: nil)
MenuButton.action = #selector(MenuButtonToggle)
}
if let window = NSApplication.shared.windows.first {
window.close()
}
}
#objc func MenuButtonToggle(sender: AnyObject? = nil){
if popOver.isShown{
popOver.performClose(sender)
}
else{
if let menuButton = statusItem?.button{
NSApplication.shared.activate(ignoringOtherApps: true)
self.popOver.show(relativeTo: menuButton.bounds, of: menuButton, preferredEdge: NSRectEdge.minY)
}
}
}
#objc func closePopover(_ sender: AnyObject? = nil) {
popOver.performClose(sender)
}
#objc func togglePopover(_ sender: AnyObject? = nil) {
if popOver.isShown {
closePopover(sender)
} else {
MenuButtonToggle(sender: sender)
}
}
}
I make the popover view inside the AppDelegate, I'd like to either render this (with the icon in the menu bar) or just a normal macOS window (without the icon in the menu bar). Then have the ability to switch between the two easily via something like this:
if session != nil{
// show menu bar style
else{
// show window view to log in
}
I think you can reference the demo
Create a reference to an instance of NSWindowController in your AppDelegate class.
private var mainVC: MainViewController?
func showMainWindow() {
if mainVC == nil {
mainVC = MainViewController.create()
mainVC?.onWindowClose = { [weak self] in
self?.mainVC = nil
}
}
mainVC?.showWindow(self)
}
The MainviewController is like following:
class MainViewController: NSWindowController {
var onWindowClose: (() -> Void)?
static func create() -> MainViewController {
let window = NSWindow()
window.center()
window.styleMask = [.titled, .closable, .miniaturizable, .resizable]
window.title = "This is a test main title"
let vc = MainViewController(window: window)
// Use your SwiftUI here as the Main Content
vc.contentViewController = NSHostingController(rootView: ContentView())
return vc
}
override func showWindow(_ sender: Any?) {
super.showWindow(sender)
NSApp.activate(ignoringOtherApps: true)
window?.makeKeyAndOrderFront(self)
window?.delegate = self
}
}
extension MainViewController: NSWindowDelegate {
func windowWillClose(_ notification: Notification) {
onWindowClose?()
}
}

Exit Coordinator in Coodinator Pattern in 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()

Passing data from UIViewControllerRepresentable to UIViewController

I'm trying to pass data from a SwiftUI struct (a first name and a last name) and can't seem to update the variables in my UIViewController with the data in my UIViewControllerRepresentable.
I've checked and confirmed that the data I'm trying to pass in from my SwiftUI view is correct. What do I need to do/change to update the firstName and lastName variables in my UIViewController?
import UIKit
import PDFKit
import SwiftUI
class PDFPreviewViewController: UIViewController {
public var documentData: Data?
var firstName: String = "firstName did not load"
var lastName: String = "lastName did not load"
#IBOutlet weak var pdfView: PDFView!
override func viewDidLoad() {
super.viewDidLoad()
let pdfCreator = PDFCreator(firstName: firstName, lastName: lastName, format: "test format")
documentData = pdfCreator.createReleasePDF()
if let data = documentData {
pdfView.document = PDFDocument(data: data)
pdfView.autoScales = true
}
}
}
struct PDFPreviewController: UIViewControllerRepresentable {
var release: ModelRelease
let vc = PDFPreviewViewController()
func makeUIViewController(context: UIViewControllerRepresentableContext<PDFPreviewController>) -> UIViewController {
let storyboard = UIStoryboard(name: "Preview", bundle: Bundle.main)
let controller = storyboard.instantiateViewController(identifier: "Preview")
vc.firstName = release.firstName
vc.lastName = release.lastName
return controller
}
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<PDFPreviewController>) {
}
}
struct PDFPreviewControllerWrapper: View {
#Environment(\.presentationMode) var presentationMode
var release: ModelRelease
var body: some View {
NavigationView {
PDFPreviewController(release: release)
.navigationBarTitle(Text("Preview"))
.navigationBarItems(trailing: Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Done")
.fontWeight(.bold)
}
)
}
}
}
Creating view controller representable you set parameters to one controller but return another. Probably you meant the following:
//let vc = PDFPreviewViewController() // don't think it is needed at all
func makeUIViewController(context: UIViewControllerRepresentableContext<PDFPreviewController>) -> UIViewController {
let storyboard = UIStoryboard(name: "Preview", bundle: Bundle.main)
let controller = storyboard.instantiateViewController(identifier:
"Preview") as! PDFPreviewViewController
controller.firstName = release.firstName // << here !
controller.lastName = release.lastName // << and here !
return controller
}

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?