Passing CoreData Container from UITabBar to another VC - ios

i want to pass the container from my Tabbar to my VC, I tried to do it something like this, but does not work at all. How do I fix it?
My VC
class FavVC: UIViewController {
var container: NSPersistentContainer!
override func viewDidLoad() {
super.viewDidLoad()
guard container != nil else { fatalError("This view needs a persistent container.") }
NSLayoutConstraint.activate([
])
}
}
My Tabbar
class TabBar:UITabBarController {
var container: NSPersistentContainer!
#IBOutlet weak var TabBarContent: UITabBar!
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let favVC = segue.destination as? FavVC {
favVC.container = container
}
}
override func viewDidLoad() {
super.viewDidLoad()
guard container != nil else { fatalError("This view needs a persistent container.") }
print("it works!")
}
}

I think you don't need to use segue here, because UITabBarController do navigation automatically. Try to implement UITabBarControllerDelegate, add this extension for your TabBar class:
extension TabBar: UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
if let favVC = viewController as? FavVC {
favVC.container = container
}
return true
}
}
Then modify TabBar viewDidLoad like this:
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
guard container != nil else { fatalError("This view needs a persistent container.") }
print("it works!")
}
Now you have container before FavVC viewDidLoad call.

If any of you have the same struggle as I do.
You can simply add
if let tabBarVC = self.tabBarController as? TabBar {
self.container = tabBarVC.container
}
on your UIViewController

Related

Segue and delegates in UIKit

I have 2 Controllers: MainVC and SideMenuVC.
I wanted to modify MainVC using SideMenuVC, so created delegate of SideMenuVC ( as well, there's Emdebed segue "name..." to "Side Menu View Controller" on storyboard because MainViewController has subView, which contains ContainerView - this container is our SideMenuVC. And this delegate works as he should.
However, due to logic in the app, I also need to send data from MAINVC to SIDEMENUVC.
So i did the same - created another delegate of second VC... But I turned out, MainViewControllerDelegate is not responding in SideMenuViewController. And i'm absolutely clueless...
Yes, i do implement necessary protocols in both classes, in extension!
Code of both VCs below, screens of storyboard in the attachment
MainViewController + MainViewControllerDelegate
protocol MainViewControllerDelegate{
func isImageLoaded(_ isLoaded:Bool)
}
class MainViewController: UIViewController {
/* ... */
var delegate: MainViewControllerDelegate?
var sideMenuViewController: SideMenuViewController?
private var isSideMenuPresented:Bool = false
private var isImageLoaded:Bool = false
override func viewDidLoad() {
super.viewDidLoad()
self.isImageLoaded = false
self.setupUI()
self.delegate?.isImageLoaded(false)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if (segue.identifier == "MainVC_SideMenuVC_Segue")
{
if let controller = segue.destination as? SideMenuViewController
{
self.sideMenuViewController = controller
self.sideMenuViewController?.delegate = self
}
}
}
/* ... */
//I'm using PHPicker, and when new image is selected, i want to send "true" via delegate
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
dismiss(animated: true)
if let itemProvider = results.first?.itemProvider, itemProvider.canLoadObject(ofClass: UIImage.self){
let previousImage = self.presentedImage.image
itemProvider.loadObject(ofClass: UIImage.self){ [weak self] image, error in
DispatchQueue.main.async {
guard let self = self, let image = image as? UIImage, self.presentedImage.image == previousImage else {
return
}
self.presentedImage.image = image
self.isImageLoaded = true;
self.delegate?.isImageLoaded(true)
}
}
}
}
SideMenuViewController + SideMenuViewControllerDelegate
protocol SideMenuViewControllerDelegate{
func hideSideMenu()
func performAction(_ type:OperationType)
}
class SideMenuViewController: UIViewController {
/*...*/
var delegate: SideMenuViewControllerDelegate?
var mainViewController: MainViewController?
private var menuData: [ExpandingCellModel] = []
private var isImageLoaded: Bool = false
override func viewDidLoad() {
super.viewDidLoad()
// self.mainViewController?.delegate = self
menuData = setupData()
setupUI()
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let controller = segue.destination as? MainViewController {
controller.delegate = self
}
}
}
/* ... */
Here is what I think is happening.
There is segue happening from MainVC to SideMenuVC but there is no segue actually happening between SideMenuVC to MainVC in my opinion.
Happening is keyword because there is an EmbedSegue from MainVC to SideMenuVC but where is the segue from SideMenuVC to MainVC ? You did some connection in storyboard but nothing is happening in my opinion.
That is why in override func prepare is being called as planned in MainViewControllerDelegate and the delegate is getting set but it is not getting set in SideMenuViewController since override func prepare doesn't get called as no segue happens.
What you can do instead which might work is set both the delegates inside prepare in MainViewControllerDelegate
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if (segue.identifier == "MainVC_SideMenuVC_Segue")
{
if let sideMenuVC = segue.destination as? SideMenuViewController
{
sideMenuVC.delegate = self
// assign MainViewControllerDelegate here
self.delegate = sideMenuVC
}
}
}
Check now if data is sent back to main view controller also.
If your issue is still not yet solved, please have a look and try this small example I set for you in github to show passing data between mainVC and embeddedVC and it might give you some hints.
You can see the result of this in action here: https://youtu.be/J7C7SEC04_E

