A UIViewcontroller has a titleView at the top, an UIContainerViewwhich manage 2 TableviewController, and the last item is an UISegmentedControl which is used to change between LoginTableViewController and RegisterTableViewController which have 15 cells, so it's needed scroll on them.
LoginTableViewController and RegisterTableViewController are Statics Table Views but can't scroll, that means that if I Scroll it, it will immediately returns to the top.
Also I have another ViewController which has a containerView with a UITableViewController inside but it isn't static and it can scroll.
Here is my code of segmenteControlAction
#IBAction func selectTab(_ sender: UISegmentedControl) {
let nextViewController: UITableViewController!
switch sender.selectedSegmentIndex {
case 0:
let loginViewC = LoginTableViewController.instantiateFromStoryboard()
loginViewC.isPresentingFromMenu = isPresentingFromMenu
nextViewController = loginViewC
break
default:
let regiterViewC = RegisterTableViewController.instantiateFromStoryboard()
regiterViewC.isPresentingFromMenu = isPresentingFromMenu
nextViewController = regiterViewC
break
}
if let currentViewController = currentViewController{
currentViewController.willMove(toParentViewController: nil)
currentViewController.view.removeFromSuperview()
currentViewController.removeFromParentViewController()
}
currentViewController = nextViewController
addChildViewController(currentViewController!)
containerView.addSubview(currentViewController!.view)
currentViewController!.didMove(toParentViewController: self)
}
Do you know what I'm missing ?
EDITED
If I set the tableViewControllerwhich has 15 cells as embed view of ContainerView, the scroll works, but when I change the childViews in the container the scroll don't works
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 have a view controller that contains a collectionView with 2 sections. The header of the second section is a sticky header and it has a segmentedControl inside of it:
ParentViewController
--collectionView
--sectionOne // because there is specific data in sectionOne I cannot use a PageViewController
--sectionTwo
sectionTwoHeader // sticky header
[RedVC, BlueVC, GreenVC] // these should be the size of sectionTwo
When a segment is selected I'm using a ContainerVC that will show a view controller corresponding to each segment:
// each of of these color vcs have collectionViews inside of them
RedCollectionViewController(), BlueCollectionViewController(), GreenCollectionViewController()
The problem is when the segment is selected the collectionView isn't showing any of the color view controllers it's supposed to show. How do I add each color vc using addChildViewController() to a collectionView?
The collectionView w/ segmentedControl's selectedIndex:
class ParentViewController: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout{
var collectionView: UICollectionView!
var containerController: ContainerController!
var vc: UIViewController!
override func viewDidLoad() {
super.viewDidLoad()
containerController = ContainerController()
}
#objc func selectedIndex(_ sender: UISegmentedControl){
let index = sender.selectedSegmentIndex
switch index {
case 0:
containerController.vcIdentifierReceivedFromParent(segment: "BlueVC")
break
case 1:
containerController.vcIdentifierReceivedFromParent(segment: "RedVC")
break
case 2:
containerController.vcIdentifierReceivedFromParent(segment: "GreenVC")
break
default: break
}
/*
// because of the X and Y values this adds the containerVC over the collectionView instead of under the sectionTwo segmented Control header
vc = containerController
addChildViewController(vc)
vc.view.frame = CGRect(x: 0,y: 0, width: collectionView.frame.width,height: collectionView.frame.height)
view.addSubview(vc.view)
vc.didMove(toParentViewController: self)
lastViewController = vc
*/
}
}
ContainerVC:
class ContainerController: UIViewController {
var vc: UIViewController!
var lastViewController: UIViewController!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
vcIdentifierReceivedFromParent(segment: "RedVC")
}
func vcIdentifierReceivedFromParent(segment: String){
switch segment {
case "RedVC":
let redVC = RedCollectionViewController()
addVcToContainer(destination: redVC)
break
case "BlueVC":
let blueVC = BlueCollectionViewController()
addVcToContainer(destination: blueVC)
break
case "GreenVC":
let greenVC = GreenCollectionViewController()
addVcToContainer(destination: greenVC)
break
default: break
}
}
func addVcToContainer(destination: UIViewController) {
//Avoids creation of a stack of view controllers
if lastViewController != nil{
lastViewController.view.removeFromSuperview()
}
self.vc = destination
addChildViewController(vc)
vc.view.frame = CGRect(x: 0,y: 0, width: view.frame.width,height: view.frame.height)
view.addSubview(vc.view)
vc.didMove(toParentViewController: self)
lastViewController = vc
}
}
You are adding Red / Blue / Green VCs to Container View controller that is referenced from inside ParentViewController. But you are adding each of them inside ContainerVC topmost view, whose frame is probably never set, as far as I can see from your code.
It's probably CGRectZero.
Adding child VC views to this view will result in they are getting wrongly positioned, or not positioned at all. Because Container View controller is nowhere in the view controller hierarchy. You are effectively doing everything within ParentViewController's viewDidLoad(). Most probably, ContainerVC's viewDidLoad is not even called. Hence its view is never initialised properly.
You probably do not need ContainerVC at all. Try adding children to ParentViewController, and try adding them after viewDidLoad() call, i.e. in viewDidAppear(), viewDidLayoutSubviews() and upon switch segment selection.
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 trying to load a viewcontroller (with tableview) as a subview inside a contentview of a parent view controller and trying to get the height of the subview.
MainViewController:
ScrollView (scrollable)
ContentView
HeaderView
NavigationView
SegmentedControl <- this triggers which viewcontroller to load
MainContentHolder <- this is where viewcontrollers are loaded
HistoryViewController:
View
TableView (unscrollable) - displays certain number of records
TableViewCell
UIView
ImageView
LabelView
in HistoryViewController, data are being fetched (using Alamofire), then populating the cells.
In "MainViewController", there is a "SegmentedControl". I'm loading viewcontroller using this code:
#IBAction func scIndexedChanged(_ sender: UISegmentedControl) {
switch segmentedControl.selectedSegmentIndex
{
case 0:
let vc1 = self.storyboard?.instantiateViewController(withIdentifier: "vc1") as! vc1Controller
vc1.view.frame = mainContentHolder.bounds
mainContentHolder.addSubview(vc1.view)
addChildViewController(vc1)
vc1.didMove(toParentViewController: self)
case 1:
let vc2 = self.storyboard?.instantiateViewController(withIdentifier: "vc2") as! vc2Controller
vc2.view.frame = mainContentHolder.bounds
mainContentHolder.addSubview(vc2.view)
addChildViewController(vc2)
vc2.didMove(toParentViewController: self)
case 2:
let vc3 = self.storyboard?.instantiateViewController(withIdentifier: "vc3") as! vc3Controller
vc3.view.frame = mainContentHolder.bounds
mainContentHolder.addSubview(vc3.view)
addChildViewController(vc3)
vc3.didMove(toParentViewController: self)
default:
break;
}
Questions:
How to get the actual contentSize of the loaded viewcontroller (subview) AFTER populating cells? (uses Alamofire)
How to adjust the height of the ScrollView and/or ContentView of MainViewController to accommodate the height of the loaded subview (TableView, CollectionView, View.Frame)?
What is the proper way of doing this?
I'm also adding additional data to the loaded VC with:
scrollViewDidEndDecelerating(_ scrollView: UIScrollView)
from "MainViewController" and calling a function on the loaded VC to load more content after reaching a certain threshhold:
vc1.loadMoreContent()
Note: content size height is dynamically changing depending on how many NEW records are loaded.
Any help is very much appreciated
Thanks in advance
Because I want to redesign the tab bar UI, I wrote a custom tab bar controller according to https://github.com/codepath/ios_guides/wiki/Creating-a-Custom-Tab-Bar
In TabBarViewController's viewDidLoad(), define several subviews corresponding to each tab bar
homeViewController = storyboard.instantiateViewControllerWithIdentifier("HomeViewController")
...
viewControllers = [homeViewController, searchViewController, accountViewController, trendingViewController]
and the main method when tapping tab
#IBAction func didPressTab(_ sender: UIButton) {
let previousIndex = selectedIndex
selectedIndex = sender.tag
tabButtons[previousIndex!].isSelected = false
let previousVC = viewControllers[previousIndex!]
// remove previous VC
previousVC.willMove(toParentViewController: nil)
previousVC.view.removeFromSuperview()
previousVC.removeFromParentViewController()
// set current VC
sender.isSelected = true
let vc = viewControllers[selectedIndex]
addChildViewController(vc)
// Adjust the size to match content view
vc.view.frame = contentView.bounds
contentView.addSubview(vc.view)
vc.didMove(toParentViewController: self)
}
I could set a default tab bar index selectedIndex when the tab bar view is loaded. However, how can I switch to next tab bar in homeViewController (without tapping tab bar buttons)?
This doesn't work in homeViewController
TabBarViewController().tabButtons[2].isSelected = true TabBarViewController().didPressTab(TabBarViewController().tabButtons[2])
I'm not sure how to get the running tab controller, set the selectedIndex, and update subview in the subview controllers.
All you need to do is to call tabBar.setSelectedViewController: and pass the view controller.
If you only know the tab index, you call tabBar.viewControllers[index] and get the view controller.
I finally use Delegate to solve.
In SubViewController add protocol
protocol SubViewControllerDelegate {
func transferToView(index: Int)
}
and declare this in the class
var delegate: SubViewControllerDelegate?
In TabBarViewController set to conform SubViewControllerDelegate
Implement the method
func transferToView(index: Int) {
tabButtons[index].isSelected = true
didPressTab(tabButtons[index])
}
Set delegate
subViewController = storyboard.instantiateViewControllerWithIdentifier("HomeViewController")
let subVC = subViewController as! SubViewController
subVC.delegate = self