How could a tableview.window is nil when i use tableViewController? [duplicate] - ios

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
}
}

Related

How to display UITableViewController as a swipe up gesture in front of another ViewController?

I am trying to get something like this to work. This is the Uber App. Where a user can swipe another view up, in front of a background view.
The background view is fairly simple, it has been done already. The view which will be swiped on top will be a UITableView. I want the user to be able to see just a little top part first when the app launches, then upon swiping a little it should stop in the middle and then after fully swiping up should take it all the way to the top, replacing the Background view.
Frameworks I have looked at are pullable view for iOS. But it is way too old and doesn't get any nice animations across. I have also looked at SWRevealViewController but I can't figure out how to swipe up from below.
I have also tried to use a button so when a user clicks on it, the table view controller appears modally, covering vertical, but that is not what I want. It needs to recognize a gesture.
Any help is greatly appreciated. Thanks.
I'm aware that the question is almost 2 and a half years old, but just in case someone finds this through a search engine:
I'd say that your best bet is to use UIViewPropertyAnimator. There's a great article about it here: http://www.swiftkickmobile.com/building-better-app-animations-swift-uiviewpropertyanimator/
EDIT:
I managed to get a simple prototype working with UIViewPropertyAnimator, here's a GIF of it:
Here's the project on Github: https://github.com/Luigi123/UIViewPropertyAnimatorExample
Basically I have two UIViewControllers, the main one called ViewController and the secondary one called BottomSheetViewController. The secondary view has an UIPanGestureRecognizer to make it draggable, and inside the recognizer's callback function I do 3 things (after actually moving it):
① calculate how much percent of the screen have been dragged,
② trigger the animations in the secondary view itself,
③ notify the main view about the drag action so it can trigger it's animations. In this case I use a Notification, passing the percentage inside notification.userInfo.
I'm not sure how to convey ①, so as an example if the screen is 500 pixels tall and the user dragged the secondary view up to the 100th pixel, I calculate that the user dragged it 20% of the way up. This percentage is exactly what I need to pass into the fractionComplete property inside the UIViewPropertyAnimator instances.
⚠️ One thing to note is that I couldn't make it work with an actual navigation bar, so I used a "normal" view with a label in it's place.
I tried making the code smaller by removing some utility functions like checking if the user interaction is finished, but that means that the user can stop dragging in the middle of the screen and the app wouldn't react at all, so I really suggest you see the entire code in the github repo. But the good news is that the entire code that executes the animations fits in about 100 lines of code.
With that in mind, here's the code for the main screen, ViewController:
import UIKit
import MapKit
import NotificationCenter
class ViewController: UIViewController {
#IBOutlet weak var someView: UIView!
#IBOutlet weak var blackView: UIView!
var animator: UIViewPropertyAnimator?
func createBottomView() {
guard let sub = storyboard!.instantiateViewController(withIdentifier: "BottomSheetViewController") as? BottomSheetViewController else { return }
self.addChild(sub)
self.view.addSubview(sub.view)
sub.didMove(toParent: self)
sub.view.frame = CGRect(x: 0, y: view.frame.maxY - 100, width: view.frame.width, height: view.frame.height)
}
func subViewGotPanned(_ percentage: Int) {
guard let propAnimator = animator else {
animator = UIViewPropertyAnimator(duration: 3, curve: .linear, animations: {
self.blackView.alpha = 1
self.someView.transform = CGAffineTransform(scaleX: 0.8, y: 0.8).concatenating(CGAffineTransform(translationX: 0, y: -20))
})
animator?.startAnimation()
animator?.pauseAnimation()
return
}
propAnimator.fractionComplete = CGFloat(percentage) / 100
}
func receiveNotification(_ notification: Notification) {
guard let percentage = notification.userInfo?["percentage"] as? Int else { return }
subViewGotPanned(percentage)
}
override func viewDidLoad() {
super.viewDidLoad()
createBottomView()
let name = NSNotification.Name(rawValue: "BottomViewMoved")
NotificationCenter.default.addObserver(forName: name, object: nil, queue: nil, using: receiveNotification(_:))
}
}
And the code for the secondary view (BottomSheetViewController):
import UIKit
import NotificationCenter
class BottomSheetViewController: UIViewController, UIGestureRecognizerDelegate {
#IBOutlet weak var navBarView: UIView!
var panGestureRecognizer: UIPanGestureRecognizer?
var animator: UIViewPropertyAnimator?
override func viewDidLoad() {
gotPanned(0)
super.viewDidLoad()
let gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(respondToPanGesture))
view.addGestureRecognizer(gestureRecognizer)
gestureRecognizer.delegate = self
panGestureRecognizer = gestureRecognizer
}
func gotPanned(_ percentage: Int) {
if animator == nil {
animator = UIViewPropertyAnimator(duration: 1, curve: .linear, animations: {
let scaleTransform = CGAffineTransform(scaleX: 1, y: 5).concatenating(CGAffineTransform(translationX: 0, y: 240))
self.navBarView.transform = scaleTransform
self.navBarView.alpha = 0
})
animator?.isReversed = true
animator?.startAnimation()
animator?.pauseAnimation()
}
animator?.fractionComplete = CGFloat(percentage) / 100
}
// MARK: methods to make the view draggable
#objc func respondToPanGesture(recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translation(in: self.view)
moveToY(self.view.frame.minY + translation.y)
recognizer.setTranslation(.zero, in: self.view)
}
private func moveToY(_ position: CGFloat) {
view.frame = CGRect(x: 0, y: position, width: view.frame.width, height: view.frame.height)
let maxHeight = view.frame.height - 100
let percentage = Int(100 - ((position * 100) / maxHeight))
gotPanned(percentage)
let name = NSNotification.Name(rawValue: "BottomViewMoved")
NotificationCenter.default.post(name: name, object: nil, userInfo: ["percentage": percentage])
}
}
EDIT: So, some time has passed and now there is a really awesome library called Pulley. It does exactly what I wanted it to do, and its a breeze to setup!
Original answer:
Thanks to both Rikh and Tj3n for giving me hints. I managed to do something very basic, it doesn't have nice animations like Uber but it gets the job done.
With the following code, you can swipe any UIViewController. I use a UIPanGestureRecognizer on my image, which will stay on top of the dragged view at all times. Basically, you use that image and it recognizes where it gets dragged, and it sets the view's frame according to the user's input.
First go to your storyboard and add an identifier for the UIViewController that will be dragged.
Then in the MainViewController, use the following code:
class MainViewController: UIViewController {
// This image will be dragged up or down.
#IBOutlet var imageView: UIImageView!
// Gesture recognizer, will be added to image below.
var swipedOnImage = UIPanGestureRecognizer()
// This is the view controller that will be dragged with the image. In my case it's a UITableViewController.
var vc = UIViewController()
override func viewDidLoad() {
super.viewDidLoad()
// I'm using a storyboard.
let sb = UIStoryboard(name: "Main", bundle: nil)
// I have identified the view inside my storyboard.
vc = sb.instantiateViewController(withIdentifier: "TableVC")
// These values can be played around with, depending on how much you want the view to show up when it starts.
vc.view.frame = CGRect(x: 0, y: self.view.frame.height, width: self.view.frame.width, height: -300)
self.addChildViewController(vc)
self.view.addSubview(vc.view)
vc.didMove(toParentViewController: self)
swipedOnImage = UIPanGestureRecognizer(target: self, action: #selector(self.swipedOnViewAction))
imageView.addGestureRecognizer(swipedOnImage)
imageView.isUserInteractionEnabled = true
}
// This function handles resizing of the tableview.
func swipedOnViewAction() {
let yLocationTouched = swipedOnImage.location(in: self.view).y
imageView.frame.origin.y = yLocationTouched
// These values can be played around with if required.
vc.view.frame = CGRect(x: 0, y: yLocationTouched, width: UIScreen.main.bounds.width, height: (UIScreen.main.bounds.height) - (yLocationTouched))
vc.view.frame.origin.y = yLocationTouched + 50
}
Final Product
Now, It is possible that my answer might not be the most efficient way of going at this, but I am new to iOS so this is the best I could come up with for the time being.
You can embed that table view inside a custom scroll view that will only handle touch when touch that table view part (override hittest), then drag it up (disable tableview scroll), till the upper part then disable scroll view and enable tableview scroll again
Or, you can just add the swipe gesture into your tableview and change it's frame along and disable swipe when it reach the top
Experiment with those and eventually you will achieve the effect you wanted
As Tj3n pointed out, you could use a UISwipeGesture to display the UITableView. So using constraints (instead of frames) heres how you could go about doing that:
Go to your UIViewController inside your Story board on which you wish to display the UITableView. Drag and drop the UITableView and add a leading, trailing and height to the UITableView. Now add a vertical constraint between the UIViewController and UITableView so that the UITableView appears below the UIViewController(Play around with this vertical value until you can display the top part of the UITableView to suit your need). Create outlets for the vertical spacing constraint and height constraint (in case you need to set a specific height that you can figure out at run time). On the swipe up just animatedly set the vertical constraint to be equal to the negative value of the height sort of like:
topSpaceToViewControllerConstraint.constant = -mainTableViewHeightConstraint.constant
UIView.animate(withDuration: 0.3) {
view.layoutIfNeeded()
};
Alternatively
If you want to be able to bring the UITableView up depending on the pan amount (i.e depending on how much the user has moved across the screen or how fast) you should use a UIPanGestureRecognizer instead and try and set frames instead of autoLayout for the UITableView (as I'm not a big fan of calling view.layoutIfNeeded repeatedly. I read somewhere that it is an expensive operation, would appreciate it if someone would confirm or correct this).
func handlePan(sender: UIPanGestureRecognizer) {
if sender.state == .Changed {
//update y origin value here based on the pan amount
}
}
Alternatively using UITableViewController
Doing what you wish to perform is also possible using a UITableViewController if you wish to but it involves a lot of faking and effort by creating a custom UINavigationControllerDelegate mainly to create a custom animation that will use UIPercentDrivenInteractiveTransition to pull the new UITableViewController up using a UIPanGestureRecognizer if you want it depending on the pan amount. Otherwise you can simply add a UISwipeGestureRecognizer to present the UITableViewController but you will still have to again create a custom animation to "fake" the effect you want.

Trouble displaying programmatically created views as topmost views?

I have a UIViewController (named Friends) with a tableview and I want to display views above the Controller's view when the user is offline. This is how I create my views
class func showOffline(host: UIViewController) {
// showOffline image
var disconnectedImage = UIImage(named: "offlineIconGrey")
disconnectedImage = disconnectedImage?.imageWithRenderingMode(.AlwaysOriginal)
let imageView = UIImageView(image: disconnectedImage)
imageView.center = CGPoint(x: host.view.center.x,
y: host.view.center.y - imageView.frame.height/2)
// showOffline Label
let Label = UILabel(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
let labelText = "You appear to be offline \n Please connect to the internet"
Label.center = CGPoint(x: host.view.center.x,
y: imageView.center.y + 3*imageView.frame.height/4)
Label.numberOfLines = 0
Label.text = labelText
Label.textAlignment = .Center
Label.font = UIFont(name: (Label.font?.fontName)!, size: 14.0)
Label.sizeToFit()
Label.textColor = UIColor.whiteColor()
Label.lineBreakMode = .ByCharWrapping
Label.frame.offsetInPlace(dx: -Label.frame.size.width/2, dy: 0)
host.view.insertSubview(imageView, aboveSubview: host.view)
host.view.insertSubview(Label, aboveSubview: host.view)
}
Now it is my understanding, that the host view (host.view) should be the topmost view as that is shown on my storyboard; however, when I call my showOffline function like so
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
if !Reachability.isConnected() {
state = .Offline
} else {
state = .Default
}
}
where offline state calls,
General.showOffline(self)
like so
var state: State = .Default {
didSet {
switch (state) {
case .Default:
print("ViewMode = Default")
//query = ParseHelper.allUsers(updateList)
case .Search:
let searchText = searchBar?.text ?? ""
print(searchText)
//query = ParseHelper.searchUsers(searchText, completionBlock:updateList)
case .Offline:
General.showOffline(self)
searchBar.userInteractionEnabled = false
query = nil
users = [PFUser]() ?? []
print("ViewMode = Offline")
}
}
}
the views do not appear as the topmost views unless I am returning to the view controller (Friends) from a show segue. Otherwise, navigating to that view controller does not show the labels when it should? Am I missing something? I have tried to fix the problem by placing the views above the tableview but in that case the math for centering the views is off, probably because I am not loading any cells and I have a footer view as a UIView. I also tried adding the views to the tableview's footer view, but that didn't work either. What I DON'T want is to add the views to the navigation controller's view as I do not want that behavior, though I will do that if I have no other option.
This is what I want
Can someone please tell me what I am doing wrong or refer me to information that may help me fix my problem? Thanks!
host.view.insertSubview(imageView, aboveSubview: host.view)
This cannot work. You add a subview to host.view. The "aboveSubview" must be a subview of host.view. There are other methods for g
adding a subview.
When you call insertSubview:aboveSubview:, the view you give as second parameter must be a subview of the view you're inserting in. Here, it's the same view, so obviously it won't work.
Unless there are other views that should remain above the ones you're inserting, the easiest way is to just use addSubview:, which will make the new view topmost among the child of the target view.

How to display UIView over keyboard in iOS

I want to create a simple view over keyboard, when users tap "Attach" button in inputAccessoryView.
Something like this:
Is there an easy way to do it? Or i should create my custom keyboard?
You can add that new subview to your application window.
func attach(sender : UIButton)
{
// Calculate and replace the frame according to your keyboard frame
var customView = UIView(frame: CGRect(x: 0, y: self.view.frame.size.height-300, width: self.view.frame.size.width, height: 300))
customView.backgroundColor = UIColor.redColor()
customView.layer.zPosition = CGFloat(MAXFLOAT)
var windowCount = UIApplication.sharedApplication().windows.count
UIApplication.sharedApplication().windows[windowCount-1].addSubview(customView);
}
Swift 4 version:
let customView = UIView(frame: CGRect(x: 0, y: self.view.frame.size.height - 300, width: self.view.frame.size.width, height: 300))
customView.backgroundColor = UIColor.red
customView.layer.zPosition = CGFloat(Float.greatestFiniteMagnitude)
UIApplication.shared.windows.last?.addSubview(customView)
The trick is to add the customView as a top subview to the UIWindow that holds the keyboard - and it happens to be the last window in UIApplication.shared.windows.
Swift 4.0
let customView = UIView(frame: CGRect(x: 0, y: self.view.frame.size.height-300, width: self.view.frame.size.width, height: 300))
customView.backgroundColor = UIColor.red
customView.layer.zPosition = CGFloat(MAXFLOAT)
let windowCount = UIApplication.shared.windows.count
UIApplication.shared.windows[windowCount-1].addSubview(customView)
As Tamás Sengel said, Apple's guidelines does not support adding a view over the keyboard. The recommended way to add a view over keyboard in Swift 4 & 5 is:
1) Add view with your "Next" button in your storyboard as external view and connect in your class (see Explain Image), in my case:
IBOutlet private weak var toolBar: UIView!
2) For the textfield you want to add your custom view over keyboard, add it as accessory view in viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
phoneNumberTextField.inputAccessoryView = toolBar
}
3) Add action for "Next" button:
#IBAction func nextButtonPressed(_ sender: Any) {
descriptionTextView.becomeFirstResponder()
// or -> phoneNumberTextField.resignFirstResponder()
}
Explain Image:
Method 2: Result with image
In TableView Controller - add stricked view at bottom
Please follow this great link to handle safe area for screens like iPhone X if you want to use this method(2). Article: InputAccessoryView and iPhone X
override var inputAccessoryView: UIView? {
return toolBar
}
override var canBecomeFirstResponder: Bool {
return true
}
Do you have find some effective method to solve this problem? In iOS9,you put your customView on the top of the windows:
UIApplication.sharedApplication().windows[windowCount-1].addSubview(customView);
But if the keyboard dismisses, the top Windows will be removed, so your customView will be removed.
Looking forward for your help!
Thank you for your help!
You can definitely add the view to your application’s window, and you can also add another window entirely. You can set its frame and level. The level could be UIWindowLevelAlert.
While this can be possible with accessing the topmost window, I would avoid doing this, as it clearly interferes with Apple's guidelines.
What I would do is dismissing the keyboard and replacing its frame with a view with same dimensions.
The keyboard's frame can be accessed from keyboard notifications listed here, their userInfo contain a key that can be accessed with UIKeyboardFrameEndUserInfoKey.

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

