View not getting redrawed - ios

I have a view that I need to move 102 points to the right to show a menu, so I just call a offset every new view call. It was working till some builds, we changed something that made the view break. Since I can't try to undo something that is fixing another thing, I need to patch the view.
When I print the view after applying the offset(with po self.view.frame), I can see the offset applied correctly, but the view doesn't show it to me unless I change it again(go to another screen or rotate the device).
func resizeAndOffset() {
let appDelegate = UIApplication.appDelegate()
let offset = UIApplication.appDelegate().offset
self.view.frame = CGRectMake(offset, 0, appDelegate.window!.frame.width-offset, appDelegate.window!.frame.height)
//self.view.frame = CGRectOffset(self.view.frame, offset, 0)
self.navigationController?.navigationBar.frame = CGRectMake(offset, 0, appDelegate.window!.frame.width-offset, self.navigationController!.navigationBar.frame.height)
}
This is where I resize and move the view.
func rotated() {
if UIDevice.currentDevice().userInterfaceIdiom == .Pad {
resizeAndOffset()
}
}
I call it every time the screen changes orientation.
And I also call the resize function on viewDidLoad() and viewDidAppear().
Is there anywhere else I should be calling it? Where I can force an entire screen redraw?

Add setNeedsDisplay() to the bottom of your function:
func resizeAndOffset() {
let appDelegate = UIApplication.appDelegate()
let offset = UIApplication.appDelegate().offset
self.view.frame = CGRectMake(offset, 0, appDelegate.window!.frame.width-offset, appDelegate.window!.frame.height)
//self.view.frame = CGRectOffset(self.view.frame, offset, 0)
self.navigationController?.navigationBar.frame = CGRectMake(offset, 0, appDelegate.window!.frame.width-offset, self.navigationController!.navigationBar.frame.height)
self.view.setNeedsDisplay()
}

Related

safeAreaInsets in UIView is 0 on an iPhone X

