In working on this app with a TabBar at the bottom, NavBar at the top with a Segmented Control:
I have an issue where the View A (Segment One) with a UITableView, upon selecting a cell and displaying a new view with more details, when I click back, the Segmented control at the top will disappear and the TableView from View A will be pushed up.
This doesn't always happen - sometimes after many tries or sometimes just one. I haven't found any correlation to what's causing it.
I have found that if I select View B from the segmented Control, then back to View A, then click on one of the table cells to get to the details screen and then click back, 100% of the time the Top Nav Bar disappears with the segmented control.
TabBarItemOneViewController
let segmentOneVC: SegmentOneViewController
let segmentTwoVC: SegmentTwoViewController
var currentViewController: UIViewController
let viewControllerYLoc = 60 // statusBarHeight + topBarHeight
let viewWidth = Helper.getViewWidth()
let tabBarHeight = 40
func pressedSegItem(segControl: UISegmentedControl){
let viewControllerHeight = Int(self.view.frame.height)
let viewFrame = CGRect(x: 0, y: viewControllerYLoc, width: Int(viewWidth), height: viewControllerHeight)
let selectedIndex = segControl.selectedSegmentIndex
previouslySelectedMyLoadsIndex = selectedIndex
self.currentViewController.removeFromParentViewController()
if(selectedIndex == 0){
currentViewController = segmentOneVC
}
else if(selectedIndex == 1){
currentViewController = segmentTwoVC
}
self.view.addSubview(self.currentViewController.view)
self.currentViewController.didMove(toParentViewController: self)
}
public init() {
segmentOneVC = SegmentOneViewController(nibName: nil, bundle: nil)
segmentTwoVC = SegmentTwoViewController(nibName: nil, bundle: nil)
if(previouslySelectedIndex == 0){
currentViewController = segmentOneVC
}
else{
currentViewController = segmentTwoVC
}
super.init(nibName: nil, bundle: nil)
self.calculateItems()
self.addSegmentedControl()
let viewControllerHeight = (Int(self.view.frame.height) - viewControllerYLoc) - tabBarHeight
let viewFrame = CGRect(x: 0, y: viewControllerYLoc, width: Int(viewWidth), height: viewControllerHeight)
self.currentViewController.view.frame = viewFrame
self.addChildViewController(segmentOneVC)
self.addChildViewController(segmentTwoVC)
self.view.addSubview(self.currentViewController.view)
self.currentViewController.didMove(toParentViewController: self)
}
SegmentOneViewController (note: SegmentTwoViewController is identical)
let cellReuseIdentifier = "ItemDetailTableViewCell"
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let row = indexPath.row
let dataItem = self.dataArray[row]
let itemDetailVC = ItemDetailViewController()
itemDetailVC.dataItem = dataItem
self.present(itemDetailVC, animated: true, completion: nil)
}
func addTableView(){
self.tableView = UITableView()
tableView.register(ItemDetailTableViewCell.self, forCellReuseIdentifier: self.cellReuseIdentifier)
tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
tableView.frame = CGRect(x: 0, y: 0, width: Int(viewWidth), height: (Int(self.view.frame.height) - bottomOfTopNavBar) - heightOfTabBar)
self.view.addSubview(tableView)
}
override func viewDidAppear(_ animated: Bool){
super.viewDidAppear(animated)
loadData()
tableView.dataSource = self
tableView.delegate = self
}
override func viewDidLoad() {
super.viewDidLoad()
addTableView()
}
ItemDetailViewController
// Connected to a back button in a top Navigation Bar
func goBack(){
self.dismiss(animated: false, completion: nil)
}
Nice graphics BTW... that illustration make it much easier to understand your problem.
Also BTW, I'm an Obj-C person, still learning the nuances of Swift, so please let me know if my syntax or otherwise is incorrect. I'm also relatively inexperienced in using container VC's.
I've written my response in two parts.
The First Part is my attempt to solve your problem.
The Second Part is my suggestion for an alternative for you to consider.
First Part
This is my understanding of the order/sequence of execution in your code...
Parent View with Segmented Control
public init () : on instantiation of the parent view controller, two child VCs (segmentOneVC and segmentTwoVC) are instantiated and depending on previous selection are assigned as currentViewController. Then you add a segmented control to the TabBarItemOneViewController.
User taps a segmented control.
Depending on user input, either the SegmentOneViewController or SegmentTwoViewController view is added as a subview to the TabBarItemOneViewController.view. (Note that this is also done when the VC is initialised.)
Child View
override func viewDidLoad() : once the view did load, you call the function addTableView.
func addTableView() : in this custom function you instantiate your table view and place it within the SegmentOneViewController, which is itself I assume a UIViewController.
override func viewDidAppear(_ animated: Bool) : you call the custom function loadData and set your table view data source and delegate.
Back Button
User taps the back button.
Child VC is dismissed and the TabBarItemOneViewController becomes the active view on screen.
Let's look at what does not happen in the view controller lifecycle when the back button is pressed... Item 1 in the list.
This may explain the inconsistency.
Try this... run the app, tap the tab control to take you to TabBarItemOneViewController. Don't tap the segmented control. Tap a line in your table view. Tap the back button in your child VC. I'd take a guess your segmented control is still there.
Now try this... run the app, tap the tab control to take you to TabBarItemOneViewController. Tap the segmented control. Tap a line in your table view. Tap the back button in your child VC. I'd take a guess your segmented control is no longer there.
Why? Because the custom function pressedSegItem that I assume has a target action assigned to the segmented control, will overwrite your init, which is where you add the segmented control into the tab bar view controller.
So by way of example, try placing the code to instantiate your segmented control instead in an override function of viewWillAppear of the TabBarItemOneViewController VC.
So a couple of concepts to think about...
lazy loading to save memory allocation - only instantiate the objects you need when the user specifically requests that function in the app;
order of execution of each function in the UIViewController lifecycle; and
which functions are executed once and which are executed each time your view becomes first responder / the active view.
Some reading recomendations:
This SO question titled :Looking to understand the iOS UIViewController lifecycle" presents a lot of good information, but understand that some of the information is incorrect due to deprecation of viewDidUnload from iOS 6.
Which is why you should always go to the Apple documentation for UIViewController to refer to the latest API reference.
Second Part
By providing this alternative, I'm not suggesting that your approach is incorrect, however I am suggesting an alternative for you to consider.
I've always used tab bar controllers to change views and segmented controls to filter data sets or change the appearance of the current view.
Think about using a UISegmentedControl to manage or adjust the data set within only one table view. This will alleviate the need for multiple view controllers and the juggling act of managing these.
For example, when writing your data source and delegate methods / functions for your tabel view, you can include the following code to ensure the table view loads and responds accordinagly:
let selectedIndex = segControl.selectedSegmentIndex
if(selectedIndex == 0) {
rowHeight = 20 'for example
} else {
rowHeight = 30 'for example
}
Then you'd need to relaod your table view to effect the changes.
Related
I implemented a Hamburger Menu which gets called when the user taps a BarButtonItem.
When the user clicks an index of the menu a delegate method gets called and selects the correct row:
func rowTapped(index: MenuIndex) {
let vc1 = storyboard?.instantiateViewController(withIdentifier: "VC1") as! VC!
// lazy loading
_ = vc1.self.view
vc1.transitionToNew(index)
}
And in my VC1 the ** transitionToNew** method gets called and selects the correct index:
(Let´s assume that the user tapped index 1 which is associated to .a)
func transitionToNew(_ index : MenuIndex) {
switch index {
case .a:
addSubviewToContainer(asChildViewController: childVC)
...
}
Now the childVC should be added into the scrollView of my VC1.
The childVC is instantiated lazy:
private lazy var childVC: ChildVC = {
let viewController = self.storyboard?.instantiateViewController(withIdentifier: "ChildVC") as! ChildVC
return viewController
}()
To add the childVC into the scrollView the addSubViewToContainer method gets called in the switch-case statement:
private func addSubviewToContainer(asChildViewController viewController: UIViewController)
{
viewController.view.autoresizingMask = [.flexibleHeight, .flexibleWidth]
viewController.view.frame = scrollView.bounds
addChildViewController(viewController)
scrollView.addSubview(viewController.view)
viewController.didMove(toParentViewController: self)
}
I know that views gets instantiated lazy (sadly) so we have to input something like
_ = self.view
(although its a stupid hack) to instantiate the view which indeed shows me that my scrollView got instantiated (at least I think that because the preview in the debugger shows me the view)
Can someone tell me without seeing all of the code why my the childVC is not added or displayed (!?) in my scrollView?
I got the correct frame, the scrollView should be instantiated at the moment the user taps an index.
UPDATE
I also have some navigation buttons which the user can select:
#IBAction func navigateToChildVC(_ sender: UIButton) {
addSubviewToContainer(asChildViewController: childVC)
)
}
It is calling the exact same method but here it is working.
It looks like with your implementation the scroll view cannot determine its content size, so setting it explicitly might fix your issue. Something in the lines of scrollView.contentSize = scrollView.bounds.size sets the content size so that it fills the scroll view in both dimensions - which might not be what you want for a scroll view, but that is a different discussion.
There is also no need to call addChildViewController when lazily creating the child view controller, it is enough to have it called in addSubviewToContainer.
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
}
}
I'm developing an iOS app with swift in which I have a TabBarController with 5 tab bar items. All of them points to a navigation controller and then to a view controller. One of them I want to show a view controller without the tab bar and when the user press cancel it should go back to the previous tab bar item/view that was selected (previously - sorry for the redundancy). They are all linked/referenced by a "Relationship "view controllers" to "name of the view", but I don't have any specific segue or whatsoever.
This is the code for that specific "button" which I call in the viewDidLoad function:
func setupMiddleButton() {
let menuButton = UIButton(frame: CGRect(x: 0, y: 0, width: 64, height: 64))
var menuButtonFrame = menuButton.frame
menuButtonFrame.origin.y = self.view.bounds.height - menuButtonFrame.height
menuButtonFrame.origin.x = self.view.bounds.width/2 - menuButtonFrame.size.width/2
menuButton.frame = menuButtonFrame
menuButton.backgroundColor = UIColor.white
menuButton.layer.cornerRadius = menuButtonFrame.height/2
menuButton.setImage(UIImage(named: "klein_fototoestel_2"), for: UIControlState.normal) // 450 x 450px
menuButton.contentMode = .scaleAspectFit
menuButton.addTarget(self, action: #selector(menuButtonAction), for: UIControlEvents.touchUpInside)
self.view.addSubview(menuButton)
self.view.layoutIfNeeded()
}
func menuButtonAction(sender: UIButton) {
self.selectedIndex = 2
}
I tried to perform the behaviour I want by delegating the tab bar controller with the following code but this function is never called when the central button is selected (though the correct view shows up..!):
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
print("the selected index is : \(tabBar.items?.index(of: item))")
}
What I really want to know is what is the correct way to implement that behaviour I want. Remembering that all views have a navigationController before. I read a lot of people suggesting using UserDefaults to store the index of the previous controller but to be honest I really don't think that's appropriate.
Any help is appreciated.
Thanks in advance.
I think you were on the right track - just need to get the correct connections.
In this diagram (it's kinda big - easier to read if you open it in a new tab), you see a "standard" UITabBar structure. The key is putting a default "do-nothing" view controller as the 3rd tab, and then adding a "special" view controller which will be loaded via code:
Then, your "action" function will look something like this:
func menuButtonAction(sender: UIButton) {
// Don't navigate to the tab index
//self.selectedIndex = 2
// instead, load and present the view you really want to see
if let vc = storyboard?.instantiateViewController(withIdentifier: "SpecialVC") as? SpecialViewController {
vc.modalPresentationStyle = UIModalPresentationStyle.overCurrentContext
self.present(vc, animated: true, completion: nil)
}
}
You can see and download a working example here: https://github.com/DonMag/SWTabsWithSpecialTab
I need to show a custom tableview in 1 side and the detail in other in ipad like with splitview controller. And so, the detail is visible after a selection of a button on the cell.
My problem comes from the displaying of the detail controller. I sent data from the button in tableview to container controller via a delegate method
let containerController = self.storyboard?.instantiateViewController(withIdentifier: "ContainerController") as! ContainerViewController
containerController.reactionViewControllerResponse(selectedMechanism: selectedMechanism)
self.navigationController?.pushViewController(containerController, animated: true)
and in the container, I create the detail controller via
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
if (selectedMechanism != "" && self.mechanismViewController != nil){
self.mechanismViewController = self.storyboard?.instantiateViewController(withIdentifier: "MechanismController") as! MechanismViewController?
self.mechanismViewController?.selectedMechanism = selectedMechanism
self.addChildViewController(mechanismViewController!)
mechanismViewController?.view.frame = CGRect(x: self.containerView.frame.size.width/2, y: 0, width: self.containerView.frame.size.width/2, height: self.containerView.frame.size.height)
self.containerView.addSubview((mechanismViewController?.view)!)
mechanismViewController?.didMove(toParentViewController: self)
}
}
but due to this line in the tableview controller
self.navigationController?.pushViewController(containerController, animated: true)
the detail controller is not shown in the same container controller, but in another one. I tried several things, but either nothing appears or in a different container.
Please help me!!!
P.S.: I don't use splitview controller since I don't need it as inital view controller, and I've already tried and had problems to display only the master on the whole screen, customize the tableview cell....
Problem solved, the delegate was badly instantiated.
Thanks to me :)
I'm creating a custom tab bar based on this tutorial: https://github.com/codepath/ios_guides/wiki/Creating-a-Custom-Tab-Bar
Everything is fine but when I want to segue from a certain view controller to its "details view", the details view covers the bottom bar with menu. This behaviour is logical as I'm pushing a new view controller but how would I have to do it in order to keep the bottom bar always visible and functional?
I'm using segue for this because I need to pass some data. I need custom bar because the functionality and the look would be very difficult to implement using the Apple's one.
Any tips or suggestions?
Thanks
EDIT:
Here all "tab" are working well but when you tap on a row I navigate to "details" view
In the details view, bottom bar is not visible.
have you tried setting hidesBottomBarWhenPushed = false for the details view?
Solved it. Instead of calling
self.performSegueWithIdentifier("detailsViewSegue", sender: self)
in
tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath)
I add DetailsView view to the hierarchy like so:
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let detailsView = storyboard.instantiateViewControllerWithIdentifier("contactDetailsView")
addChildViewController(detailsView)
detailsView.view.frame = CGRectMake(self.view.frame.width, 0, self.view.frame.width, self.view.superview!.frame.height)
self.view.addSubview(detailsView.view)
UIView.animateWithDuration(0.25) {
detailsView.view.frame = CGRectMake(0, 0, self.view.frame.width, self.view.superview!.frame.height)
}
detailsView.didMoveToParentViewController(self)
then when user taps on the back arrow I do:
#IBAction func goBackButtonAction(sender: UIButton) {
self.willMoveToParentViewController(self)
UIView.animateWithDuration(0.25, animations: {
self.view.frame = CGRectMake(self.view.frame.width, 0, self.view.frame.width, self.view.frame.height)
}) { (completed) in
if completed {
self.view.removeFromSuperview()
self.removeFromParentViewController()
}
}
}