Add button on top of UITableViewController (Swift)

I am trying to add a button ontop of a uitableview controller table view. The view controller has a navigation controller and static cells, which is why it is a uitableviewcontroller and not a uiviewcontroller. Now I am trying to add a button at the bottom of the screen that is attached to the navigation controller so that it doesn't scroll with the table view.
I am trying to make something similar to what is below. It has a navigation controller for the top bar, a table view with static cells and then a button, but how did they do the button?
Image: http://postimg.org/image/ilsmqqrip/
Thanks!
UPDATE: How can I use a uiviewcontroller with a tableview with static cells using Swift?
I find Container Views very useful in this scenario! A clean solution and very easy to implement.
Just create a normal UIViewController, add your button and a ContainerView as subviews of this UIViewController (the middle one in the image below). Finally create Embed Segue from ContainerView to your UITableViewController (the one on the right).
This way you can use static cell prototypes, not being limited only to UITableView at the same time.
Result:
there is a better solution for this. you can do this by disabling the Auto Layout(button.translatesAutoresizingMaskIntoConstraints = false) property of the corresponding Button or any UIView for floating button:
Swift 4
//create a button or any UIView and add to subview
let button=UIButton.init(type: .system)
button.setTitle("NEXT", for: .normal)
button.frame.size = CGSize(width: 100, height: 50)
self.view.addSubview(button)
//set constrains
button.translatesAutoresizingMaskIntoConstraints = false
if #available(iOS 11.0, *) {
button.rightAnchor.constraint(equalTo: tableView.safeAreaLayoutGuide.rightAnchor, constant: -10).isActive = true
button.bottomAnchor.constraint(equalTo: tableView.safeAreaLayoutGuide.bottomAnchor, constant: -10).isActive = true
} else {
button.rightAnchor.constraint(equalTo: tableView.layoutMarginsGuide.rightAnchor, constant: 0).isActive = true
button.bottomAnchor.constraint(equalTo: tableView.layoutMarginsGuide.bottomAnchor, constant: -10).isActive = true
}
I did something similar with UITableViewController and a static datasource. I added the button in the footerview of my tableview.
To make it align to the bottom of the screen i needed this code in my viewcontroller:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
// Make footerview so it fill up size of the screen
// The button is aligned to bottom of the footerview
// using autolayout constraints
self.tableView.tableFooterView = nil
self.footerView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.tableView.frame.size.height - self.tableView.contentSize.height - self.footerView.frame.size.height)
self.tableView.tableFooterView = self.footerView
}
In short, I resize the footerview to take up all the remaining space after the contentsize of the table view is removed. Since the button is aligned to the bottom of the footerView with autolayout, it will stay in the bottom of the screen.
The Storyboard:
Here is the result:
The UITableViewController will take up the whole space, so you won't be able to add the button. Refactor your UITableViewController based code into UIViewController with UITableView manually added. This way you will be able to set the size of your table view and put the button to the bottom.
Unfortunately UITableViewController has a tableView as its top level view. Of course if you look in the view debugger you can see that the tableview is not the root view. Therefore you can add the buttons to the tableView's window programatically. If you have to do it after the fact, this is probably the easiest way to add a top level element over a UITableViewController. Otherwise if you are doing it in the initial design, you can use container view for your buttons and a UITableViewController for the TableView. The downside of this approach is you end up with two view controllers, one for the container and one for the table and its often necessary to pass information back and for between them. If you are using swift you can simplify this by nesting the tableViewcontroller inside the container view controller class.
If you want to add a button to the window, you can do this lazily once you are sure that the view has a window. Note that the buttons belong to the window and not to the view controller, so its your responsibility to remove them when the view controller disappears.
private weak var button: UIButton!
...
override func didMove(toParentViewController parent: UIViewController?) {
super.didMove(toParentViewController: parent)
guard self.button == nil, let window = tableView.window else {
return
}
let button = UIButton(frame: CGRect(x:0, y:40, width: 200, height: 20))
button.setTitle("This is a red button", for: .normal)
button.backgroundColor = UIColor.red
window.addSubview(button)
self.button = button
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
button?.removeFromSuperview()
}
Step 1 :-
Drag and drop one uiview to UITable View Controller (Static)
Automatically it sticks to the bottom.
If you need to, you can also add two buttons inside UIView... It depends on your requirements.
Step 2 :-
Connect the outlet for uiview (outletView)
Step 3 :-
Add this below code in View Will Appear.
override func viewWillAppear(_ animated: Bool) {
outletViewBottom.backgroundColor = .red
tableView.addSubview(outletViewBottom)
// set position
outletView.translatesAutoresizingMaskIntoConstraints = false
outletView.leftAnchor.constraint(equalTo: tableView.safeAreaLayoutGuide.leftAnchor).isActive = true
outletView.rightAnchor.constraint(equalTo: tableView.safeAreaLayoutGuide.rightAnchor).isActive = true
outletView.bottomAnchor.constraint(equalTo: tableView.safeAreaLayoutGuide.bottomAnchor).isActive = true
outletView.widthAnchor.constraint(equalTo: tableView.safeAreaLayoutGuide.widthAnchor).isActive = true
outletView.heightAnchor.constraint(equalToConstant: 50).isActive = true // specify the height of the view
}
Step 4 :-
Now run the code... Happy coding.
all you need to do is to add your Top view whichever it is to the navigationController.view like so:
self.navigationController?.view.addSubview(YOUR_TOP_VIEW)
so if you need a sticky button/view etc... on top of TableViewController which does not scroll with tableView, use this approach.
Here is a UIViewController, with a UITableView added as a subview. At the top right, you can see a dropdown that says Content: Dynamic Prototypes. Change it to Static Cells.

Resources