I am updating my app to adapt it for iPhone X. All views work fine by now except one. I have a view controller that presents a custom UIView that covers the whole screen. Before I was using UIScreen.main.bounds to find out the size of the view before all layout was done (I need it for putting the correct itemSize for a collectionView). I thought that now I could do something like
UIScreen.main.bounds.size.height - safeAreaInsets.bottom to get the right usable size. The problem is, safeAreaInsets returns (0,0,0,0) trying on an iPhone X (Simulator). Any ideas? In other views, I get the right numbers for safeAreaInsets.
Thank you!
I recently had a similar problem where the safe area insets are returning (0, 0, 0, 0) as soon as viewDidLoad is triggered. It seems that they are set fractionally later than the rest of the view loading.
I got round it by overriding viewSafeAreaInsetsDidChange and doing my layout in that instead:
override func viewSafeAreaInsetsDidChange() {
// ... your layout code here
}
I already figure out the solution: I was doing all the implementation in the init of the view. safeAreaInsets has the correct size in layoutSubviews()
I've run into this issue too trying to move up views to make way for the keyboard on the iPhone X. The safeAreaInsets of the main view are always 0, even though I know the subviews have been laid out at this point as the screen has been drawn. A work around I found, as and mentioned above, is to get the keyWindow and check its safe area insets instead.
Obj-C:
CGFloat bottomInset = [UIApplication sharedApplication].keyWindow.safeAreaInsets.bottom;
Swift:
let bottomInset = UIApplication.shared.keyWindow?.safeAreaInsets.bottom
You can then use this value to adjust constraints or view frames as required.
I have a view which is a subview inside another view.
I found that I can't get safeAreaInsets correctly, it always return 0, in that view on iPhoneX even if I put it in layoutSubviews.
The final solution is I use following UIScreen extension to detect safeAreaInsets which can work like a charm.
extension UIScreen {
func widthOfSafeArea() -> CGFloat {
guard let rootView = UIApplication.shared.keyWindow else { return 0 }
if #available(iOS 11.0, *) {
let leftInset = rootView.safeAreaInsets.left
let rightInset = rootView.safeAreaInsets.right
return rootView.bounds.width - leftInset - rightInset
} else {
return rootView.bounds.width
}
}
func heightOfSafeArea() -> CGFloat {
guard let rootView = UIApplication.shared.keyWindow else { return 0 }
if #available(iOS 11.0, *) {
let topInset = rootView.safeAreaInsets.top
let bottomInset = rootView.safeAreaInsets.bottom
return rootView.bounds.height - topInset - bottomInset
} else {
return rootView.bounds.height
}
}
}
I try to use "self.view.safeAreaInset" in a view controller. First, it is a NSInsetZero when I use it in the controller's life cycle method "viewDidLoad", then I search it from the net and get the right answer, the log is like:
ViewController loadView() SafeAreaInsets :UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
ViewController viewDidLoad() SafeAreaInsets :UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
ViewController viewWillAppear() SafeAreaInsets :UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
ViewController viewDidLayoutSubviews() SafeAreaInsets :UIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0)
ViewController viewDidAppear() SafeAreaInsets :UIEdgeInsets(top: 44.0, left: 0.0, bottom: 34.0, right: 0.0)
so you can choice the right method that you need the safeAreaInset and use it!
Swift iOS 11,12,13+
var insets : UIEdgeInsets = .zero
override func viewDidLoad() {
super.viewDidLoad()
insets = UIApplication.shared.delegate?.window??.safeAreaInsets ?? .zero
//Or you can use this
insets = self.view.safeAreaInsets
}
In my case I was adding a UICollectionView inside viewDidLoad()
collectionView = UICollectionView(frame: view.safeAreaLayoutGuide.layoutFrame, collectionViewLayout: createCompositionalLayout())
Unfortunately at this stage safeAreaLayoutGuide is still zero.
I solved it by adding:
override func viewDidLayoutSubviews() {
collectionView.frame = view.safeAreaLayoutGuide.layoutFrame
}
the viewDidAppear(_:) method of the container view controller that extends the safe area of its embedded child view controller to account for the views in .
Make your modifications in this method because the safe area insets for a view are not accurate until the view is added to a view hierarchy.
override func viewDidAppear(_ animated: Bool) {
if (#available(iOS 11, *)) {
var newSafeArea = view.safeAreaInsets
// Adjust the safe area to accommodate
// the width of the side view.
if let sideViewWidth = sideView?.bounds.size.width {
newSafeArea.right += sideViewWidth
}
// Adjust the safe area to accommodate
// the height of the bottom view.
if let bottomViewHeight = bottomView?.bounds.size.height {
newSafeArea.bottom += bottomViewHeight
}
// Adjust the safe area insets of the
// embedded child view controller.
let child = self.childViewControllers[0]
child.additionalSafeAreaInsets = newSafeArea
}
}
I've come across the same problem. In my case the view I'm inserting would be sized correctly after calling view.layoutIfNeeded(). The view.safeAreaInsets was set after this, but only the top value was correct. The bottom value was still 0 (this on an iPhone X).
While trying to figure out at what point the safeAreaInsets are set correctly, I've added a breakpoint on the view's safeAreaInsetsDidChange() method. This was being called multiple times, but only when I saw CALayer.layoutSublayers() in the backtrace the value had been set correctly.
So I've replaced view.layoutIfNeeded() by the CALayer's counterpart view.layer.layoutIfNeeded(), which resulted in the safeAreaInsets to be set correctly right away, thus solving my problem.
TL;DR
Replace
view.layoutIfNeeded()
by
view.layer.layoutIfNeeded()
[UIApplication sharedApplication].keyWindow.safeAreaInsets return none zero
Just try self.view.safeAreaInsets instead of UIApplication.shared.keyWindow?.safeAreaInsets
Safe area insets seems to not fill on iOS 11.x.x devices when requested via application keyWindow.
View layout is never guaranteed until layoutSubviews or viewDidLayoutSubviews. Never rely on sizes before these lifecycle methods. You will get inconsistent results if you do.
To calculate safe area safeAreaInsets, try to obtain it in viewWIllAppear(), as in didLoad() the view have not been formed.
You will have the correct inset in willAppear!
In case you cannot subclass, you can use this UIView extension.
It gives you an API like this:
view.onSafeAreaInsetsDidChange = { [unowned self] in
self.updateSomeLayout()
}
The extension adds an onSafeAreaInsetsDidChange property using object association. Then swizzles the UIView.safeAreaInsetsDidChange() method to call the closure (if any).
extension UIView {
typealias Action = () -> Void
var onSafeAreaInsetsDidChange: Action? {
get {
associatedObject(for: "onSafeAreaInsetsDidChange") as? Action
}
set {
Self.swizzleSafeAreaInsetsDidChangeIfNeeded()
set(associatedObject: newValue, for: "onSafeAreaInsetsDidChange")
}
}
static var swizzled = false
static func swizzleSafeAreaInsetsDidChangeIfNeeded() {
guard swizzled == false else { return }
swizzle(
method: "safeAreaInsetsDidChange",
originalSelector: #selector(originalSafeAreaInsetsDidChange),
swizzledSelector: #selector(swizzledSafeAreaInsetsDidChange),
for: Self.self
)
swizzled = true
}
#objc func originalSafeAreaInsetsDidChange() {
// Original implementaion will be copied here.
}
#objc func swizzledSafeAreaInsetsDidChange() {
originalSafeAreaInsetsDidChange()
onSafeAreaInsetsDidChange?()
}
}
It uses some helpers (see NSObject+Extensions.swift and NSObject+Swizzle.swift), but you don't really need it if you use sizzling and object association APIs directly.