coordinator is nil and not navigating to the next screen on button click

I have been trying to refactor my source code so that it would conform to the Coordinator Pattern. I have used UITabBarController as the parent viewController of my app which contains 4 viewControllers.
I have been following the tutorials on how to implement the Coordinator pattern for iOS apps, and I have created and set up the protocols and classes of the Coordinator. I have a button inside my viewController (child viewController of the TabbarViewController), however, on button click, coordinator is not pushing / navigating to the desired VC, and I see the coordinator is returning nil on the debug console while debugging through the breakpoint, and I could not figure it out how to resolve this issue.
MainCoordinator.swift:
class MainCoordinator: SubCoordinator {
var subCoordinators = [SubCoordinator]()
var navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func start() {
print("Initialized.. .")
UIApplication.app().window?.rootViewController = self.navigationController
let vc = SplashViewController.instantiate()
vc.coordinator = self
navigationController.pushViewController(vc, animated: false)
}
}
// testing using a simple Viewcontroller class, its background color is set to red, so if the
// navigation works, a blank red VC should appear. but not working so far
func testView() {
let vc = ViewController.instantiate()
vc.coordinator = self
navigationController.pushViewController(vc, animated: false)
}
}
SubCoordinator.swift:
protocol SubCoordinator {
var subCoordinators: [SubCoordinator] { get set }
var navigationController: UINavigationController { get set }
func start()
}
StoryBoarded.swift:
protocol StoryBoarded {
static func instantiate() -> Self
}
// I am using storyBoard, and `instantiate()` should instantiate and return the specified VC
// from the Storyboard with the specified VC id (?)
extension StoryBoarded where Self: UIViewController {
static func instantiate() -> Self {
let id = String(describing: self)
let storyboard = UIStoryboard(name: "Main", bundle: nil)
return storyboard.instantiateViewController(withIdentifier: id) as! Self
}
}
FirstViewController.Swift:
class FirstViewController: UIViewController, StoryBoarded {
#IBOutlet weak var button: UIButton!
var coordinator: MainCoordinator?
//MARK: - viewDidLoad()
override func viewDidLoad() {
super.viewDidLoad()
// If uncommented the below line, coordinator is not returning `nil`, but not navigating
anyways!
//coordinator = MainCoordinator(navigationController: UINavigationController())
}
#IBAction func onButtonTap(_ sender: Any) {
// So, basically I would expect the coordinator to navigate to the testView, but not
navigating
coordinator?.testView()
}
}
ViewController.swift:
// testView
class ViewController: UIViewController, StoryBoarded {
var coordinator: MainCoordinator?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.view.backgroundColor = .red
}
}
and
// TabbarController, set as the root VC after the splashVC is completed
class MainViewController: UITabBarController, StoryBoarded {
var coordinator: MainCoordinator?
override func viewDidLoad() {
super.viewDidLoad()
let firstVC = UIStoryboard.firstViewController()
let secondVC = UIStoryboard.secondViewController()
let views: [UIViewController] = [firstVC, secondVC]
self.setViewControllers(views, animated: false)
self.navigationController?.navigationBar.isHidden = false
}
}
start() is being called, and splashVC appears and updates rootViewController with MainViewontroller on completion, But the navigation is not working at all on button click event.
Any feedback or help would highly be appreciated!
Since you're using the StoryBoarded protocol, you should follow the pattern and call instantiate() for initialization. Then, just set the coordinator.
class MainViewController: UITabBarController, StoryBoarded {
var coordinator: MainCoordinator?
override func viewDidLoad() {
super.viewDidLoad()
let firstVC = FirstViewController.instantiate()
let secondVC = SecondViewController.instantiate()
firstVC.coordinator = self.coordinator
secondVC.coordinator = self.coordinator
let views: [UIViewController] = [firstVC, secondVC]
self.setViewControllers(views, animated: false)
self.navigationController?.navigationBar.isHidden = false
}
}

