Use of UITabBarController within a UIVisualEffectView programmatically - ios

I'm currently learning about Swift and UIKit as I'm trying to make a tvOS app.
I am trying to stay away from Storyboards and so I'm doing this with just code.
I think this is more of a code design question though.
Let's say I have an OverlayVC class, derived from UIViewController (It's a VC which is presented modally on top of some existing context). That class has a redefined loadView method where I instantiate my own OverlayView class, which derives from UIView. I'm trying to strictly separate the View from the View controller.
Now, in that OverlayView constructor, I instantiate a UIVisualEffectView, and set the constraints so that it takes the entire frame. The goal is to make something appear on top of the existing context with a blur effect.
So far so good, when the OverlayView is being presented, I get everything blurry.
Now, I want to have a tab bar controller inside this, so I want to make a class that derives from UITabBarController and instantiate it in my OverlayView.
But I need to give a View to the UIVisualEffectView through its contentView attribute.
The easy way for me to do this would be to instantiate my TabBarVC within the OverlayView class. But I feel like it doesn't belong there, but should instead be held within the OverlayVC class. Problem is that the UIVisualEffectView is instantiated in the OverlayView, not the VC, so setting its contentView in the VC isn't straightforward.
I've been turning this around in my head a few times and can't actually figure out what is the proper class design for this.
Any suggestion would be appreciated. Thanks !
Below is a simplified snippet of how I have done this right now:
class TabBarViewController: UITabBarController {
convenience init() {
self.init(nibName: nil, bundle: nil)
let vc1 = ViewController1()
let vc2 = ViewController2()
self.viewControllers = [vc1, vc2]
}
}
class OverlayView: UIView {
let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .regular))
let dimmingView = UIView()
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = .clear
self.clearsContextBeforeDrawing = false
self.isOpaque = false
blurView.translatesAutoresizingMaskIntoConstraints = false
addSubview(blurView)
dimmingView.backgroundColor = .black
dimmingView.alpha = 0.75
dimmingView.translatesAutoresizingMaskIntoConstraints = false
addSubview(dimmingView)
NSLayoutConstraint.activate([
blurView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
blurView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
blurView.topAnchor.constraint(equalTo: self.topAnchor),
blurView.heightAnchor.constraint(equalToConstant: 750),
])
NSLayoutConstraint.activate([
dimmingView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
dimmingView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
dimmingView.topAnchor.constraint(equalTo: blurView.bottomAnchor),
dimmingView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class OverlayViewController: UIViewController {
convenience init() {
self.init(nibName: nil, bundle: nil)
self.modalPresentationStyle = .overCurrentContext
}
override func viewDidLoad() {
super.viewDidLoad()
let tabBarController = TabBarViewController()
addChild(tabBarController)
tabBarController.view.translatesAutoresizingMaskIntoConstraints = false
(view as! OverlayView).blurView.contentView.addSubview(tabBarController.view)
NSLayoutConstraint.activate([
tabBarController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tabBarController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tabBarController.view.topAnchor.constraint(equalTo: view.topAnchor),
tabBarController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
tabBarController.didMove(toParent: self)
}
override func loadView() {
view = OverlayView(frame: UIScreen.main.bounds)
}
}
So, here I have a clear split between the Overlay view and its View controller. The OverlayView class contains everything related to the content of the view and all the constraints for Auto Layout, which makes the OverlayViewController class free from any stuff related to the view itself, except for one thing: the constraints for the tab bar controller, which I can only set in the OverlayViewController because the OverlayViewController instantiates the TabBarController, and so I don't have access to the TabBarController view in the constructor of the OverlayView class. This is basically what annoys me here, as the constraints should, strictly speaking, be a View thing, not a ViewController thing, yet I can only set those in the ViewController, unless I've missed something.

Related

ViewController is blank after navigating away and returning

I'm building an app where two view controllers share a UIView subclass as the main source of UI. It works perfectly when the app is starting, but if I navigate away from the initial view, and return to it, all of the UI is lost. What do I need to do to preserve the views UI post-navigation?
My app flow is: MainView -> TableView -> DetailView
Just going from Main to Table to Main itself makes the UI vanish.
(rank isn't 10 yet, so here's a link to view: https://gfycat.com/enormousanchoredindochinesetiger)
What I do is load the UI in the UIView class through layoutSubviews, and in the UIViewControllers I set the instantiate the class, UI in the loadViews method by saying view = viewClass. I've tried adding this (view = viewClass) to viewWillAppear() as well, but it does nothing.
I've also tried creating two unique view classes in case instantiating was a problem. It didn't change anything.
ViewController:
override func loadView() {
super.loadView()
view = baseView
view.backgroundColor = .white
self.navigationController?.isNavigationBarHidden = true
requestLaunchData()
setButtonTargets()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.isNavigationBarHidden = true
view = baseView
}
//How I push to the next view
#objc func upcomingButtonTapped() {
let vc = TableViewController()
navigationController?.pushViewController(vc, animated: true)
vc.upcomingLaunches = upcomingLaunches
}
UIView:
class BaseView: UIView {
//Lots of labels and buttons instantiated
override func layoutSubviews() {
super.layoutSubviews()
setUI() //adding subviews
}
//Layout configurations
}
Before it was this structure, I had all the UI (labels, buttons, a map) directly created and configured in each ViewController, which made both massive. But, it also worked.
I solved it after a night's rest.
So here's how you need to use a custom UIView class as your ViewController's view:
class YourView: UIView {
//Create your properties, views, etc.
let exampleView = UIView()
override layoutSubviews(){
super.layoutSubviews()
addSubview(exampleView)
//Add layouts, etc.
}
And then in your ViewController, in either viewDidLoad, or loadViews (like me here):
let customView = YourView()
override func loadView() {
super.loadView()
view = customView //Sets the entire view to all the UI you created in the custom class
}
The FATAL mistake I made was this:
override layoutSubviews(){
super.layoutSubviews()
if let sView = superview { //This gives you frame size and such
sView.addSubview(exampleView)
}
}
This sets the UI's memory to the SuperView, which gets lost the moment you leave that view, because of view = customView. So my controller was rendering view = customView, which was empty, because all the UI was set to the superView which was superseded by customView.
I hope that helps anyone trying to use this same architecture in the future.

lagging when push uiviewcontroller

When I use storyboard segue, it is pretty smooth showing another viewcontroller onscreen. However, when I'm not using storyboard, just add a simple line of code with navigationController?.pushViewController(UIViewController(), animated: true), it's a little bit lagging in transition.
Also I read about Delay when pushing view controller (iOS). But even when I'm pushing an brand new viewcontroller (no extra code inside), the transition is still a little bit lagging, any idea?
Swift 5.3, Xcode 12
I was facing this same issue, tried adding to main thread, but apparently the problem was I was pushing the viewController created programatically, and it backgroundColor was nil.
Just by setting the color in pushedViewController to something else, solved the issue.
self.view.backgroundColor = yourColor
In app delegate, set your window's background color to red.
window?.backgroundColor = .red
Also in the the pushed view controller, set its view to red.
view.backgroundColor = .red
I experienced the same issue when programmatically embedding my view controller in a UINavigationController.
I came across the same issue when I was drawing UI programmatically.
In my case, overriding loadView function solved my problem.
import UIKit
class MyViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.title = "My tasks"
}
override func loadView() {
// You can load your view here. For simplicity, I just give it a background color
let v = UIView()
v.backgroundColor = UIColor.orange
view = v // This line does the magic
}
}
Viewcontroller is expecting to render view so add below view background color or set title
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.title = "Title"
self.view.backgroundColor = .red
}
I debug with slow animation on simulator. The "lagging" is the underlying previous view controller’s view as #yesleon mentioned. I set the previous view's alpha to 0 when viewWillDisappear, and 1 when viewWillAppear. It seems much better when pushing to the new controller now, but when pushing back to original view controller, it still a little bit not perfect, any better solution?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.tabBarController?.tabBar.isHidden = false
view.alpha = 1.0
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.tabBarController?.tabBar.isHidden = true
view.alpha = 0.0
}
#IBAction func barbuttonDidTapped(_ sender: UIBarButtonItem) {
navigationController?.pushViewController(UIViewController(), animated: true)
}
I met the same issue, put ui code and its constraints inside viewDidLoad into loadView() fix it. Now the animation is moving smoothly.
Refer here: https://stackoverflow.com/a/3424052/10158398
loadView is the method that actually sets up your view (sets up all
the outlets, including self.view).
viewDidLoad you can figure out by its name. It's a delegate method
called after the view has been loaded (all the outlets have been set)
that just notifies the controller that it can now start using the
outlets.
viewDidLoad: "This method is called after the view controller has
loaded its associated views into memory. This method is called
regardless of whether the views were stored in a nib file or created
programmatically in the loadView method."
loadView: "If you create your views manually, you must override this
method and use it to create your views."
For an example:
class DetailViewController: UIViewController {
let imageView = UIImageView()
var picture: Picture?
override func viewDidLoad() {
super.viewDidLoad()
}
override func loadView() {
view = UIView()
view.backgroundColor = .white
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFit
view.addSubview(imageView)
NSLayoutConstraint.activate([
imageView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
imageView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
imageView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
if let picture = picture {
let path = getDocumentDirectory().appendingPathComponent(picture.picture)
imageView.image = UIImage(contentsOfFile: path.path)
}
}

Creating Camera view as overlay of another view?

I have a QR code scanner viewcontroller, rather than pushing it in a navigation controller i wondered if it was possible to instantiate the view controller to be an overlay on my previous main screen taking up a quarter of the screen, as i dont need it to be a whole separate screen.
It has its own viewcontroller and view, i just need it to overlay at a smaller size.
No code to provide as this is more of a theoretical question
I assume you want to programmatically add a container in which a second UIViewController can be added in in your current UIViewController. See the example on how to achieve this.
import UIKit
class ParentViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Set a yellow background color on the parentViewController
self.view.backgroundColor = .yellow
// Create our detailViewController which will be contained in the parent
let detailViewController = DetailViewController()
// Add this detailViewController as a child in the current ParentViewController
addChildViewController(detailViewController)
// Add the detailViewController view as a subview on the ParentViewController
view.addSubview(detailViewController.view)
// Since we dont use IB we disable this property to allow programmatic constraints
detailViewController.view.translatesAutoresizingMaskIntoConstraints = false
// We create the constraints for our detailViewControllers view
NSLayoutConstraint.activate([
detailViewController.view.centerYAnchor.constraint(equalTo: view.centerYAnchor),
detailViewController.view.centerXAnchor.constraint(equalTo: view.centerXAnchor),
detailViewController.view.heightAnchor.constraint(equalToConstant: 200),
detailViewController.view.widthAnchor.constraint(equalToConstant: 200)
])
}
}
class DetailViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .green
print("DetailViewController loaded!")
}
}
We have a ParentViewController with a simple yellow background. In this controller we add a green DetailViewController as a container. The result is:
When you run this app you'll notice that the console prints the result from the DetailViewController.
If you want to remove the DetailViewController:
// Call this in your ParentViewController
let vc = self.childViewControllers.last
vc?.view.removeFromSuperview()
vc?.removeFromParentViewController()

