NOTE: I originally asked this question wondering why didSelectRowAtIndex was not getting called. Digging into it further, I've realized that my core problem is really with one of my views not receiving touch events. I am going to leave this question here for posterity and ask a more clarified version of it. If an admin would like to close or delete this question as appropriate, please go ahead and do so.
I am implementing a sidebar in my app (like a hamburger menu) and I'm using a UITableViewController to do it. I can get the sidebar to show up and cells are initializing correctly.
My code is pretty simple, I am including all of it here. Right now I just have a view controller for the main screen and a table view controller for the sidebar. Any help is appreciated!
Update: it looks like the problem is not that didSelectRowAtIndexPath is not being called -- it's that my SimpleTableViewController is not receiving any touches at all! I tried overriding 'touchesBegan:withEvent:' in both the ViewController and the SimpleTableViewController: the ViewController receives touches just fine, but the table view controller appears to receive nothing.
Further update: I've realized that the issue has to do with how I am doing the animation to reveal the tableview on the right hand side of the screen. Right now I am "pushing" the main application view to the left, which "pulls" the containerView along with it. When I do that, no touches go to the containerView when I tap on it.
However! If I "pull" the containerView on top of the main view, touches are received just fine. Maybe I am missing something elementary here regarding what iOS considers to be the "active" part of the screen where touches are legal?
Code here:
Before -- BROKEN
#IBAction func push() {
// containerView is "pulled" alongside self.view
UIView.animateWithDuration(3.0) {
self.view.frame = CGRectMake(self.originalX - 250, self.originalY, self.view.frame.width, self.view.frame.height)
}
}
Here is a gif showing what the app looks like when touches don't work.
After -- WORKS
#IBAction func push() {
// containerView is "pulled" on top of self.view
UIView.animateWithDuration(3.0) {
self.containerView.frame = CGRectMake(self.originalX - 250, self.originalY, 250, self.frameHeight)
}
}
Here is a gif showing what the app looks like when touches do work. I added some code to change the background color of the main view to illustrate that touches are being received.
Code follows - I previously had my entire implementation here but I've redacted it to include only the relevant parts.
First the main view controller:
import UIKit
class ViewController: UIViewController, SimpleTableViewControllerDelegate {
var x = UIView()
var y = UIView()
var containerView = UIView()
let animationDuration = 1.5;
let delayTime = 0.25;
let animationTime = 0.25;
var tableViewController = SimpleTableViewController()
override func viewDidLoad() {
super.viewDidLoad()
originalX = self.view.frame.origin.x
originalY = self.view.frame.origin.y
frameHeight = self.view.frame.height
frameWidth = self.view.frame.width
containerView.frame = CGRectMake(frameWidth, originalY, 250, frameHeight)
containerView.backgroundColor = UIColor.magentaColor()
containerView.clipsToBounds = false
view.addSubview(containerView)
tableViewController.items = ["Lemon", "Lime", "Agave"]
tableViewController.delegate = self
tableViewController.tableView.dataSource = tableViewController
tableViewController.tableView.delegate = tableViewController
tableViewController.tableView.frame = containerView.bounds
tableViewController.tableView.backgroundColor = UIColor.lightGrayColor()
tableViewController.tableView.scrollsToTop = false
tableViewController.tableView.separatorStyle = .None
tableViewController.tableView.contentInset = UIEdgeInsets(top: 64.0, left: 0, bottom: 0, right: 0)
tableViewController.tableView.reloadData()
addChildViewController(tableViewController)
containerView.addSubview(tableViewController.tableView)
tableViewController.didMoveToParentViewController(self)
}
func didSelectRowAtIndexPath(indexPath: NSIndexPath) {
NSLog("[ViewController] invoked didSelectRowAtIndexPath with \(indexPath.row)")
}
var originalX : CGFloat!
var originalY : CGFloat!
var frameHeight : CGFloat!
var frameWidth : CGFloat!
#IBAction func push() {
UIView.animateWithDuration(animationTime, delay: 0, options: .CurveLinear, animations: {
self.view.frame = CGRectMake(self.originalX - 250, self.originalY, self.view.frame.width, self.view.frame.height)
}, completion: { (var b) -> Void in
})
}
#IBAction func pull() {
UIView.animateWithDuration(animationTime, delay: 0, options: .CurveLinear, animations: {
self.view.frame = CGRectMake(self.originalX, self.originalY, self.view.frame.width, self.view.frame.height)
}, completion: { (var b) -> Void in
})
}
}
For what it's worth, if you'd like to see the whole app (it's just a learning app) you can go here: https://github.com/bitops/sagacious-quack
When you add another view controller's view to your view hierarchy, you have to let both parent and child know, so the parent can forward relevant messages to the child. This is explained in Apple's View Controller Programming Guide, in the "Implementing a Custom Container View Controller" section.
In your case, you need something like this:
[self addChildViewController:tableViewController];
containerView.addSubview(tableViewController.tableView)
[tableViewController didMoveToParentViewController:self];
In viewDidLoad() of ViewController:
//Add these two lines
tableViewController.tableView.dataSource = tableViewController
tableViewController.tableView.delegate = tableViewController
tableViewController.delegate = self
tableViewController.tableView.frame = containerView.bounds
tableViewController.tableView.backgroundColor = UIColor.lightGrayColor()
tableViewController.tableView.scrollsToTop = false
tableViewController.tableView.separatorStyle = .None
tableViewController.tableView.contentInset = UIEdgeInsets(top: 64.0, left: 0, bottom: 0, right: 0)
tableViewController.tableView.reloadData()
Related
I've been looking for this for hours and have no luck yet. I want to use two custom views inside a scrollview. The first view have a button as a subview that leads the user to the next page(scrolls down). But the button action it's never fired. If I use the button as a scrollview subview everything works fine, but that's not what I want.
The code for the scrollview view controller:
class ViewController: UIViewController {
#IBOutlet weak var scrollView: UIScrollView!
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let redView = View1()
redView.view.frame = CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.scrollView.frame.height)
self.scrollView.addSubview(redView.view!)
let blueView = UIView(frame: CGRect(x: 0, y: self.scrollView.frame.height, width: self.view.frame.width, height: self.scrollView.frame.height))
blueView.backgroundColor = .blue
self.scrollView.addSubview(blueView)
self.scrollView.contentSize = CGSize(width: self.view.frame.width, height: self.scrollView.frame.height * 2)
}
func go(to page: Int) {
let y = CGFloat(page) * self.scrollView.frame.size.height
self.scrollView.setContentOffset(CGPoint(x: 0, y: y), animated: true)
}
}
ScrollView Storyboard Configuration
The code of the View1 Class:
class View1: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func didTouchUpInsideMoreInfoButton(_ sender: Any) {
print("test")
}
}
Any ideas are welcome. Thanks in advance.
Turn out that I was using a view controller's view and for some reason, the selectors don't work this way. So, what I did was to create a UIView only and then everything works just fine.
Check if the buttons userInteraction is enabled.
Sometimes if your button is inside another view, a UIView for example, it doesn't allow you to tap it. To fix this, you might want to add the button ON TOP of that view but not inside it. Make sure it's above but not inside any view in on your storyboard.
Situation: I've got a UITableViewController loading some data asynchronously from a service. During this time I would like to place a full screen (except navigation bar) view over the table view showing my custom indicator and text.
Problem: The problem I'm facing is that when my custom view (it has a red background) is placed over the UITableView the lines of the table view are shown trough my custom view (see image below).
What I tried:
I tried to use insertBelow and above, didn't work. I also tried to do: tableview.Hidden = true, but this also hides the custom view for some reason as seen on image 2.
Image1: For some reason I can see the lines threw my view.
Image 2: Tableview + custom view gone when hidden = true used.
My code:
public override void ViewDidLoad ()
{
base.ViewDidLoad ();
UIView view = new UIView (new RectangleF (0, 0, this.TableView.Frame.Width, this.TableView.Frame.Height));
view.BackgroundColor = UIColor.Red;
this.TableView.AddSubview (view);
TableView.Source = new SessionTableViewSource ();
}
You can use self.navigationController.view as view for adding subview.
The issue is that the View of a UITableViewController is a UITableView, so you cannot add subviews to the controller on top of the table.
I'd recommend switching from a UITableViewController to a simple UIViewController that contains a UITableView. This way the controller main view is a plain UIView that contains a table, and you can add subviews to the main UIView and they will be placed on top of the table view.
You can try to add the view to the window instead of nesting it in the table view like this:
UIWindow* mainWindow = [[UIApplication sharedApplication] keyWindow];
[mainWindow addSubview: overlayview];
UIWindow* window = [[UIApplication sharedApplication].delegate.window;
[window addSubview: your-overlayview];
Swift / Storyboard Solution
Note: The code below assumes one has a custom view (ratingView in my case) that is to be presented over a UITableView.
I've read many answers to this and similar questions on SO. The other answers from these sources worked to varying degrees for me (e.g.,view loaded but not shown or not accessible,...). I am using Swift 2.0+ and I am sharing the complete solution for doing this using a UITableViewController.
Create an outlet to the Navigation Bar and the view, which you want to bring over the tableview.
//MARK:Outlets
#IBOutlet weak var navBar:UINavigationBar!
#IBOutlet var ratingView: MNGStarRating!
In my case I also wanted to animate the view over the tableview so I used a class variable to hold a reference to the inflection point and a point above the scene (off-screen).
var centerYInflection:NSLayoutConstraint!
var aPointAboveScene = -(max(UIScreen.mainScreen().bounds.width,UIScreen.mainScreen().bounds.height) * 2.0)
Then in viewDidLoad I called a function (configureRatingViewAutoLayout) which configures and adds the constraints for the new view to be animated over the tableview.
func configureRatingViewAutoLayout() {
//REQUIRED
self.navBar.superview?.addSubview(self.ratingView)
var newConstraints:[NSLayoutConstraint] = []
newConstraints.append(self.ratingView.leadingAnchor.constraintEqualToAnchor(self.view.leadingAnchor,constant: 10))
newConstraints.append(self.ratingView.trailingAnchor.constraintEqualToAnchor(self.view.trailingAnchor,constant: 10))
newConstraints.append(self.ratingView.centerXAnchor.constraintEqualToAnchor(self.view.centerXAnchor))
//hides the rating view above the scene
self.centerYInflection = self.ratingView.centerYAnchor.constraintEqualToAnchor(self.view.centerYAnchor, constant: self.aPointAboveScene)
//the priority must be set below 1000 if you intend to change it after it has been added to a view
self.centerYInflection.priority = 750
newConstraints.append(self.centerYInflection)
//constraints must be added to the container view of the two items
self.ratingView.superview?.addConstraints(newConstraints)
}
Nota Bene - On a UITableViewController; the self.view is the
self.tableView. They point to the same thing so I guess one could also
use the self.tableView reference above.
Sometime later... In response to a UIControl event I call this method.
#IBAction func toggleRatingView (sender:AnyObject?){
//REQUIRED
self.ratingView.superview?.layoutIfNeeded()
UIView.animateWithDuration(1.0, delay: 0.0, usingSpringWithDamping: 0.37, initialSpringVelocity: 0.99, options: [.CurveEaseOut], animations: { () -> Void in
if CGRectContainsRect(self.view.frame, self.ratingView.frame) {
//in frame ~ animate away
//I play a sound to alert the user something is happening
self.centerYInflection.constant = self.aPointAboveScene
self.centerYInflection.priority = UILayoutPriority(950)
//I disable portions of the UI
self.disableUIElements(nil)
} else {
//out of frame ~ animate in
//I play a different sound here
self.centerYInflection.constant = 0
self.centerYInflection.priority = UILayoutPriority(950)
//I enable the UI fully
self.enableUIElements(nil)
}
//REQUIRED
self.ratingView.superview?.setNeedsLayout()
self.ratingView.superview?.layoutIfNeeded()
}) { (success) -> Void in
//do something else
}
}
These helper methods can be configured to control access to elements in your scene during the presentation of the view.
func disableUIElements(sender:AnyObject?) {
//UI
}
func enableUIElements(sender:AnyObject?) {
//UI
}
Caveats
My view is a custom view in the Storyboard (sitting outside of the
tableview but connected to the TableView Controller). The view has a
required user runtime attribute defined layer.zPosition with a Number value set to 2 (this ensures that it presents in front of the
UITableView).
One could also try playing around with bringSubviewToFront:
and sendSubviewToBack: methods if you don't want to set the zPosition
(I think zPosition is simpler to use)
Try this to hook a button at bottom of the UITableViewController
declare button as a variable:
var submitButton: UIButton!
and in viewDidLoad:
submitButton = UIButton(frame: CGRect(x: 5, y: UIScreen.main.bounds.size.height - 50, width: UIScreen.main.bounds.size.width - 10, height: 50))
submitButton.backgroundColor = UIColor.init(red: 180/255, green: 40/255, blue: 56/255, alpha: 1.0)
submitButton.setTitle("Submit", for: .normal)
submitButton.titleLabel?.font = UIFont(name: "Arial", size: 15)
submitButton.titleLabel?.textColor = .white
submitButton.addTarget(self, action: #selector(submit), for: .touchUpInside)
submitButton.layer.cornerRadius = 5
self.view.addSubview(submitButton)
and implement this method:
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
submitButton.frame = CGRect.init(x: submitButton.frame.origin.x, y: UIScreen.main.bounds.size.height + scrollView.contentOffset.y - 50, width: submitButton.frame.width, height: submitButton.frame.height)
}
This works for me:
if let myTopView = Bundle.main.loadNibNamed("MyTopView", owner: self, options: nil)?.first as? MyTopView {
if let view = UIApplication.shared.keyWindow{
view.addSubview(myView);
myTopView.translatesAutoresizingMaskIntoConstraints = false
myTopView.topAnchor.constraint(equalTo: view.topAnchor ).isActive = true
myTopView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
myTopView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
myTopView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
}
}
I want to make 1 UIScrollView that has inside of it 1 background which can be used by multiple ViewControllers. When the user scrolls to the next ViewController, the background should scroll with it. This is my code:
override func viewDidLoad() {
super.viewDidLoad()
let leftViewController: StartLeftViewController = StartLeftViewController(nibName: "StartLeftViewController", bundle: nil)
self.addChildViewController(leftViewController)
self.theScrollView.addSubview(leftViewController.view)
leftViewController.didMove(toParentViewController: self)
let middleViewController: StartMiddleViewController = StartMiddleViewController(nibName: "StartMiddleViewController", bundle: nil)
self.addChildViewController(middleViewController)
self.theScrollView.addSubview(middleViewController.view)
middleViewController.didMove(toParentViewController: self)
let rightViewController: StartRightViewController = StartRightViewController(nibName: "StartRightViewController", bundle: nil)
self.addChildViewController(rightViewController)
self.theScrollView.addSubview(rightViewController.view)
rightViewController.didMove(toParentViewController: self)
var middleFrame : CGRect = middleViewController.view.frame
middleFrame.origin.x = self.view.frame.width
middleViewController.view.frame = middleFrame
var thirdFrame : CGRect = rightViewController.view.frame
thirdFrame.origin.x = 2 * self.view.frame.width
rightViewController.view.frame = thirdFrame
self.theScrollView.contentSize = CGSize(width: self.view.frame.width * 3, height: self.view.frame.height)
theScrollView.contentOffset.x = view.frame.width
}
I already tried setting a background in the middleViewController which has an width of 3x, but I got problems adding elements to left/right ViewController causing the elements hiding behind the background. Thank you.
In your storyboard you can place three Container Views into your scroll view, one after each other horizontally, and connect each of them with Embed segue to your corresponding view controllers.
I seem to be having some issues getting the UIPageControl to work.
I have a ViewController that holds a ScrollView. This ScrollView loads nib files that can be swiped. See image:
Here is the code that loads these:
self.addChildViewController(vc0)
self.displayReportView.addSubview(vc0.view)
vc0.didMoveToParentViewController(self)
var frame1 = vc1.view.frame
frame1.origin.x = self.view.frame.size.width
vc1.view.frame = frame1
self.addChildViewController(vc1)
self.displayReportView.addSubview(vc1.view)
vc1.didMoveToParentViewController(self)
// And so on
...
This works fine as in they scroll correctly etc..
Now, on the ViewController (one holding the scrollview) I added the delegate:
UIScrollViewDelegate
created some variables:
var frame: CGRect = CGRectMake(0, 0, 0, 0)
var colors:[UIColor] = [UIColor.redColor(), UIColor.blueColor(), UIColor.greenColor(), UIColor.yellowColor()]
var pageControl : UIPageControl = UIPageControl(frame: CGRectMake(50, 300, 200, 20))
I added some functions that are needed:
func configurePageControl() {
// The total number of pages that are available is based on how many available colors we have.
self.pageControl.numberOfPages = 4
self.pageControl.currentPage = 0
self.pageControl.tintColor = UIColor.redColor()
self.pageControl.pageIndicatorTintColor = UIColor.blackColor()
self.pageControl.currentPageIndicatorTintColor = UIColor.greenColor()
self.view.addSubview(pageControl)
}
// MARK : TO CHANGE WHILE CLICKING ON PAGE CONTROL
func changePage(sender: AnyObject) -> () {
let x = CGFloat(pageControl.currentPage) * displayReportView.frame.size.width
displayReportView.setContentOffset(CGPointMake(x, 0), animated: true)
}
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
let pageNumber = round(scrollView.contentOffset.x / scrollView.frame.size.width)
pageControl.currentPage = Int(pageNumber)
}
Now, When I run the app the scrollview dots show, but when I swipe they do not update.
Question
How do I update the dots to reflect what view is showing?
let me know if you need anything else from my code to see functionality.
You can certainly do what you're describing, if you have a paging scroll view; I have an example of it that uses this code:
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
let x = scrollView.contentOffset.x
let w = scrollView.bounds.size.width
pageControl.currentPage = Int(x/w)
}
Except for your round, that looks a lot your code, which makes me think that your code should work. That makes me think that something else is just misconfigured. Is this a paging scroll view? Did you remember to make this object your scroll view's delegate? Use logging or a breakpoint to be certain that your scrollViewDidEndDecelerating is even being called in the first place.
However, I would just like to point out that the configuration you are describing is effectively what UIPageViewController gives you for free — a scroll view with view controller views, plus a page control — so you might want to use that instead.
I would replace the scroll view with a UICollectionView. This way you get paging for free, and it will be better, because paging will work out of the box, without you having to calculate the frame offsets.
Be sure to set collectionView.pagingEnabled = true
To get the current page number, do collectionView.indexPathsForVisibleItems().first?.item
To change the page:
collectionView.scrollToItemAtIndexPath(newIndexPath, atScrollPosition: CenteredHorizontally, animated: true)
I have created a "HorizontalSplit"-UIViewController. In that I created two UIViews as containers for other UIViewController. The height of the Containers is the half of the Screen.
override func viewDidLoad() {
super.viewDidLoad()
...
self.topViewContainer = UIView(frame: CGRectMake(0, 0, self.view.bounds.width, completeHeight / 2))
self.topViewContainer.backgroundColor = UIColor.orangeColor()
self.view.addSubview(topViewContainer)
self.bottomViewContainer = UIView(frame: CGRectMake(0, completeHeight / 2, self.view.bounds.width, completeHeight / 2))
self.bottomViewContainer.backgroundColor = UIColor.blueColor()
self.view.addSubview(bottomViewContainer)
...
}
After that I put a new Controller in that View an used these lines of code:
func setTopViewController(viewController: UIViewController) {
if self.topViewController != nil {
self.topViewController.removeFromParentViewController()
self.topViewController.view.removeFromSuperview()
}
var l = topViewContainer.frame.height
self.topViewController = viewController
self.topViewController.view.frame = CGRect(x: 0, y: 0, width: topViewContainer.bounds.width, height: topViewContainer.bounds.height)
var o = topViewController.view.frame.height
self.addChildViewController(self.topViewController)
self.topViewContainer.addSubview(self.topViewController.view)
self.topViewController.didMoveToParentViewController(self)
}
I want that the SubViewController-Frame-Size (here called simple viewController) have the same size like the UIView-container that I added before.
It works here because l and o (the variables) says the right height. But when my ViewController that I setup via the setTopViewController method on the HorizontalViewContrller the size is'n correct anymore.
When I call on viewDidLoad 'self.view.bounds.height' on the subViewController it called the full size! And not the size I have set before... Why?
Ok, I found the solution. Thanks to these post of UIViewControllers lifecycle:
In the Sub-ViewController I've get the height in viewDidLoad but in there it haven't actually the settet frame size. Also with the help of these I found that the viewWillAppear have the settet height of the frame.
Hope that helps anyone too :)