Correct release NSViewController embedded in subviews - swift

I have a task to write a small app where various controllers must be embedded in default controller. All controllers stored in one storyboard.
Sample code of embedding controllers in subviews
if let id = getControllerId(pageIndex) { // get controller's storyboard id by segmented index
let storyBoard = NSStoryboard(name: "Main", bundle: nil)
let controller = storyBoard.instantiateControllerWithIdentifier(id) as! NSViewController
controller.view.translatesAutoresizingMaskIntoConstraints = false
if self.view.subviews.count > 0 {
let prevView = self.view.subviews[0]
prevView.removeFromSuperview() // here should be releasing previous controller
}
self.view.addSubview(controller.view)
// make all side constraints
let views = ["view": controller.view]
self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("|-(0)-[view]-(0)-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: views))
self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-(0)-[view]-(0)-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: views))
} else {
NSLog("ERROR: Unable get controller storyboard id for index \(pageIndex)")
}
And I noticed that embedded controller don't execute viewWillDisappear. I need this event for clearing observers and some other stuff.
I'm not sure it's correct way to show controllers as embedded in subviews, but i don't found any other solution.
I make sample project to test this situation
https://github.com/avvensis/embeddedviewcontrollers
Could anybody help me with this trouble?

viewWillDisappear wont execute as you dont hide anything.
You controller dies quickly after you create him. So step #1 is to hold a reference on it:
class ViewController: NSViewController {
// MARK: - Custom properties
let pageIds: [String] = ["redController", "yellowController", "greenController"]
var currentControler : NSViewController!
...
private func showEmbeddedController(pageIndex: Int) {
if let id = getControllerId(pageIndex) { // get controller's storyboard id by segmented index
let storyBoard = NSStoryboard(name: "Main", bundle: nil)
currentControler = storyBoard.instantiateControllerWithIdentifier(id) as! NSViewController
currentControler.view.translatesAutoresizingMaskIntoConstraints = false
if self.view.subviews.count > 0 {
let prevView = self.view.subviews[0]
prevView.removeFromSuperview() // here should be releasing previous controller
}
self.view.addSubview(currentControler.view)
// make all side constraints
let views = ["view": currentControler.view]
self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("|-(0)-[view]-(0)-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: views))
self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-(0)-[view]-(0)-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: views))
} else {
NSLog("ERROR: Unable get controller storyboard id for index \(pageIndex)")
}
}
}
Then just have deinit method in your base controller:
class EmbeddedViewController: NSViewController {
....
deinit {
print("DEBUG: \(self.className) deinit")
}
}

Related

How do I reload UIScrollView containing ViewControllers to add/remove view controllers