hidesBottomBarWhenPushed makes UITabBar "jump"

I have an app with a tabbar and a navbar.
I have a BaseVC and a DetailVC. I'm pushing DetailVC from BaseVC. I want the tabbar to be under the pushed VC DetailVC. I'm using hidesBottomBarWhenPushed to achieve it. It works great, but for some reason while it's animating the push the tabbar is still visible and just when the animation ends the tabbar is hidden. I want it to be under the pushed VC in the animation too.
My code is:
self.hidesBottomBarWhenPushed = true
self.navigationController?.pushViewController(detailVC, animated: true)
self.hidesBottomBarWhenPushed = false
And the result (the bug) is this:
Anyone has an idea why the tabbar "jumps"? Thank you!
Having looked at the project in question I have found one way to make it work:
Remove the viewWillLayoutSubviews from the TabBarViewController so that it is not determining the height of the tab bar anymore and thus not stopping the animation working correctly.
Create a new swift file called MyTabBar (or whatever you want) and put this in it:
import UIKit
class MyTabBar: UITabBar {
var tabBarHeight: CGFloat = 100
override func sizeThatFits(_ size: CGSize) -> CGSize {
let superSize = super.sizeThatFits(size)
return CGSize(width: superSize.width, height: self.tabBarHeight)
}
}
Create a storyboard called TabBarStoryboard (or whatever). It's not going to be used for anything other then to hold a UITabBarController which you later create.
In the storyboard set the class type of the UITabBarController to your class of TabBarViewController so it gets the correct class when instantiated.
In the storyboard set the class type of the UITabBar that belongs to the UITabBarController to MyTabBar so that it too is the correct class when instantiated.
In your RootViewController replace this:
fileprivate let tabBarViewController = TabBarViewController()
with this:
fileprivate lazy var tabBarViewController: TabBarViewController = {
let storyboard = UIStoryboard(name: "TabBarStoryboard", bundle: nil)
return storyboard.instantiateViewController(withIdentifier: "MyTabBarController") as! TabBarViewController
}()
In your TabBarViewController add this to the end of the viewDidLoad to set the height of the tab bar:
if let tabBar = self.tabBar as? MyTabBar {
tabBar.tabBarHeight = self.tabBarHeight
}
Now if you get all that correct you should have a tab bar the size you want and the animation should work correctly because the height of tab bar is not longer controlled by the viewDidLayoutSubviews method.
I had to use a storyboard to hold the basic UITabBarController because I couldn't find a way to set the class of its UITabBar property otherwise (if anyone knows a way add a comment.
In case this is difficult to follow I have uploaded my version of your project to dropbox and this is the link: PlayWiz-NewVersion.zip. Be careful as it will unzip to the same directory structure so extract it to a different folder than the original otherwise you will lose the original.
That method appears to work correctly for me and I see no reason for there to be any problem but test it thoroughly first.
I have a simpler variation of the above example (cheers by the way)
I pasted everything in viewDidLoad, but you can write it prettier.
class TabBarController: UITabBarController {
override func viewDidLoad() {
// create the normal buttons (controllers)
let viewControllers = [UINavigationController(rootViewController: firstButton), UINavigationController(rootViewController: secontButton)]
self.viewControllers = viewControllers
// create the middle rounded button
self.tabBar.addSubview(addItemButton)
// setup constraints
addItemButton.widthAnchor.constraint(equalToConstant: 64).isActive = true
addItemButton.heightAnchor.constraint(equalToConstant: 64).isActive = true
tabBar.centerXAnchor.constraint(equalTo: self.addItemButton.centerXAnchor).isActive = true
tabBar.topAnchor.constraint(equalTo: self.addItemButton.centerYAnchor, constant: -8).isActive = true
}
extension UITabBar {
// fix clicking the (+) external to the tabbar bounds
override open func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if (!self.clipsToBounds && !self.isHidden && self.alpha > 0.0) {
let subviews = self.subviews.reversed()
for member in subviews {
let subPoint = member.convert(point, from: self)
if let result:UIView = member.hitTest(subPoint, with:event) {
return result;
}
}
}
return super.hitTest(point, with: event)
}
// this fixes the "jumping" tabBar when using the "hidesBottomBarWhenPushed = true"
override open func sizeThatFits(_ size: CGSize) -> CGSize {
let superSize = super.sizeThatFits(size)
return CGSize(width: superSize.width, height: 85)
}
}
Now, just call hidesBottomBarWhenPushed = true and push the desired view controller.

