Is it possible to animate views of a parent VC in Swift?
I've got a root/master VC with a UIView which I'm using as a sort of a UITabBarController, so the rest of my 4 main VCs are children of the root.
On some of the child VCs, I have subviews that should take up the whole screen, without seeing the custom tab bar (UIView) from the root VC, but it still floats above.
I would like to have it slide off the screen via Y axis whenever I open the fullscreen subviews, but I can't seem to access or manipulate the root VCs properties as it returns nil on runtime.
Here's the custom tab bar root VC so you can understand the structure of the code:
class RootVC: UIViewController {
//This is where we pull all of our content from other VCs
//when a tab bar button is selected
#IBOutlet weak var contentView: UIView!
//The custom tab bar itself with an array of button outlets
#IBOutlet public weak var customTabBarContainer: UIView!
#IBOutlet var tabBarButtons: [UIButton]!
//4 main view VCs that are reflected in the tab bar
public var mapVC: UIViewController!
public var favoritesVC: UIViewController!
public var chatVC: UIViewController!
public var profileVC: UIViewController!
//Array for the VCs above
public var viewControllers: [UIViewController]!
//Index of the selected button determend by their tags
public var selectedIndex: Int = 0
#IBOutlet weak var loadingLogo: UIImageView!
override public func viewDidLoad() {
//Populating viewControllers array with
//initiated VCs in Main storyboard
let storyboard = UIStoryboard(name: "Main", bundle: nil)
mapVC = storyboard.instantiateViewController(withIdentifier: "MapVC")
favoritesVC = storyboard.instantiateViewController(withIdentifier: "FavoritesVC")
chatVC = storyboard.instantiateViewController(withIdentifier: "ChatVC")
profileVC = storyboard.instantiateViewController(withIdentifier: "ProfileVC")
viewControllers = [mapVC, favoritesVC, chatVC, profileVC]
//Custom tab bar + buttons visual properties
customTabBarContainer.layer.cornerRadius = customTabBarContainer.frame.height / 2
customTabBarContainer.layer.shadowColor = UIColor.darkGray.cgColor
customTabBarContainer.layer.shadowOffset = CGSize.zero
customTabBarContainer.layer.shadowRadius = 10
customTabBarContainer.layer.shadowOpacity = 0.9
tabBarButtons[0].imageView?.contentMode = .scaleAspectFit
tabBarButtons[1].imageView?.contentMode = .scaleAspectFit
tabBarButtons[2].imageView?.contentMode = .scaleAspectFit
tabBarButtons[3].imageView?.contentMode = .scaleAspectFit
}
override public func viewDidAppear(_ animated: Bool) {
loadingLogo.popOut()
//Loads the initial VC
contentView.addSubview(mapVC.view)
mapVC.view.frame = self.view.frame
mapVC.didMove(toParentViewController: self)
customTabBarContainer.isHidden = false
//Selects the inital home button
tabBarButtons[0].isSelected = true
}
#IBAction func didTabButton(_ sender: UIButton) {
//Keeps a track of which bar button is selected
let previousIndex = selectedIndex
selectedIndex = sender.tag
//Deselects the previous bar button
tabBarButtons[previousIndex].isSelected = false
//Removes the previous VC
let previousVC = viewControllers[previousIndex]
previousVC.view.removeFromSuperview()
previousVC.removeFromParentViewController()
print("switced to \(viewControllers[selectedIndex])")
//Selects the tapped bar button
tabBarButtons[selectedIndex].isSelected = true
tabBarButtons[selectedIndex].popIn()
//Brings up the selected VC
let nextVC = viewControllers[selectedIndex]
contentView.addSubview(nextVC.view)
nextVC.view.frame = self.view.frame
nextVC.didMove(toParentViewController: self)
}
}
And here's the code I'm trying to use to manipulate the customTabBarContainer from a child of the MapVC:
UIView.animate(withDuration: 0.4, animations: {
let root = self.parent?.parent as! RootVC
root.customTabBarContainer.frame.origin.y -= root.customTabBarContainer.frame.height
}, completion: nil)
why are you trying to access the parent of the parent then?
self.parent?.parent as RootVC
assuming you are using an extension like this one to find your parentVC:
extension UIView {
var parentViewController: UIViewController? {
var parentResponder: UIResponder? = self
while parentResponder != nil {
parentResponder = parentResponder!.next
if parentResponder is UIViewController {
return parentResponder as! UIViewController!
}
}
return nil
}
}
you should be able to access parent via
let root = self.parentViewController as! RootVC
I've figured out an answer, just in case anyone else encounters a similar problem. It will not take you to the VCs immediate parent, but instead, to its most distant ancestor, which solves my particular problem in this case.
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let rootVC = appDelegate.window?.rootViewController as! RootVC
rootVC.customTabBarContainer.isHidden = true
Related
I'm basically trying to create a custom UITabBarController since I need some specific functionality. The TabBar itself is done and working, but I don't quite know how to display ViewControllers in this CustomTabBarViewController itself.
Assuming i have the following method:
func tabSelected(_ index: Int) {}
and knowing the height of my TabBar through tabbar.frame.size, how do I instantiate two ViewControllers above the TabBar and switch between them when the tabSelected method is called? A transition animation would be even nicer, but not really necessary.
NOTE: my TabBar doesn't inherit from UITabBarController, only from the regular UIViewController, to avoid further confusion.
Here I created sample project:
CustomTabBarViewController
You should have container view for child ViewControllers
Then you should have array with embed ViewControllers
You should call method in
CustomTabBarViewController which change ViewController inside
container view to ViewController from array of VCs at index which you pass as parameter of this method
Start with declaring outlet collection for your TabBar buttons and also get reference for container view where your ViewControllers will be showed
#IBOutlet var tabBarButtons: [UIButton]!
#IBOutlet weak var container: UIView!
then create array for your tab bar items
var items: [UIViewController]?
next create lazy variables for your controllers
private lazy var aVC: A = {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
return storyboard.instantiateViewController(withIdentifier: "a") as! A
}()
private lazy var bVC: B = {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
return storyboard.instantiateViewController(withIdentifier: "b") as! B
}()
.... this can be simplified by creating method which returns ViewController depending on VC’s identifier
After that append ViewControllers to your items array and also each add as child of your TabBarViewController
override func viewDidLoad() {
super.viewDidLoad()
items = [aVC, bVC]
items!.forEach { addChild($0) }
}
continue with declaring method for setting ViewController
private func setViewController(_ viewController: UIViewController) {
items!.forEach { $0.view.removeFromSuperview(); $0.willMove(toParent: nil) }
container.addSubview(viewController.view)
viewController.view.frame = container.bounds
viewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
viewController.didMove(toParent: self)
}
now add action for your tab bar buttons and get index of button. Then with this index call your tabSelected method
#IBAction func buttonPressed(_ sender: UIButton) {
if let index = tabBarButtons.index(of: sender) {
tabSelected(index)
}
}
inside tabSelected set VC from items depending on index of sender tab bar button
func tabSelected(_ index: Int) {
if let item = items?[index] {
setViewController(item)
}
}
finally in viewDidLoad set first item
override func viewDidLoad() {
...
tabSelected(0)
}
Now you can fully customize your ViewController and make other epic stuff which you know from UITabBarController
Here's another approach:
1. In your CustomTabBarViewController define an array to hold the ViewControllers:
var viewControllers: [UIViewController]
Instantiate the view controllers and add them to the array:
// If you're not using storyboard:
let homeViewController = HomeViewController()
// If using storyboard:
let searchViewController = storyboard.instantiateViewController(withIdentifier: "SearchViewController")
viewControllers = [homeViewController, searchViewController, ...]
2. Define a variable to keep track of the tab button that is selected:
var selectedIndex: Int = 0
3. Implement your tabSelected method like so. I've explained each line in code:
func tabSelected(_ index: Int) {
let previousIndex = selectedIndex
selectedIndex = index
// Use previousIndex to access the previous ViewController from the viewControllers array.
let previousVC = viewControllers[previousIndex]
// Remove the previous ViewController
previousVC.willMove(toParentViewController: nil)
previousVC.view.removeFromSuperview()
previousVC.removeFromParentViewController()
// Use the selectedIndex to access the current ViewController from the viewControllers array.
let vc = viewControllers[selectedIndex]
// Add the new ViewController (Calls the viewWillAppear method of the ViewController you are adding)
addChildViewController(vc)
vc.view.frame = contentView.bounds
// contentView is the main view above your tab buttons
contentView.addSubview(vc.view)
// Call the viewDidAppear method of the ViewController you are adding using didMove(toParentViewController: self)
vc.didMove(toParentViewController: self)
}
I'm trying to build an app with layout similar to Apple Music - a tab bar navigation with a persistent view, accessible from everywhere in the app. The view can be expanded to take up the whole screen or minimised with a static height of 80. The UI is built in a storyboard with a normal UITabBarController. Here's a first draft:
This is how I've build it:
class TabbarViewController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
embedLiveFeedbackController()
}
private func embedLiveFeedbackController() {
guard let feedbackController = UIStoryboard(name: "LiveFeedback", bundle: nil).instantiateInitialViewController() as? LiveFeedbackViewController else { return }
feedbackController.stateDelegate = self
addChildViewController(feedbackController)
view.addSubview(feedbackController.view)
feedbackController.view.translatesAutoresizingMaskIntoConstraints = false
feedbackController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
feedbackController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
feedbackController.view.bottomAnchor.constraint(equalTo: tabBar.topAnchor).isActive = true
liveFeedbackTopConstraint = feedbackController.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor)
liveFeedbackHeightConstraint = feedbackController.view.heightAnchor.constraint(equalToConstant: Constants.minimizedHeight)
liveFeedbackHeightConstraint?.isActive = true
liveFeedbackTopConstraint?.isActive = false
}
}
The problem I have is that the content of the view controllers goes behind the persistent view and is not completely visible. One of the things I've tried is to constraint the view controllers to the top of the persistent view:
private func constraintViewControllers() {
guard let vcs = viewControllers else { return }
guard let topAnchor = liveFeedbackTopAnchor else { return } // a reference to the top anchor of the persistent view
for viewController in vcs {
viewController.view.translatesAutoresizingMaskIntoConstraints = false
viewController.view.bottomAnchor.constraint(equalTo: topAnchor).isActive = true
}
}
Of course, I get the following error:
Unable to activate constraint with anchors <NSLayoutYAxisAnchor:0x600000864640 "UILayoutContainerView:0x7fc813f0ea60.bottom"> and <NSLayoutYAxisAnchor:0x60400047b980 "UIView:0x7fc813f08af0.bottom"> because they have no common ancestor. Does the constraint or its anchors reference items in different view hierarchies? That's illegal.'
Any suggestions how to go about implementing this?
Here is my suggestion, I tested it and worked fine
create a main viewController and put 2 view inside it (as in picture) :
-startViewController
--containerView (your app root viewController/tabBarController goes here)
--persistentView
in startViewController
class ViewController: UIViewController {
var tabController : TabController!
#IBOutlet weak var containerView: UIView!
#IBOutlet weak var persistentView: UIView!
#IBOutlet weak var persistentBottomConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
tabController = TabController.initFromStoryBord()
self.addChild(tabController)
tabController.view.frame = containerView.bounds
containerView.addSubview(tabController.view)
tabController.view.autoresizingMask = [.flexibleHeight, .flexibleWidth]
persistantBottomConstraint.constant = tabController.tabBar.frame.height
}
}
and then a class for TabBarController:
make sure to set Stroyboard ID for tabController
class TabController : UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
}
static func initFromStoryBord() -> TabController {
let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "TabController") as! TabController
return vc
}
}
On my storyboard I have main ViewController, not TabBarViewController, which consist of TabBar on the bottom, view on the top and ContainerView on the middle. ContainerView have a NavigationController. I also have 4 ViewControllers, one of them - RootViewController of NavigationController. I wish to show one of ViewControllers when I selecting TabBarItem, and in future I will add slide menu, which also will show selected ViewController.
I have next code, which only shows initial ViewController inside ContainerView, and when I selecting TabBarItems, new ViewControllers don't showing and I see only first View Controller. What goes wrong?
class ViewController: UIViewController {
#IBOutlet weak var container: UIView!
#IBOutlet weak var first: UITabBarItem!
#IBOutlet weak var second: UITabBarItem!
#IBOutlet weak var third: UITabBarItem!
#IBOutlet weak var fours: UITabBarItem!
#IBOutlet weak var tabBar: UITabBar!
var firstVC: FirstViewController?
var secondVC: SecondViewController?
var thirdVC: ThirdViewController?
var foursVC: FoursViewController?
var navi: UINavigationController?
override func viewDidLoad() {
super.viewDidLoad()
tabBar.delegate = self
initialSetup()
}
func initialSetup() {
tabBar.selectedItem = tabBar.items?.first
navi = self.storyboard?.instantiateViewController(withIdentifier: "containerNavi") as? UINavigationController
firstVC = self.storyboard?.instantiateViewController(withIdentifier: "FirstViewController") as? FirstViewController
secondVC = self.storyboard?.instantiateViewController(withIdentifier: "SecondViewController") as? SecondViewController
thirdVC = self.storyboard?.instantiateViewController(withIdentifier: "ThirdViewController") as? ThirdViewController
foursVC = self.storyboard?.instantiateViewController(withIdentifier: "FoursViewController") as? FoursViewController
}
func showVC(number: Int) {
switch number {
case 0:
navi?.popToRootViewController(animated: true)
print("0")
case 1:
if let second = secondVC {
navi?.pushViewController(second, animated: true)
}
print("1")
case 2:
if let third = thirdVC {
navi?.pushViewController(third, animated: true)
}
print("2")
case 3:
if let fours = foursVC {
navi?.pushViewController(fours, animated: true)
}
print("3")
default:
return
}
}
}
extension ViewController: UITabBarDelegate {
func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
showVC(number: item.tag)
}
}
Storyboard screenshot:
You can try to use this extesion to add/remove any of the 4 to containerView
extension UIViewController {
func add(_ child: UIViewController, frame: CGRect? = nil) {
addChildViewController(child)
if let frame = frame {
child.view.frame = frame
}
view.addSubview(child.view)
child.didMove(toParentViewController: self)
}
func remove() {
willMove(toParentViewController: nil)
view.removeFromSuperview()
removeFromParentViewController()
}
}
// use it like this
let vc = self.storyboard?.instantiateViewController(withIdentifier: "first")
self.add(vc, frame: self.containerView.frame)
to remove
vc.remove()
I have been searching for how the delegate works and I tried to do it in my project. Unfortunately, the delegate method I implement does not get called ever. I am trying to do a slide-out navigation panel. so what I did is that I put two uicontainerviews, one is for slide-out navigation panel and the other for main view controller
enter image description here
The code is that
For main view controller
protocol MainViewControllerDelegate {
func toggleSideMenu()
}
class MainViewController: UIViewController {
var delegate: MainViewControllerDelegate?
override func viewDidLoad() {
super.viewDidLoad()
}
// MARK: - Slide Action
#IBAction func slideMenuTapped(_ sender: UIBarButtonItem){
delegate?.toggleSideMenu()
print("Slide Menu has been tapped")
}
}
For container view controller
class ContainerVC: UIViewController {
#IBOutlet weak var SideMenuConstraint: NSLayoutConstraint!
#IBOutlet weak var slideMenuContainer: UIView!
#IBOutlet weak var mainViewContainer: UIView!
var mainViewController: MainViewController?
var isSideMenuOpened = false
override func viewDidLoad() {
super.viewDidLoad()
mainViewController = UIStoryboard.mainViewController()
mainViewController?.delegate = self
}
}
extension ContainerVC: MainViewControllerDelegate{
func toggleSideMenu() {
print("It works")
if isSideMenuOpened{
isSideMenuOpened = false
SideMenuConstraint.constant = -260
mainViewContainer.layer.shadowOpacity = 0
} else {
isSideMenuOpened = true
SideMenuConstraint.constant = 0
mainViewContainer.layer.shadowOpacity = 0.59
}
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()
}
}
}
extension UIStoryboard{
static func mainStoryboard() -> UIStoryboard { return UIStoryboard(name: "Main", bundle: Bundle.main) }
static func mainViewController() -> MainViewController? {
return mainStoryboard().instantiateViewController(withIdentifier: "MainViewController") as? MainViewController
}
}
Please let know what's wrong
I think the reason is that you embed your main view controller in navigation controller :
let navigationController = self.childViewControllers.last as! UINavigationController
let mainViewController = navigationController.topViewController as! MainViewController
mainViewController?.delegate = self
Here is where you got wrong:
mainViewController = UIStoryboard.mainViewController()
mainViewController?.delegate = self
this mainViewController is not the same as the child of the container view controller, so setting its delegate doesn't really do anything.
You need to first get the VC that is the child of the container view controller:
mainViewController = self.childViewControllers.last as! MainViewController
mainViewController.delegate = self
I want to add a swipe action to my app. Basically I have 5 view controllers and I main view controller. On my main view controller I have a view and I am calling the content from other 5 view controllers to that view. And I want to swipe those 5 view controllers.
My code:
import UIKit
class TabViewController: UIViewController {
#IBOutlet var contentView: UIView!
#IBOutlet var buttons: [UIButton]!
#IBOutlet var backgroundView: UIImageView!
var movingView = UIView()
var rifleViewController: UIViewController!
var pistolViewController: UIViewController!
var shotgunViewController: UIViewController!
var smgsViewController: UIViewController!
var sniperViewController: UIViewController!
var viewControllers: [UIViewController]!
var selectedIndex: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
let storyboard = UIStoryboard(name: "Main", bundle: nil)
rifleViewController = storyboard.instantiateViewController(withIdentifier: "rifles")
sniperViewController = storyboard.instantiateViewController(withIdentifier: "snipers")
smgsViewController = storyboard.instantiateViewController(withIdentifier: "smgss")
shotgunViewController = storyboard.instantiateViewController(withIdentifier: "shotguns")
pistolViewController = storyboard.instantiateViewController(withIdentifier: "pistols")
viewControllers = [rifleViewController,
pistolViewController,
shotgunViewController,
smgsViewController,
sniperViewController]
buttons[selectedIndex].isSelected = true
didPressTab(buttons[selectedIndex])
let screenWidth = UIScreen.main.bounds.width
movingView = UIView(frame: CGRect(x: 0, y: 80, width: screenWidth / 5, height: 5))
movingView.backgroundColor = UIColor.white
backgroundView.addSubview(movingView)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func didPressTab(_ sender: UIButton) {
let previousIndex = selectedIndex
selectedIndex = sender.tag
buttons[previousIndex].isSelected = false
let previousVC = viewControllers[previousIndex]
previousVC.willMove(toParentViewController: nil)
previousVC.view.removeFromSuperview()
previousVC.removeFromParentViewController()
sender.isSelected = true
let vc = viewControllers[selectedIndex]
addChildViewController(vc)
vc.view.frame = contentView.bounds
contentView.addSubview(vc.view)
vc.didMove(toParentViewController: self)
let newx = sender.frame.origin.x
UIView.animate(withDuration: 0.2) {
self.movingView.frame.origin.x = newx
}
}
}
You must go for UIPageViewController.
With the setViewControllers(_:direction:animated:completion:) you can set an array of your view controllers and also you can customise the animation.
Add on all of your viewController two Swipe Gesture Recognizer. Take care that you really drag and drop them onto the View Controllerin the hierarchy because else it might not work (For the most left and most right view controller you just have to add one swipe gesture recognizer). After that change for one of the the Swipe Gesture Recognizerper view controller the Swipe option in the attribute inspector from the standard Left to Right.
Create from each view controller to the next a Showseague and one back. Give them an Identifier you can remember. Then write something like that into each ViewController.swift and link the #IBAction with the two Swipe Gesture Recognizer of each corresponding view controller:
#IBAction func didSwipe(_ sender: UISwipeGestureRecognizer) {
if sender.direction == UISwipeGestureRecognizerDirection.left {
performSegue(withIdentifier: "identifierOfSegue", sender: nil)
}
if sender.direction == UISwipeGestureRecognizerDirection.right {
performSegue(withIdentifier: "identifierOfOtheSegue", sender: nil)
}
}
I don't know if I understood your question, but you can add (from Interface Builder) a UIGestureRecognizer (for the swipe action) to the View and in the selector of that gesture, you present the View you'd like to show.