Changing views frame origin causes it to bounce back into place

I am attempting, when the keyboard appears, to shift the view up. This works on two views, but on the third the same code causing the view to seemingly move down a certain amount, then move back into the exact place it started, or so the animation seems. Debugging, I see nowhere else the self.view.frame is getting set but this method. In addition, the offsets look right, as if the view should move up like the other views have. See the keyboardWillShow method below.
func keyboardWillShow(notification: NSNotification){
if self.origFrame == nil{
self.origFrame = self.view.frame
}
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.CGRectValue(){
var testRect = self.view.frame
testRect.size.height -= keyboardSize.height
if !testRect.contains(loginBtn!.frame.origin){
let bottomSpace = self.view.frame.size.height - loginBtn.frame.origin.y - loginBtn.frame.size.height
let keyboardOverlap = keyboardSize.height - bottomSpace
let newY = self.origFrame!.origin.y - keyboardOverlap
self.view.frame.origin.y = newY
}
}
}
This is what I've done in the past
func textViewDidBeginEditing(textView: UITextView) {
let point:CGPoint = CGPoint(x: textView.frame.origin.x - 8, y: textView.frame.origin.y - 100)
scrollView.contentOffset = point
}
This moves the view up based on the position of textview the user tapped on. 8 and 100 pixels just happened to be good ranges for my specific purpose.
Alternatively, instead of moving the frames, you could programmatically adjust your constraints.

Hiding the tabbar and removing the space