Container view function not calling in parent view

I'm trying to run a function in a container view that I added in the storyboard editor but when I call it in the parent view controller, nothing happens. I want to be able to call a function and change a property of the child view controller triggered from the parent. Am I doing something wrong or should I just be doing this another way?
TLDR: Trigger a function in a child view controller from parent
Parent View Controller:
// ViewController.swift
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var routeConfirmationView: UIView! //This is the container view that I'm trying to work with
var RouteSelectionViewController: RouteSelectionViewController?
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(routeConfirmationView)
self?.RouteSelectionViewController?.getRidOfLoadingCover(isHidden: true) //The code that isn't doing anything
}
}
Container View Controller:
import UIKit
class RouteSelectionViewController: UIViewController {
#IBOutlet weak var loadingCoverView: UIActivityIndicatorView!
override func viewDidLoad() {
super.viewDidLoad(
}
//The function that I want to trigger from the other view controller:
func getRidOfLoadingCover (isHidden: Bool){
if (isHidden == true) {
loadingCoverView.alpha = 0
}
else if (isHidden == false) {
loadingCoverView.alpha = 100
}
}
}
Storyboard:
To call functions, or access properties, in a View Controller embedded in a Container View, you need to get and keep a reference to that controller.
When the Container View loads the embedded VC, it calls Prepare For Segue. Grab your reference there:
class WithContainerViewController: UIViewController {
var routeSelectionVC: RouteSelectionViewController?
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let vc = segue.destination as? RouteSelectionViewController {
// save reference to VC embedded in Container View
self.routeSelectionVC = vc
}
}
#IBAction func didTap(_ sender: Any) {
if let vc = routeSelectionVC {
vc.getRidOfLoadingCover(isHidden: true)
}
}
}
class RouteSelectionViewController: UIViewController {
#IBOutlet weak var loadingCoverView: UIActivityIndicatorView!
override func viewDidLoad() {
super.viewDidLoad()
}
//The function that I want to trigger from the other view controller:
func getRidOfLoadingCover (isHidden: Bool){
if (isHidden == true) {
loadingCoverView.alpha = 0
}
else if (isHidden == false) {
loadingCoverView.alpha = 100
}
}
}
You will likely next ask about calling a function in the "parent" VC from the embedded VC. This can be done with protocol / delegate pattern, or with closures. Again, you can set that up in prepare for segue:
class WithContainerViewController: UIViewController {
var routeSelectionVC: RouteSelectionViewController?
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let vc = segue.destination as? RouteSelectionViewController {
// set the closure in the VC embedded in Container View
vc.callbackClosure = {
self.routeSelectionButtonTapped()
}
// save reference to VC embedded in Container View
self.routeSelectionVC = vc
}
}
#IBAction func didTap(_ sender: Any) {
if let vc = routeSelectionVC {
vc.getRidOfLoadingCover(isHidden: true)
}
}
func routeSelectionButtonTapped() -> Void {
print("Button in RouteSelectionViewController in Container View was tapped!")
}
}
class RouteSelectionViewController: UIViewController {
#IBOutlet weak var loadingCoverView: UIActivityIndicatorView!
var callbackClosure: (() -> ())?
override func viewDidLoad() {
super.viewDidLoad()
}
//The function that I want to trigger from the other view controller:
func getRidOfLoadingCover (isHidden: Bool){
if (isHidden == true) {
loadingCoverView.alpha = 0
}
else if (isHidden == false) {
loadingCoverView.alpha = 100
}
}
#IBAction func didTap(_ sender: Any) {
callbackClosure?()
}
}
I guess you did not set the RouteSelectionViewController variable (btw., variable names should start with a small letter), so it is nil and nothing happens.
If you want you embed a view controller into another, you need to implement the view controller containment requirements (see documentation).