I have a scrollView that contains a dynamic amount of WeatherViewControllers each displaying the weather data of a different city the user has saved. The user can segue from the WeatherViewControllers to a CityListViewController. Where they can add and remove cities from their list which in turn should add and remove WeatherViewControllers from the scrollView upon dismissing the CityListViewController, this is where I am running into a problem.
Currently I am trying to use a protocol to call viewDidLoad in the scrollViewController upon dismissing the CityListViewController but am getting an error:
Fatal error: Unexpectedly found nil while unwrapping an Optional
value: file)
when it gets to:
let weatherScreen = storyboard!.instantiateViewController(identifier: "View Controller") as! ViewController
Side Note: Upon initially opening the app the scrollView loads properly with all the correct WeatherViewControllers in the UIScrollView and the correct cities in the list.
class ScrollViewController: UIViewController, ScrollReloadProtocol {
func reloadScrollView() {
print("SCROLL RELOADED!!!!!*******")
self.viewDidLoad()
}
#IBOutlet weak var totalScrollView: UIScrollView!
var pages = [ViewController]()
var x = 0
var weatherScreensArray = [SavedCityEntity]()
var weatherScreenStringArray = [String]()
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
var horizString = "H:|[page1(==view)]"
let defaults = UserDefaults.standard
override func viewDidLoad() {
super.viewDidLoad()
//userDefaults used to keep track of which screen is which to put different cities on different viewControllers
defaults.set(0, forKey: "screenNumber")
//load cities to get number of cities saved
loadCities()
var views : [String: UIView] = ["view": view]
//create all weatherWeatherControllers
while x <= weatherScreensArray.count {
pages.append(createAndAddWeatherScreen(number: x))
weatherScreenStringArray.append("page\(x+1)")
views["\(weatherScreenStringArray[x])"] = pages[x].view
let addToHoriz = "[\(weatherScreenStringArray[x])(==view)]"
horizString.append(addToHoriz)
x+=1
}
horizString.append("|")
let verticalConstraints = NSLayoutConstraint.constraints(withVisualFormat: "V:|[page1(==view)]|", options: [], metrics: nil, views: views)
let horizontalConstraints = NSLayoutConstraint.constraints(withVisualFormat: horizString, options: [.alignAllTop, .alignAllBottom], metrics: nil, views: views)
NSLayoutConstraint.activate(verticalConstraints + horizontalConstraints)
}
//Function to create and add weatherViewController
func createAndAddWeatherScreen(number: Int) -> ViewController {
defaults.set(number, forKey: "screenNumber")
let weatherScreen = storyboard!.instantiateViewController(identifier: "View Controller") as! ViewController
weatherScreen.view.translatesAutoresizingMaskIntoConstraints = false
totalScrollView.addSubview(weatherScreen.view)
addChild(weatherScreen)
weatherScreen.didMove(toParent: self)
return weatherScreen
}
}
You could try something like this:
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let weatherScreen = storyboard.instantiateViewController(withIdentifier: "View Controller") as! ViewController
It's also probably better to use one word for the identifier: "ViewController", and give it the same name as the class. Make sure to set these values also in your actual storyboard.

Change ViewController from Tabbar

