I am embedding an AVPlayerViewController(which is expensive in terms of resources) in a UIViewController (using containment). I need to use another AVPlayerViewController in the subsequently pushed view controller in navigation stack, but it would be nice if I can remove it from the parent & embed it in the child. When the child pops, I want to embed it back in the parent. What is the elegant way to do this (code or storyboards)?
You need to do it in code. Create your 'expensive' view controller and store it using strong reference somewhere. You can show it programmatically anywhere, then you can dismiss it, but it will be store by strong reference. Later you can show it again.
P.S. looks like you view controller have AVPlayer, in this case probably you need to add some method to 'wipe' its state before reuse or at leave pause playback
It can be done like this way. You need to setup player .
import UIKit
import AVKit
class AVViewController: UIViewController {
static var player : AVPlayerViewController?
#IBOutlet var containerView: UIView!
override func viewWillAppear(_ animated: Bool) {
DispatchQueue.main.async {
self.addChildViewController(AVViewController.player!)
self.containerView.addSubview((AVViewController.player?.view)!)
// setup player here.
}
super.viewWillAppear(animated)
}
override func viewDidLoad() {
super.viewDidLoad()
for vc in self.childViewControllers{
if let vc = vc as? AVPlayerViewController, AVViewController.player == nil {
AVViewController.player = vc
}
}
// Do any additional setup after loading the view.
}
}
Related
Is there a way to reposition the UIKit tab bar vertically? Like the
gmail app navigation
This question has been asked quite a few times. None of the solutions I found seem to work for me. I’m looking for a solution from scratch in Swift, no third party libs.
Just like what #rmaddy has commented, you will need to create a customised tab and place it in a view as shown below.
FYI, you can easily build a customised tab bar by using buttons.
Once clicked, you just need to add the new controller as a subview by using the extension below.
extension UIViewController {
func add(parentViewController: UIView, childViewController: UIViewController) {
// Add Child View Controller
addChild(childViewController)
// Add Child View as Subview
parentViewController.addSubview(childViewController.view)
// Configure Child View
childViewController.view.frame = parentViewController.bounds
// Notify Child View Controller
childViewController.didMove(toParent: self)
}
}
So from your main view controller, it should look something like this.
class MainController: UIViewController {
// Outlets
#IBOutlet weak var mainView: UIView!
// Lazy loaded controller
private lazy var tab1Controller: Tab1Controller = {
// Instantiate View Controller
let tab1Controller = UIStoryboard(name: "Tab1Controller", bundle: nil).instantiateViewController(withIdentifier: "Tab1") as! Tab1Controller
return tab1Controller
}()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
// MARK: - Tab Bar
#IBAction func Tab1TouchUpInside(_ sender: Any) {
// Navigate to child view
self.add(parentViewController: self.mainView, childViewController: self.tab1Controller)
}
}
Hope it helps!
I have four ViewController, I don't use an UITabbedbar because It's more difficult to customize.
I use modal segue but I think the memory consumption is excessive.
this is a screen shot of my first and second VC.
What I have to use to change View correctly?
That's the code I use :
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
if (segue.identifier == "second") {
let secondVC = segue.destinationViewController as SecondViewController;
}
}
From your Storyboard diagram, it is clear that you have created a segue from each button in your "tab bar" to another view controller. Except for the unwind segue, segues always create a new instance of the view controller they are switching to. So if you use your setup to switch from view controller 1 to view controller 2 and then back to view controller 1, you won't be returning to the view controller you came from but instead you will be creating an entirely new view controller 1.
This is why your memory consumption is excessive. You keep creating view controllers until your app crashes.
I would recommend you return to using a tab bar controller. They were designed to allocate the view controllers once up front and then just switch between them. Also, they have a standard look for a reason, it helps the user of your app know immediately how to interact with them.
To pass data between tabs, you won't use segues because there is no segue happening when you switch tabs. There are many ways you can do this, but they all boil down to having model data stored where all of the tabs can access it. This can be done with CoreData in a larger app. For a simple app, you can do the following:
Create a custom subclass of UITabBarController. Let's call it CustomTabBarController. Have that class create and hold the model data that will be accessed by each of your tabs.
CustomTabBarController.swift:
import UIKit
// This class holds the data for my model.
class ModelData {
var name = "Fred"
var age = 50
}
class CustomTabBarController: UITabBarController {
// Instantiate the one copy of the model data that will be accessed
// by all of the tabs.
var model = ModelData()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
}
In your Storyboard, in the Identity Inspector, change the class of UITabBarController to CustomTabBarController.
In viewWillAppear in each of your tabs, get a reference to the model data and then you can use it.
FirstViewController.swift:
import UIKit
class FirstViewController: UIViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Get a reference to the model data from the custom tab bar controller.
let model = (self.tabBarController as! CustomTabBarController).model
// Show that we can access and update the model data from the first tab.
// Let's just increase the age each time this tab appears and assign
// a random name.
model.age += 1
let names = ["Larry", "Curly", "Moe"]
model.name = names[Int(arc4random_uniform(UInt32(names.count)))]
}
}
SecondViewController.swift:
import UIKit
class SecondViewController: UIViewController {
#IBOutlet weak var nameLabel: UILabel!
#IBOutlet weak var ageLabel: UILabel!
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Get a reference to the model data from the custom tab bar controller.
let model = (self.tabBarController as! CustomTabBarController).model
// This tab will simply access the data and display it when the view
// appears.
nameLabel.text = model.name
ageLabel.text = "\(model.age)"
}
}
I'm still an early stage Swift developer. I'm trying to create a feature where I click a button on a screen, then I get prompted by the iPhone to allow location, and when I click allow, I am automatically navigated to another screen.
So far I've done the following things:
created a seperate class ("User") where all the location functions are handled
Setup a button on the first view controller and the appropiate IBAction that calls the location prompt function from "User"
Added storyboard IDs for the first and second view controller
Created a function ("changeScreen") in the first view controller class that performs the navigation to another view
Setup a listener in the "User" class for when user has clicked allow location, which then calls the "changeScreen" function
I think there's another way to do this (call some sort of completion handler) and I toyed around with that for 1-2 hours and nothing worked. So far this solution has almost completely worked but I get the following error:
Warning: Attempt to present ____ on _____ whose view is not in the window hierarchy!
Here's the code in my "User" class
class User: NSObject, CLLocationManagerDelegate {
func getLocation(){
// For use in foreground
locationManager.requestWhenInUseAuthorization()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestWhenInUseAuthorization()
//locationManager.startUpdatingLocation()
println("hello location")
}
func locationManager(manager: CLLocationManager, didChangeAuthorizationStatus status:CLAuthorizationStatus){
if status == .AuthorizedWhenInUse {
locationManager.startUpdatingLocation()
firstView!.showSecondScreen() // this is where I call the other class's function
}
}
}
Here's the code in my First View controller class
var firstView: FirstView? = nil
class FirstView: UIViewController {
var newUser = User()
override func viewDidAppear(animated: Bool) {
firstView = self.storyboard!.instantiateViewControllerWithIdentifier("firstView") as? FirstView
// here i take the first view controller class and store it into a public variable
}
#IBAction func allowLocationBtn(sender: AnyObject) {
newUser.getLocation() // this is the first button clicked
}
func showSecondScreen(){
let secondScreen = self.storyboard!.instantiateViewControllerWithIdentifier("SecondScreen") as! SecondScreen
self.presentViewController(secondScreen, animated: true, completion: nil )
}
Ps. I know I can combine all the code into one class, and it could all work like that. But I want to do it the "proper" way, have different functionalities in different classes.
try to set first = self , dont instantiate a new view-controller
if using storyboard, better to use segues to display another screen
Though the correct thing here would be to have a UserDelegate Protocol
protocol UserDelegate {
func displayOtherScreen( )
}
then in User add var delegate: UserDelegate?
and in the locationManager function instead of calling
firstView!.showSecondScreen call
delegate?.displayOtherScreen( )
Then make FirstView adopt the UserDelegate Protocol
class FirstView: UIViewController, UserDelegate {
var newUser = User()
override func viewDidAppear(animated: Bool) {
newUser.delegate = self
...
}
...
...
func displayOtherScreen( ) {
showSecondSceen( )
}
Now there is no need for the first variable..
Ideally the situation you are trying handle multiple MVC's. Your second Screen by itself should be a different View and should have its own Model View Controller. That way you are not breaking the rules of MVC architecture. Your ShowSecondScreen() function should be controlled by a SecondViewController.
Once you have this multiple MVC's defined you can wire them up.
There are some ways to do this - UISplitViewController, UITabViewController, UINavigationViewController. In your case, UINavigationViewController seems best option. You can add Navigation Controller to wireup MVC's using Editor -> Embed on your class FirstView. That would make FirstView controller as your rootViewController.
Then add a segue from your button on the FirstView to your controller for the SecondScreen i.e SecondViewController
class FirstView: UIViewController {
override func prepareForSegue(..) {
if let secondControllerObject = segue.destinationViewController as? SecondViewController {
if let identifier = segue.identifier {
switch identifier {
case "<your_buttons_identifier_string" : secondControllerObject .callToSecondScreen()
/* Now will move the Second View to the top of the MVC stack and you
will be also able to go back to first view as you have a
Navigation Controller hooked up.*/
/* callToSecondScreen - should be a controller object displaying the
controlling and displaying contents in the second screen's view.
Data for second screen's view will come from its Model */
}
Short explanation.
I have a ContainerViewController that I'm pushing to the navigationStack.
The ContainerViewController has 2 child ViewControllers. A SlidePanelViewController (a slide-out menu) and a CenterViewController (the content)
I have a button in my menu to "sign Out". When this button is clicked I want to push ContainerViewController (and it's 2 childViewControllers) to my LandingPageViewController.
Here's the function I am trying to call:
func signOut() {
println("signOut")
// Set up the landing page as the main viewcontroller again.
let mainTableViewController = LandingPageVC()
mainTableViewController.navigationItem.setHidesBackButton(true, animated: false)
mainTableViewController.skipView = false
self.navigationController!.pushViewController(mainTableViewController, animated: true)
// Disable menu access
menuEnabled = false
// change status bar style back to default (black)
UIApplication.sharedApplication().statusBarStyle = UIStatusBarStyle.Default
}
At first I tried putting this in my SlidePanelViewController. That didn't work. So I put it where I'm assuming it belongs in the ContainerViewController.
However when I click my signOutButton in my menu. I'm presented with the error:
fatal error: unexpectedly found nil while unwrapping an Optional value
When looking into the error. This is the line causing it:
self.navigationController!.pushViewController(mainTableViewController, animated: true)
After the error I checked that the function works, by adding a UINavigationBarButtonItem that called the function (in my ContainerViewController). It did exactly what I wanted.
However when I call this function from my Menu (again my menu is a childViewController of the ContainerViewController). It does not work.
I'm attempting to call it like so:
ContainerViewController().signOut()
I also tried adding a Delegate to my SidePanelViewController like this:
Before the class:
#objc protocol SidePanelViewControllerDelegate {
optional func needsSignOut(sender: SidePanelViewController)
optional func toggleLeftPanel()
optional func collapseSidePanels()
}
in viewDidLoad():
// Make sure your delegate is weak because if a ContainerViewController owns
// a reference to a SidePanelViewController and the container view controller
// is its delegate, you'll end up with a strong reference cycle!
weak var delegate: SidePanelViewControllerDelegate?
in my tap gesture function:
func signOutTapGesture() {
println("signOutTapGesture")
selectView(signOutView)
delegate?.needsSignOut?(self)
println(delegate)
}
before my ContainerViewController class:
var leftViewController: SidePanelViewController?
my ContainerViewController class:
class ContainerViewController: UIViewController, CenterViewControllerDelegate, SidePanelViewControllerDelegate, UIGestureRecognizerDelegate {
in my ContainerViewController's viewDidLoad()
leftViewController?.delegate = self
And I changed the signOut function in the ContainerViewController class to this:
func needsSignOut(sender: SidePanelViewController) {
println("needsSignOut called")
self.signOut()
}
However using the delegate like above, doesn't seem to do anything either.
Any help as to How I can successfully push my LandingPageVC from the menu would be greatly appreciated! (I'm not using storyboards)
You're attempting to call signOut with ContainerViewController().signOut(). This will create a new ContainerViewController and because you haven't pushed it onto the navigation controller's stack, navigationController is nil. Try just calling self.signOut(). (I'm assuming signOut in a method of ContainerViewController)
Update - delegates
Your delegate property should go in SidePanelViewController. I'll give you and example of how to implement it:
SidePanelViewController:
(Note - the protocol doesn't have to go here but I think it keeps things organised)
#objc protocol SidePanelViewControllerDelegate {
optional func needsSignOut(sender: SidePanelViewController)
}
class SidePanelViewController: UIViewController {
// Make sure your delegate is weak because if a ContainerViewController owns
// a reference to a SidePanelViewController and the container view controller
// is its delegate, you'll end up with a strong reference cycle!
weak var delegate: SidePanelViewControllerDelegate?
// Called when the UIButton is pressed.
func myButtonWasPressed() {
delegate?.needsSignOut?(self)
}
}
ContainerViewController:
class ContainerViewController: UIViewController {
var sidePanel: SidePanelViewController!
// Setup the side panel...
override func viewDidLoad() {
super.viewDidLoad()
sidePanel.delegate = self
}
func signOut() {
// Sign out stuff here.
}
}
// The ContainerViewController needs to conform to the SidePanelViewControllerDelegate
// protocol if we want the delegate to work. (This could have gone in the initial
// class declaration.)
extension ContainerViewController : SidePanelViewControllerDelegate {
func needsSignOut(sender: SidePanelViewController) {
self.signOut()
}
}
Hope that helps.
The problem seems to be that navigationController is nil and you're trying to force unwrap it (as indicated by your error).
One problem I discussed in my other answer.
Another problem may be you haven't added a navigation controller. To do this you need to:
If you're using Storyboards
You need to make sure you've embedded your UINavigationController. After that, when you use navigationController it won't be nil and you'll be able to push your view controller.
When you're on your storyboard:
Also, if you're using storyboards, have you considered using segues to move around instead of calling presentViewController? I've found it makes everything much easier.
If you're not using Storyboards
Have a look at this post: Programatically creating UINavigationController in iOS
I have four ViewController, I don't use an UITabbedbar because It's more difficult to customize.
I use modal segue but I think the memory consumption is excessive.
this is a screen shot of my first and second VC.
What I have to use to change View correctly?
That's the code I use :
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
if (segue.identifier == "second") {
let secondVC = segue.destinationViewController as SecondViewController;
}
}
From your Storyboard diagram, it is clear that you have created a segue from each button in your "tab bar" to another view controller. Except for the unwind segue, segues always create a new instance of the view controller they are switching to. So if you use your setup to switch from view controller 1 to view controller 2 and then back to view controller 1, you won't be returning to the view controller you came from but instead you will be creating an entirely new view controller 1.
This is why your memory consumption is excessive. You keep creating view controllers until your app crashes.
I would recommend you return to using a tab bar controller. They were designed to allocate the view controllers once up front and then just switch between them. Also, they have a standard look for a reason, it helps the user of your app know immediately how to interact with them.
To pass data between tabs, you won't use segues because there is no segue happening when you switch tabs. There are many ways you can do this, but they all boil down to having model data stored where all of the tabs can access it. This can be done with CoreData in a larger app. For a simple app, you can do the following:
Create a custom subclass of UITabBarController. Let's call it CustomTabBarController. Have that class create and hold the model data that will be accessed by each of your tabs.
CustomTabBarController.swift:
import UIKit
// This class holds the data for my model.
class ModelData {
var name = "Fred"
var age = 50
}
class CustomTabBarController: UITabBarController {
// Instantiate the one copy of the model data that will be accessed
// by all of the tabs.
var model = ModelData()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
}
In your Storyboard, in the Identity Inspector, change the class of UITabBarController to CustomTabBarController.
In viewWillAppear in each of your tabs, get a reference to the model data and then you can use it.
FirstViewController.swift:
import UIKit
class FirstViewController: UIViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Get a reference to the model data from the custom tab bar controller.
let model = (self.tabBarController as! CustomTabBarController).model
// Show that we can access and update the model data from the first tab.
// Let's just increase the age each time this tab appears and assign
// a random name.
model.age += 1
let names = ["Larry", "Curly", "Moe"]
model.name = names[Int(arc4random_uniform(UInt32(names.count)))]
}
}
SecondViewController.swift:
import UIKit
class SecondViewController: UIViewController {
#IBOutlet weak var nameLabel: UILabel!
#IBOutlet weak var ageLabel: UILabel!
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Get a reference to the model data from the custom tab bar controller.
let model = (self.tabBarController as! CustomTabBarController).model
// This tab will simply access the data and display it when the view
// appears.
nameLabel.text = model.name
ageLabel.text = "\(model.age)"
}
}