How can I determine the calling view controller?

I made a drawer with two sections.. therefore I implemented a delegate pattern to access a embedded tableView inside of my container.
class Drawer: UIViewController, DrawerTableViewDelegate {
var drawerVC : DrawerTableView?
override func viewDidLoad() {
super.viewDidLoad()
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "tableViewSegue" {
self.drawerVC = (segue.destination as! DrawerTableView
self.drawerVC!.delegate = self
}
}
In my DrawerTableView class is the delegate implemented and also some code to access the rows in the tableView:
class MenuTableViewController: UITableViewController {
var delegate : MenuTableViewControllerDelegate?
//tableview delegate
The drawer gets called in two scenes in the same way:
#objc func didTapRowInDrawer(_ sender: UIBarButtonItem) {
guard let drawerVC = storyboard?.instantiateViewController(withIdentifier: "DrawerCtrl") as? Drawer else { return }
drawerVC.modalPresentationStyle = .overCurrentContext
drawerVC.transitioningDelegate = self
present(drawerVC, animated: true)
}
Can I determine the calling ViewController of my drawer?
I thought of something like
if segue.source is VC {
//do something
} else if segue.source is VC2 {
// do something else
}
in Drawer::prepare
From the code, it looks like the Drawer was presented as a Modal. The viewController which presented a Modal is stored in presentingViewController variable. You can use the below code in the DrawerViewController to determine which ViewController had presented the Drawer.
if let presentingVC = self.presentingViewController {
if presentingVC.isKind(of: VC1.self) {
//do something
} else if presentingVC.isKind(of: VC2.self) {
// do something else
}
}

How to update the value in a VC when a tab is selected from a UITabbarController?

I have a tabbar controller. When the user clicks on one of the tab bar buttons, I need to update a value in the UIPageViewController that is in the target view controller.
I am trying to use a delegate to inform a UIPageViewController which tab bar button was clicked:
protocol PlanTypeDelegate {
func setIntro(thisFlow planType: UITabBarItem)
}
class NewTabBarController: UITabBarController {
var planTypeDelegate : PlanTypeDelegate?
override func viewDidLoad() {
super.viewDidLoad()
// create and handle tab bar button actions
}
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
planTypeDelegate?.setIntro(thisFlow: item)
}
In my UIPageController I have the following:
class IntroPageController: UIPageViewController {
override func viewDidLoad() {
super.viewDidLoad()
guard let tabbar = self.parent as? NewTabBarController() else { return }
tabbar.delegate = self
}
}
extension IntroPageController : PlanTypeDelegate {
func setIntro(thisFlow planType: UITabBarItem) {
print("this item:\(planType)")
}
}
Instead I get this error message:
I am new to passing data between VCs so I have no idea how to go about handling this scenario.
EDIT
Same error after update
You can Achieve it like this.. without Delegate ... write setIntro method in IntroPageController i hope it will solve your Issue
class NewTabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
}
}
func tabBarController(_ tabBarController: UITabBarController,
shouldSelect viewController: UIViewController) -> Bool{
if let controller = viewController as? IntroPageController {
controller.setIntro(thisFlow: tabBarController.tabBarItem)
}
return true
}
You can also achieve it through Protocol for that write... All controllers who confirm PlanTypeDelegate can perform action against this method
func tabBarController(_ tabBarController: UITabBarController,
shouldSelect viewController: UIViewController) -> Bool{
if let navController = viewController as? UINavigationController {
if let myViewController = navController.topViewController , let homeController = myViewController as? PlanTypeDelegate {
homeController.setIntro(thisFlow: tabBarController.tabBarItem)
}
} else if let controller = viewController as? PlanTypeDelegate {
controller.setIntro(thisFlow: tabBarController.tabBarItem)
}
return true
}

Resources