I have a UITabBarController with 3 tabs and a SettingsVC where you can choose the appearance of MainVC (There are 2 different ViewControllers which based on what user prefers should be shown as the MainVC).
I made the application and it works but it's really buggy, cause I was just pushing the ViewControllers on top of each other, and in some certain conditions you see bugs while switching tabs.
Main.storyboard
Here is some part of my code(after trying a lot of stuff) which changes the VC's based on UserDefaults
CheckSettingsStatus.swift
//
// Check for Double Column Settings and navigate to Double Column VC
//
func checkMainVCViewStatusAndNavigateAcordingly(){
if UserDefaults.standard.integer(forKey: KEY_MAIN_VC_VIEW) == 1{
//let appDel = UIApplication.shared.delegate as! AppDelegate
//appDel.goToDoubleMainVC()
let mainVC = storyboard?.instantiateViewController(withIdentifier: "DoubleColumnMainVC") as! DoubleColumnMainVC
self.navigationController?.viewControllers[0] = mainVC
//navigationController?.pushViewController(mainVC, animated: false)
var navigationArray = self.navigationController?.viewControllers
print("Number of VCs = \(String(describing: navigationArray?.count))")
if navigationArray?.count == nil{
self.navigationController?.pushViewController(mainVC, animated: false)
}else{
self.navigationController?.popToRootViewController(animated: false)
}
// self.navigationController?.viewControllers = navigationArray!
}
}
//
// same as function above but goes to MainVC
//
func checkDoubleColumnMainVCViewStatusAndNavigateAcordingly(){
if UserDefaults.standard.integer(forKey: KEY_MAIN_VC_VIEW) == 0{
// let appDel = UIApplication.shared.delegate as! AppDelegate
// appDel.goToMainVC()
let mainVC = storyboard?.instantiateViewController(withIdentifier: "MainVC") as! MainVC
self.navigationController?.viewControllers[0] = mainVC
var navigationArray = self.navigationController?.viewControllers
print("Number of VCs = \(String(describing: navigationArray?.count))")
if navigationArray?.count == nil{
navigationController?.pushViewController(mainVC, animated: false)
}else{
self.navigationController?.popToRootViewController(animated: false)
}
// self.navigationController?.viewControllers = navigationArray!
}
}
With this in ViewWillAppear of both this Controllers I call these functions to switch accordingly.
Any ideas what I'm doing wrong? Can I push controllers from UITabBarController and not NavigationController? something like:
tabBarController.pushViewController(mainVC, animated: false)
instead of:
navigationController.pushViewController(mainVC, animated: false)
Thank you in advance
UPDATE:
In my best case when everything works nicely, on first launch of the app (to the second mainVC) the Buttons in NavBar doesn't work.
EDIT:
I just realised that the NavigationBar that I'm seeing above my SecondMainVC is the NavigationBar from MainVC, that is why the buttons are not working. how is that possible?
I faced same issue while trying to develop a tab bar controller in storyboard , for this reason I found that the best way is to implement it by code.
so you can make your code look like this :
1) create a a sub class of UITabController and let it conform to UITabBarControllerDelegate
class TabBarVC: UITabBarController , UITabBarControllerDelegate {
2)then write this method in your class to initialize the tabs in your project:
private func settingUpTabBarAndInitializingViewControllers(){
//self delagting
self.delegate = self
//home tab
let homeStoryboard = UIStoryboard(name: Constants.StoryBoards.homeStoryboard, bundle: nil)
let homeVC = homeStoryboard.instantiateViewController(withIdentifier: Constants.ControllersIDs.homeScreen) as! HomeVC
// shop tab
let shopStoybaord = UIStoryboard(name: Constants.StoryBoards.shopStoryboard, bundle: nil)
let shopVC = shopStoybaord.instantiateViewController(withIdentifier: Constants.ControllersIDs.shopScreen) as! ShopVC
//offers tab
let offersStoryboard = UIStoryboard(name: Constants.StoryBoards.offersStoryboard, bundle: nil)
let offersVC = offersStoryboard.instantiateViewController(withIdentifier: Constants.ControllersIDs.offersScreen) as! OffersVC
//more tab
let moreStoryboard = UIStoryboard(name: Constants.StoryBoards.MoreScreenStoryboard, bundle: nil)
let moreVC = moreStoryboard.instantiateViewController(withIdentifier: Constants.ControllersIDs.moreVCScreen) as! MoreOptionsTVC
//setting VCs
self.viewControllers = [homeVC , shopVC , offersVC , moreVC]
//setting buttons
//buttons images
let homeImage = UIImage(named: "TabHome")
let shopImage = UIImage(named : "TabShop")
let offerImage = UIImage(named: "TabOffers")
let moreImage = UIImage(named: "TabMenu")
// buttons shakhsiyan
let homeButton = UITabBarItem(title: homeText, image: homeImage, selectedImage: homeTappedImage)
let shopButton = UITabBarItem(title: shopText, image: shopImage, selectedImage: shopTappedImage)
let offersButton = UITabBarItem(title: offersText, image: offerImage, selectedImage: offersTappedImage)
let moreButton = UITabBarItem(title: moreText, image: moreImage, selectedImage: moreTappedImage)
homeVC.tabBarItem = homeButton
shopVC.tabBarItem = shopButton
offersVC.tabBarItem = offersButton
moreVC.tabBarItem = moreButton
}
P.S. you can add tabs as much as you want and while scrolling in them you wont face any bug,
make sure to have only one navigation controller before your tab bar controller.
Check this hope it helps
Ok you can create a DEMO to see how thing works.After that you can implement it in your project.I know it may not look good to you but read it once and see image attached i am sure will get your answer.
1.I have created two UIViewControllers with the UITabBarController.
2.I have created class ItemFirstVC,ItemTwoVC and assigned to UIViewControllers in storyboard
3.Each controller is embedded in UINavigationController
4.Create a custom class of UITabBarController
5.Assign it to UITabBarController in storyboard
6.Now Each UIViewController has a button named as Firs Button,Second Button
7.From each button i have connected Action Segue to one more controller as you can see in image.
8.Now i have created a third UIViewController which is separate from UITabBarController.
9.Now we will load that Third UIViewController in UITabBarController
10.Create one more class ItemThirdVC and assign it to third UIViewController.
11.It also has a button named as third button action segued to one more UIViewController
12.Now switch to custom class of UITabBarContoller
// variable required to see work in action
var count = 0
class CustomTabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
// remember to assign delegate
delegate = self
}
// this delegate indicates whether this tab should be selected or not
extension CustomTabBarController : UITabBarControllerDelegate{
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
// switch to UIViewController tabs multiple times
count = count + 1
if(count == 5){
// when count reaches 5 a new controller will load
let navigationController = viewController as? UINavigationController
guard navigationController != nil else {return true}
let viewController = storyboard?.instantiateViewController(withIdentifier: "ItemThreeVC")
guard let vc = viewController else {return true}
navigationController?.setViewControllers([vc], animated: false)
}
return true
}
}