Is there a way to hide tabbar and remove that space left (around 50px) ?
I tried
self.tabBarController?.tabBar.hidden = true
self.extendedLayoutIncludesOpaqueBars = true
No luck. I see blank space.
If you're still seeing a black stripe under your hidden tab bar, have you tried to select Extend Edges Under Opaque Bars here?
Make also sure that Under Bottom Bars is still selected. Hope it helps!
Swift 3:
extension UITabBarController {
func setTabBarVisible(visible:Bool, duration: TimeInterval, animated:Bool) {
if (tabBarIsVisible() == visible) { return }
let frame = self.tabBar.frame
let height = frame.size.height
let offsetY = (visible ? -height : height)
// animation
UIViewPropertyAnimator(duration: duration, curve: .linear) {
self.tabBar.frame.offsetBy(dx:0, dy:offsetY)
self.view.frame = CGRect(x:0,y:0,width: self.view.frame.width, height: self.view.frame.height + offsetY)
self.view.setNeedsDisplay()
self.view.layoutIfNeeded()
}.startAnimation()
}
func tabBarIsVisible() ->Bool {
return self.tabBar.frame.origin.y < UIScreen.main.bounds.height
}
}
To use (if for example self is a UITabBarController):
self.setTabBarVisible(visible: false, duration: 0.3, animated: true)
Swift 2.x:
extension UITabBarController {
func setTabBarVisible(visible:Bool, duration: NSTimeInterval, animated:Bool) {
if (tabBarIsVisible() == visible) { return }
let frame = self.tabBar.frame
let height = frame.size.height
let offsetY = (visible ? -height : height)
// animation
UIView.animateWithDuration(animated ? duration : 0.0) {
self.tabBar.frame = CGRectOffset(frame, 0, offsetY)
self.view.frame = CGRectMake(0, 0, self.view.frame.width, self.view.frame.height + offsetY)
self.view.setNeedsDisplay()
self.view.layoutIfNeeded()
}
}
func tabBarIsVisible() ->Bool {
return self.tabBar.frame.origin.y < UIScreen.mainScreen().bounds.height
}
}
To use:
self.tabBarController?.setTabBarVisible(visible: false, duration: 0.3, animated: true)
After saw your screenshot in comment. I think you can try to set hidesBottomBarWhenPushed to true.
hidesBottomBarWhenPushed = true
Or storyboard.
It will hide bottom bar automatically when you pushed to another view controller, and appear it again when you go back.
Programmatically, add this to the next view controller for swift 4.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
tabBarController?.tabBar.isHidden = true
edgesForExtendedLayout = UIRectEdge.bottom
extendedLayoutIncludesOpaqueBars = true
}
And add a background color
NOTE - This solution is to just to remove white space left after hiding tab bar.
For hiding tab bar best solution is - #Michael Campsall answer here
The simplest solution to this is to change your view's(in my case its tableView) bottom constraints, instead of giving bottom constraints with BottomLayoutGuide give it with superview. Screenshots attached for reference.
Constraints shown in below screenshots creates the problem, change it according to next screenshot.
Actual constraints to remove white space should be according to this(below) screenshot.
For those that like to do everything programmatically, add this line to the init method of a ViewController that shouldn't have the tabBar:
hidesBottomBarWhenPushed = true
I was facing the same issue and root cause was BOTTOM CONSTRAINT
Make sure you set the bottom constraint of your bottom most view in the main view hierarchy with SUPERVIEW, NOT "SAFE AREA"
Hope this helps someone..
The third answer on this question works for me in the following way:
The code on my view controller
#IBAction func buttonPressed(sender: AnyObject) {
setTabBarVisible(!tabBarIsVisible(), animated: true)
}
func setTabBarVisible(visible: Bool, animated: Bool) {
// hide tab bar
let frame = self.tabBarController?.tabBar.frame
let height = frame?.size.height
var offsetY = (visible ? -height! : height)
print ("offsetY = \(offsetY)")
// zero duration means no animation
let duration:NSTimeInterval = (animated ? 0.3 : 0.0)
// animate tabBar
if frame != nil {
UIView.animateWithDuration(duration) {
self.tabBarController?.tabBar.frame = CGRectOffset(frame!, 0, offsetY!)
self.view.frame = CGRectMake(0, 0, self.view.frame.width, self.view.frame.height + offsetY!)
self.view.setNeedsDisplay()
self.view.layoutIfNeeded()
return
}
}
}
func tabBarIsVisible() -> Bool {
return self.tabBarController?.tabBar.frame.origin.y < UIScreen.mainScreen().bounds.height
}
In storyboard:
The view controller main view background color is black color:
Then you could have another view inside (background color white), constrained trailing and leading space to superview and top and bottom space to the layout guide.
And the result is:
My preferred way to do that is using a wrapping controller. If I want to hide the tab bar, I just increase the height of the tab bar controller, thus effectively the tab bar is moved out of the screen.
With this solution you don't need to hack tab bar frame and you don't depend on navigation controller push animation:
import UIKit
class ViewController: UIViewController {
let tabController: UITabBarController = {
let tabController = UITabBarController()
// setup your tabbar controller here
return tabController;
}()
var tabbarHidden = false {
didSet {
var frame = self.view.bounds;
if (tabbarHidden) {
frame.size.height += self.tabController.tabBar.bounds.size.height;
}
self.tabController.view.frame = frame;
}
}
override func viewDidLoad() {
super.viewDidLoad()
// add the tab controller as child controller
addChildViewController(self.tabController)
self.tabController.view.frame = self.view.bounds
self.tabController.view.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
self.view.addSubview(self.tabController.view)
self.tabController.didMoveToParentViewController(self)
// for debugging
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(switchTabbar))
self.tabController.view.addGestureRecognizer(tapRecognizer)
}
override func childViewControllerForStatusBarStyle() -> UIViewController? {
return self.tabController
}
override func childViewControllerForStatusBarHidden() -> UIViewController? {
return self.tabController
}
func switchTabbar() {
UIView.animateWithDuration(0.3) {
self.tabbarHidden = !self.tabbarHidden
}
}
}
try to set the tab bar translucent to before you hide the tab bar set to false again when you want to show again.
it works for me.
tabBarController?.tabBar.isTranslucent = true
Yes. You can hide your tab bar when you push to view controller. You can show tab bar in your home. You can hide your tab bar when you push to next View controller.
See the Hide Botton Bar on Push following image and set in all viewcontrollers where you dont want tab bar.
Hope it helps..
Sometimes that easiest way is just to add a view that uses the UIScreen bounds.
let whiteView = UIView()
whiteView.backgroundColor = .white
view.addSubview(whiteView)
whiteView.translatesAutoresizingMaskIntoConstraints = false
whiteView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
whiteView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
whiteView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
whiteView.heightAnchor.constraint(equalToConstant: UIScreen.main.bounds.height).isActive = true
Cause sometimes the view edges extends beyond the nav bar giving you new problems if you extend the view layout.
Tested in Swift 5.4.
If you're adding any ViewController's view as subview programmatically and not using pushViewController, then you can simply try as follows:
// When you wanna hide TabBar
tabBarController?.tabBar.isHidden = true
tabBarController?.tabBar.isTranslucent = true // This is the key point!
// When you wanna show TabBar
tabBarController?.tabBar.isHidden = false
tabBarController?.tabBar.isTranslucent = false // This is the key point!
This code works on iOS 10, 11, and iPhone X (including simulators) to show/hide the tabBar. I created it several years (iOS 7 time frame?) and it has worked reliably since that time.
It works great on iPhone X as long as content content in your childViewControllers (in tabs) is pinned to topLayoutGuide, bottomLayoutGuide or SafeArea and not the main views walls. Then it all just works. Enjoy!
#interface UITabBarController (HideTabBar)
#property (nonatomic, getter=isTabBarHidden) BOOL tabBarHidden;
-(void)setTabBarHidden:(BOOL)hidden animated:(BOOL)animated;
#end
#implementation UITabBarController (HideTabBar)
-(BOOL)isTabBarHidden
{
CGRect viewFrame = self.view.frame;
CGRect tabBarFrame = self.tabBar.frame;
return tabBarFrame.origin.y >= viewFrame.size.height;
}
-(void)setTabBarHidden:(BOOL)hidden
{
[self setTabBarHidden:hidden animated:NO];
}
-(void)setTabBarHidden:(BOOL)hidden animated:(BOOL)animated
{
BOOL isHidden = self.tabBarHidden;
if(hidden == isHidden)return;
UIView *transitionView = [[[self.view.subviews reverseObjectEnumerator] allObjects] lastObject];
if(transitionView == nil) {
NSLog(#"UITabBarCategory can't get the container view");
return;
}
CGRect viewFrame = self.view.bounds;
CGRect tabBarFrame = self.tabBar.frame;
CGRect containerFrame = transitionView.frame;
CGRect selectedVCFrame = containerFrame;
tabBarFrame.origin.y = viewFrame.size.height - (hidden ? 0 : tabBarFrame.size.height);
containerFrame.size.height = viewFrame.size.height - (hidden ? 0 : tabBarFrame.size.height);
if([self.moreNavigationController.viewControllers containsObject:self.selectedViewController]) {
selectedVCFrame = self.selectedViewController.view.frame;
selectedVCFrame.size.height += hidden ? tabBarFrame.size.height : -tabBarFrame.size.height;
}
self.selectedViewController.view.frame = selectedVCFrame;
[UIView animateWithDuration:.5 animations:^{
self.tabBar.frame = tabBarFrame;
transitionView.frame = containerFrame;
[self.selectedViewController.view setNeedsLayout];
}];
}
#end
Usage - I call it in the viewController on rotation events like so:
-(void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
[super didRotateFromInterfaceOrientation:fromInterfaceOrientation];
// Hide TabBar on iPhone, iPod Touch
if([UIDevice currentDevice].userInterfaceIdiom != UIUserInterfaceIdiomPad) {
if(_startDateEditor.editing) return;
if(fromInterfaceOrientation == UIInterfaceOrientationPortraitUpsideDown || fromInterfaceOrientation == UIInterfaceOrientationPortrait)
[self.tabBarController setTabBarHidden:YES animated:YES];
else
[self.tabBarController setTabBarHidden:NO animated:YES];
}
}
For me in iOS 13 I had to display image in cell with full screen, I had collection view with trailing, leading, top, bottom constraint. I removed all constraint. set collection view frame to UIScreen.main.bounds. then return sizeForItemAt as collection frame size.

Swift: Updating UIPageControl from ScrollView

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)

TableViewController not receiving touches

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()

Resources