How to implement interactive cell swiping to trigger transitioning animation like WhatsApp

I'm looking for how to implement like WhatsApp cell swiping, I already have implemented the cell swiping animation using UIPanGestureRecognizer, the only left is performing the interactive animation -adding the new UIViewController to the window and showing it based on the gesture recognizer velocity and X-axis value-.
Some additional note to be accurate on what I want to achieve:
I have a UITableViewController, which has custom UITableViewCells in it. I want to be able to drag a cell from left to right to start the interactive animations. (Note: I already have implemented the cell swiping).
The new UIViewController will be pushed from left right.
While swiping the cell, the UITableViewController's view will be moving to the right, at that point, I want to show the pushing UIViewController beside it.
Here's a GIF for more details on what I need (The GIF is swiping the cell from right to left, I need the opposite):
I suggest using SWRevealViewController. It is very easy to set up using their guide and it looks absolutely great. I have found that it even works better when you pre-load the UIViewController that you use to be what is shown underneath.
It adds a great user experience for the functionality you are looking for.
It can also be user interactive if you wish to opt-in to that functionality. I have not used the interactive feature but it is very easy to get up and running with just a few lines of code:
let storyboard = UIStoryboard(name: "Main", bundle: .main)
let mainVC = storyboard.instantiateInitialViewController()
let menuStoryboard = UIStoryboard(name: "Menu", bundle: sdkBundle)
let menuNav = menuStoryboard.instantiateInitialViewController() as! UINavigationController
let mainRevealVC = SWRevealViewController(rearViewController: menuNav, frontViewController: mainVC)
mainRevealVC?.modalTransitionStyle = .crossDissolve
present(mainRevealVC!, animated: true, completion: nil)
And to get the reveal UIViewController to get shown, you just call
// Every UIViewController will have a `self.revealViewController()` when you `import SWRevealViewController`
self.revealViewController().revealToggle(animated: true)
I agree with #DonMag, a iOS slide menu might be your best bet. Here is an example of a simple one: SimpleSideMenu
Does it necessarily have to be a new controller behind the table view? Let me try to explain my approach on the WhatsApp example. Let's assume that the app has ChatController that has the table view with the chat and a ChatDetailController that is revealed with the swipe.
When you select a conversation, instead of presenting a ChatController present instead a ChatParent, that automatically creates and adds two children. The ChatController and ChatDetailController. Next define a protocol called SwipeableCellDelegate with a function cellDidSwipe(toPosition position: CGPoint) and make the ChatParent conform to it. When the cell is swiped, the parent can make the decision whether should the chat be moved away and if so, then how much. It can then simply move the ChatController view directly through its .view property, revealing the second child, the ChatDetailController behind it.
There are two downsides to this compared to the gif you posted.
The navigation bar doesn't fade from chat to chat detail. I would, however, argue that it is better to update the navigation bar when the animation completes, at least I personally am not a fan of this fade through where you can see both sets of navigation items at times. I would think that if chat is on screen then chat items should be present and only when detail view fully appears should the items be updated.
Second thing is the animated keyboard dismissal. I have no idea how to change keyboard frame to make it disappear proportionally to how far the user scrolls, but perhaps it could be dismissed automatically as soon as a swipe is detected? This is standard practice among many apps so it should be a decent solution.
Best of luck!
There is a very simple yet Perfect for your situation Library called SWNavigationController which implements just like UINavigationController's interactivePopGestureRecognizer also interactivePushGestureRecognizer. In your case you don't want the push to be triggered from UIScreenEdgePangesturerecognizer so you're better off customizing the implementation rather than installing the pod which is what I did. Here you can find the full simple project that does just what you asked.
I've made few modifications to SWNavigationController to support replacing UIScreenEdgePangesturerecognizer with a UIPanGestureRecognizer
import UIKit
// First in AppDelegate
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
self.window = UIWindow(frame: UIScreen.main.bounds)
let firstVc = ViewController()
let initialViewController: SWNavigationController = SWNavigationController(rootViewController: firstVc)
self.window?.rootViewController = initialViewController
self.window?.makeKeyAndVisible()
return true
}
// Your chat viewController
class ViewController: UIViewController {
var backgroundColors: [IndexPath : UIColor] = [ : ]
var swNavigationController: SWNavigationController {
return navigationController as! SWNavigationController
}
/// The collectionView if you're not using UICollectionViewController
lazy var collectionView: UICollectionView = {
let cv: UICollectionView = UICollectionView(frame: self.view.bounds, collectionViewLayout: self.layout)
cv.backgroundColor = UIColor.white
cv.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
cv.dataSource = self
return cv
}()
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "chat vc"
view.addSubview(collectionView)
let panGestureRecognizer: UIPanGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(ViewController.handlePan(_:)))
panGestureRecognizer.delegate = self
collectionView.addGestureRecognizer(panGestureRecognizer)
// Replace navigation controller's interactivePushGestureRecognizer with our own pan recognizer.
// SWNavigationController uses uiscreenedgerecognizer by default which we don't need in our case.
swNavigationController.interactivePushGestureRecognizer = panGestureRecognizer
}
func handlePan(_ recognizer: UIPanGestureRecognizer) {
guard fabs(recognizer.translation(in: collectionView).x) > fabs(recognizer.translation(in: collectionView).y) else {
return
}
// create the new view controller upon .began
if recognizer.state == .began {
// disable scrolling(optional)
collectionView.isScrollEnabled = false
// pan location
let location: CGPoint = recognizer.location(in: collectionView)
// get indexPath of cell where pan is taking place
if let panCellIndexPath: IndexPath = collectionView.indexPathForItem(at: location) {
// clear previously pushed viewControllers
swNavigationController.pushableViewControllers.removeAllObjects()
// create detail view controller for pan indexPath
let dvc = DetailViewController(indexPath: panCellIndexPath, backgroundColor: backgroundColors[panCellIndexPath]!)
swNavigationController.pushableViewControllers.add(dvc)
}
} else if recognizer.state != .changed {
collectionView.isScrollEnabled = true
}
// let navigation controller handle presenting
// (you can consume the initial pan translation on x axis to drag the cell to the left until a defined threshold and call handleRightSwipe: only after that)
swNavigationController.handleRightSwipe(recognizer)
}
}
// Cell detail view controller
class DetailViewController: UIViewController {
var indexPath: IndexPath
var backgroundColor: UIColor
init(indexPath: IndexPath, backgroundColor: UIColor) {
self.indexPath = indexPath
self.backgroundColor = backgroundColor
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "detail vc at: \(indexPath.row)"
view.backgroundColor = backgroundColor
}
}

Resources