How to access a function in another class being within the same instance?

I am currently creating a login project in Swift 3.
I have a main view controller which contains
a container, which includes a page viewcontroller and multiple pages
a custom "menu" at the bottom which includes two buttons for navigating the page view controller and a custom page control
This is my storyboard setup
The view the user will be presented with is made of the pages controlled by the page viewcontroller, at the top, and the control menu, at the bottom. When the user clicks on the buttons in the control menu this code will be executed in the main viewcontroller:
#IBAction func buttonNext(_ sender: UIButton) {
print("buttonNext has been pressed!")
nextPageWithIndex(index: nextIndex)
}
and here is the function it calls:
func nextPageWithIndex(index: Int) {
let nextWalkthroughVC = pages[index]
pageContainer.setViewControllers([nextWalkthroughVC], direction: .forward, animated: true, completion: nil)
print("nextPageWithIndex has been executed with Index \(index)!")
if currentIndex as Int? == 2 {
currentIndex = 2
} else if currentIndex as Int? == 1 {
currentIndex = 2
} else if currentIndex as Int? == 0 {
currentIndex = 1
} else {
currentIndex = 0
print("currentIndex was in inpossible state!")
}
checkButtonNavigation()
assignIndexes()
if let index = currentIndex {
pageControl.currentPage = index
print("PageControl updated")
}
}
checkButtonNavigation() checks if the buttons at the bottom should be displayed depending on the page which is currently being displayed.
I am trying to implement a button of one of the pages (in this case page1) which navigates the user to the next page.
I tried to execute the function nextPageWithIndex(index: 1) with varAccountRegisterMainViewController.nextPageWithIndex(index: 1) on the action of pressing the button in the page1 viewcontroller class by defining:
var varAccountRegisterMainViewController: accountRegisterMainViewController!
in the AppDelegate,
and:
varAccountRegisterMainViewController = accountRegisterMainViewController()
in the page1 viewcontroller.
Sadly that doesn't seem to work. I get "fatal error: Index out of range" at the point:
let nextWalkthroughVC = pages[index]
in the function nextPageWithIndex(index: 1) in the main viewcontroller. I checked what count the array "pages" has. It's 0. When I execute the function right from the main viewcontroller, though, it has 3 as it should have. Here is the error as an image.
Thank you for your help!
EDIT:
here is how the pages Array is set-up:
At the beginning of the main viewcontroller class the following code is added:
var pages = [UIViewController]()
In the viewDidLoad() that code:
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let page1: UIViewController! = storyboard.instantiateViewController(withIdentifier: "IDAccountRegisterPage1ViewController")
let page2: UIViewController! = storyboard.instantiateViewController(withIdentifier: "IDAccountRegisterPage2ViewController")
let page3: UIViewController! = storyboard.instantiateViewController(withIdentifier: "IDAccountRegisterPage3ViewController")
pages.append(page1)
pages.append(page2)
pages.append(page3)
ANOTHER EDIT:
I put the declaration of "pages" also into the externalNext() function. Now that problem is solved but there is another one... this is how the function looks now:
func externalNext(index: Int) {
var pageContainer: UIPageViewController!
var pages = [UIViewController]()
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let page1: UIViewController! = storyboard.instantiateViewController(withIdentifier: "IDAccountRegisterPage1ViewController")
let page2: UIViewController! = storyboard.instantiateViewController(withIdentifier: "IDAccountRegisterPage2ViewController")
let page3: UIViewController! = storyboard.instantiateViewController(withIdentifier: "IDAccountRegisterPage3ViewController")
pages.append(page1)
pages.append(page2)
pages.append(page3)
// Create the page container
pageContainer = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
pageContainer.delegate = self
pageContainer.dataSource = self
pageContainer.setViewControllers([page1], direction: UIPageViewControllerNavigationDirection.forward, animated: false, completion: nil)
// Add it to the view
view.addSubview(pageContainer.view)
pageContainer.view.addSubview(pageControlView)
print("Current array count of pages is \(pages.count)")
print("Function: nextPageWithIndex has been called with index \(index)")
let nextWalkthroughVC = pages[index]
pageContainer.setViewControllers([nextWalkthroughVC], direction: .forward, animated: true, completion: nil)
print("nextPageWithIndex has been executed with Index \(index)!")
pageControl.currentPage = index
print("PageControl updated")
print("Hello")
}
I copied the code from the top of the main viewcontroller class and just pasted it into the function. I alway get the error "fatal error: unexpectedly found nil while unwrapping an Optional value", though.
Is there any way I can redefine the objects in the function?
Or is there any elegant way? My way looks very messy...
Thanks to Phillip Mills I know that is has something to do with being another instance of the class main viewcontroller. How can I access the function in there but within the same instance? The main viewcontroller is visible all the time.
Thank you!
When you do this: accountRegisterMainViewController() you are creating a new object, a different instance of that class. Since the new one is not being displayed, viewDidLoad is never called and your array is empty.
You need to arrange links between objects so that the one loaded from your storyboard is also the one that nextPageWithIndex is called on.

Get back parentViewController

I have a PhoneViewController : UIViewController
let phonePage = storyboard.instantiateViewControllerWithIdentifier("phoneViewController") as! PhoneViewController
I am presenting it from 2 different controllers.
// UIViewController1
self.navigationController?.pushViewController(phonePage, animated: true)
// UIViewController2
self.presentViewController(phonePage.embedInNavController(), animated: true, completion: nil)
I would like to have a have to detect which controller was its parent. How would I be able to do that?
you can try this code:-
if let wd = self.window {
var vc = wd.rootViewController
if(vc is UINavigationController){
vc = (vc as UINavigationController).visibleViewController
}
if(vc is YourViewController){
//your code
}
}
The short answer is :
if let navController = self.navigationController {
return navController.viewControllers[navController.viewControllers.count - 1]
// take care if count <= 1
else {
return self.parent
}
But is this what you really looking for ? What behavior are you trying to implement based on who is his parent ?
I don't have the answer to this question but you should make your code readable. Let me explain by an example :
let phonePage = storyboard.instantiateViewControllerWithIdentifier("phoneViewController") as! PhoneViewController
// Option 1
self.navigationController?.pushViewController(phonePage, animated: true)
phonePage.mode = PhonePageMode.list
// Option 2
self.presentViewController(phonePage.embedInNavController(), animated: true, completion: nil)
phonePage.mode = PhonePageMode.grid
// In your PhoneViewController class
switch self.mode {
case .list: // present as a list
case .grid: // present as a grid
}
is more readable than :
let phonePage = storyboard.instantiateViewControllerWithIdentifier("phoneViewController") as! PhoneViewController
// Option 1
self.navigationController?.pushViewController(phonePage, animated: true)
// Option 2
self.presentViewController(phonePage.embedInNavController(), animated: true, completion: nil)
// In your PhoneViewController class
guard let parent = self.parentViewController else { return }
if parent is ThisClassWhichWantsAList {
// present as list
} else if parent is ThisOtherClassWhichWantsAGrid {
// present as grid
}
If you want to use its parent as a condition to do things differently, you'd rather use an additional attribute. Your futur self will be thankful.
if your'r view controller is pushed from navigationViewController than parent class in parentViewController available and if presented than in presentationController available.
You can check in this way:
if let parentVC = self.navigationController.parentViewController{
//parentVC.className
}
if let presentedVC = self.navigationController?.presentationController{
//presentedVC.className
}
So you basically have to check if the view controller in the navigation controller stack, at the index of count - 1 is the kind of view controller that you are looking for.
Short version:
if navigationController.viewControllers[navigationController.viewControllers.count - 1].isKind(of: TheVCYouAreLooking for.self) {
print("it is")
} else {
print("it is NOT")
}
Long version from a playground:
//These are your two view controllers
class FirstVCClass: UIViewController {}
class SecondVCClass: UIViewController {}
let vc = FirstVCClass()
let secondVC = SecondVCClass()
//Create a navigationcontroller and add the first VC in the stack
let navigationController = UINavigationController()
navigationController.setViewControllers([vc], animated: true)
//Now push the secondVC
vc.navigationController?.pushViewController(secondVC, animated: true)
//The last vc in the stack is the one you've just pushed
print(navigationController.viewControllers.last!)
// Now check
if navigationController.viewControllers[navigationController.viewControllers.count - 1].isKind(of: FirstVCClass.self) {
//The view controller that is before you current vc in the stack is of the class
print("it is")
} else {
print("it is NOT")
}

How do I find the visibleViewController in appdelegate?

I currently instantiate the storyboards programmatically like so:
var iphone35StoryBoard:UIStoryboard = UIStoryboard(name: "iphone35", bundle: nil)
var initialViewController:UIViewController = iphone35StoryBoard.instantiateInitialViewController() as! UIViewController
self.window = UIWindow(frame: UIScreen.mainScreen().bounds)
self.window?.rootViewController = initialViewController
self.window?.makeKeyAndVisible()
This is my first time working with push notifications, so I'm unsure as the best practice, but what i'm trying to do is in didReceiveRemoteNotification and while the app is in an active state(foreground), I would like to be able to determine what view the user is currently on to determine if i should A) Call a local notificaiton or B) update the view if the push notification corresponds to the visible view.
Using various other SO related questions, I have tried to access the visibleviewcontroller using these variations of code:
let navigationController = application.windows[0].rootViewController as! UINavigationController
1)
let visibleController: AnyObject? = navigationController.visibleViewController
if visibleController is ProfileViewController {
println("this works")
}
and
2)
let viewControllers: [UIViewController] = navigationController.viewControllers as! [UIViewController]
for vc in viewControllers {
if vc is ProfileViewController {
println("this works")
}
}
3)
if let wd = self.window {
var vc = wd.rootViewController
if(vc is UINavigationController){
vc = (vc as UINavigationController).visibleViewController
}
if(vc is ProfileViewController){
println("this works")
}
}
the problem i'm running into is that I can only obtain the rootviewcontroller and never the visible view controller. The rootviewcontroller in this case is a login screen, and despite me being on the profileViewController, i can only obtain the loginviewcontroller. Is this the best method for dealing with push notifications?
Move to destination view controller from storyboard when push notification will came
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main",bundle: nil)
var destViewController : destVC
destViewController = mainStoryboard.instantiateViewControllerWithIdentifier("destVC") as! destVC
var navigationController = UIApplication.sharedApplication().keyWindow!.rootViewController as! UINavigationController
navigationController.pushViewController(destViewController, animated: true)
This is working for me from appdelegate.
Use notification center for more stuff :
NSNotificationCenter.defaultCenter().postNotificationName("name", object: nil, userInfo:nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: "method:", name: "name", object: nil)
func method(notification: NSNotification) {
//Do your stuff.
}
Passing data with notification center
NSNotificationCenter.defaultCenter().postNotificationName("name", object:nil, userInfo:["message":"Unable to add \(homeName) Home"])
func method(notification: NSNotification) {
//Do your stuff.
println(notification.userInfo)
}