Why is my uitableview cut off with too many cells ? (swift) - ios

I have a view controller with an uitableview to display some comments. I use storyboard and autolayout.
The height of cells depends on the content.
When I have many cells, my tableview is cut off, and not displayed fully. But it's correct with less cells.
In viewDidLoad :
self.commentsTableView.estimatedRowHeight = 93.0
self.commentsTableView.rowHeight = UITableViewAutomaticDimension
In viewDidAppear, I tried 2 approaches, first with the height constraint :
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
self.commentsTableView.removeConstraint(self.tableViewHeightConstraint)
self.tableViewHeightConstraint = NSLayoutConstraint(item: self.commentsTableView, attribute: NSLayoutAttribute.Height, relatedBy: NSLayoutRelation.Equal, toItem: nil, attribute: NSLayoutAttribute.NotAnAttribute, multiplier: 1, constant: self.commentsTableView.contentSize.height)
self.commentsTableView.addConstraint(self.tableViewHeightConstraint)
self.commentsTableView.setNeedsUpdateConstraints()
self.commentsTableView.updateConstraintsIfNeeded()
}
I tried also with the height of the frame (without height constraint) :
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
var frame:CGRect = self.commentsTableView.frame
frame.size.height = commentsTableView.contentSize.height
self.commentsTableView.frame = frame
}
I have exactly the same result with the 2 approaches.
When there are too many cells, the tableview is not displayed fully.
EDIT
A screenshot. In blue it's my scrollview.

I found a solution.
In my viewDidAppear, I set first the frame of the tableview depending its content size, then I set the size of the scrollview with the height of the tableview.
The solution is to do the opposite, first set the scrollview to the content size of the tableview, then adjust the tableview with the height of the scrollview. And it works !
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
self.scrollView.contentSize=CGSizeMake(self.scrollView.frame.size.width, self.commentsTableView.frame.origin.y + commentsTableView.contentSize.height)
var frame:CGRect = self.commentsTableView.frame
frame.size.height = scrollView.contentSize.height
self.commentsTableView.frame = frame
}

Related

View on Top of UITabBar

Similar to what the Spotify or Apple Music app does when a song is playing, it places a custom view on top of the UITabBar:
Solutions I've tried:
UITabBarController in a ViewController with a max-sized Container View, and the custom view on top of the Container View49pt above the Bottom Layout Guide:
Problem: Any content in ViewControllers embedded in the UITabBarController constrained to the bottom don't show because they're hidden behind the custom layout. I've tried overriding size forChildContentContainer in UITabBarController, tried updating the bottom layout guide, Nothing. I need to resize the frame of container view of the UITabBarController.
Tried #1 again, but tried solving the problem of content hiding behind it by increasing the size of UITabBar, and then using ImageInset on every TabBarItem to bring it down, and adding my custom view on top of the UITabBar. Hasn't worked really well. There are going to be times when I want to hide my custom view.
UITabBarController as root, with each children being a ViewController with a Container View + my custom view:
But now I have multiple instances of my custom view floating around. If I want to change a label on it, have to change it to all views. Or hide, etc.
Override the UITabBar property of UITabBarController and return my custom UITabBar (inflated it with a xib) that has a UITabBar + my custom view. Problem: Probably the most frustrating attempt of all. If you override that property with an instance of class MyCustomTabBar : UITabBar {}, no tab shows up! And yes, I set the delegate of myCustomTabBar to self.
Leaning towards #3, but looking for a better solution.
This is actually very easy if you subclass UITabBarController and add your view programmatically. Using this technique automatically supports rotation and size changes of the tab bar, regardless of which version you are on.
class CustomTabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
//...do some of your custom setup work
// add a container view above the tabBar
let containerView = UIView()
containerView.backgroundColor = .red
view.addSubview(containerView)
containerView.translatesAutoresizingMaskIntoConstraints = false
containerView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
containerView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
// anchor your view right above the tabBar
containerView.bottomAnchor.constraint(equalTo: tabBar.topAnchor).isActive = true
containerView.heightAnchor.constraint(equalToConstant: 50).isActive = true
}
}
I got it!
In essence, I increased the size of the original UITabBar to accomodate a custom view (and to shrink the frame of the viewcontrollers above), and then adds a duplicate UITabBar + custom view right on top of it.
Here's the meat of what I had to do. I uploaded a functioning example of it and can be found in this repo:
class TabBarViewController: UITabBarController {
var currentlyPlaying: CurrentlyPlayingView!
static let maxHeight = 100
static let minHeight = 49
static var tabbarHeight = maxHeight
override func viewDidLoad() {
super.viewDidLoad()
currentlyPlaying = CurrentlyPlayingView(copyFrom: tabBar)
currentlyPlaying.tabBar.delegate = self
view.addSubview(currentlyPlaying)
tabBar.isHidden = true
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
currentlyPlaying.tabBar.items = tabBar.items
currentlyPlaying.tabBar.selectedItem = tabBar.selectedItem
}
func hideCurrentlyPlaying() {
TabBarViewController.tabbarHeight = TabBarViewController.minHeight
UIView.animate(withDuration: 0.5, animations: {
self.currentlyPlaying.hideCustomView()
self.updateSelectedViewControllerLayout()
})
}
func updateSelectedViewControllerLayout() {
tabBar.sizeToFit()
tabBar.sizeToFit()
currentlyPlaying.sizeToFit()
view.setNeedsLayout()
view.layoutIfNeeded()
viewControllers?[self.selectedIndex].view.setNeedsLayout()
viewControllers?[self.selectedIndex].view.layoutIfNeeded()
}
}
extension UITabBar {
open override func sizeThatFits(_ size: CGSize) -> CGSize {
var sizeThatFits = super.sizeThatFits(size)
sizeThatFits.height = CGFloat(TabBarViewController.tabbarHeight)
return sizeThatFits
}
}
Since iOS 11 this became a little easier. When you add your view, you can do the following:
viewControllers?.forEach {
$0.additionalSafeAreaInsets = UIEdgeInsets(
top: 0,
left: 0,
bottom: yourView.height,
right: 0
)
}
Your idea to put it in a wrapper viewcontroller is good, but it will only cause overhead (more viewcontrollers to load in memory), and issues when you want to change the code later on. If you want the bar to always show on your UITabBarController, then you should add it there.
You should subclass UITabBarController and load the custom bar from a nib. There you will have access to the tabbar (so you can place your bar correctly above it), and you will only load it in once (which solves your problem that you will face having a different bar on each tab).
As for your views not reacting to the size of the custom bar, I don't know how you can do that, but my best suggestion is to use a public variable and notifications that you listen to in your individual tabs.
You can then use that to change the bottom constraint.
Besides playing with UITabBar or container vc, you could also consider adding the view in the App Delegate to the main window like in following post:
View floating above all ViewControllers
Since your view is all around along with the Tab bar, it is totally ok to make it in the App Delegate.
You can always access the Floating view from App Delegate Singleton by making it a property of the App Delegate. It is easy then to control its visibility in anywhere of your code.
Changing constant of the Constraints between the Floating view and super view window can adjust the position of the view, thus handsomely respond to orientation changes.
Another(similar) approach is to make the floating view another window like the uid button.
Unless I've misunderstood, you could create a custom view from your UITabBarController class. You can then insert it above and constrain it to the tabBar object, which is the tabBar associated with the controller.
So from your UITabBarController class, create your custom view
class CustomTabBarController: UITabBarController {
var customView: UIView = {
let bar = UIView()
bar.backgroundColor = .white
bar.translatesAutoresizingMaskIntoConstraints = false
return bar
}()
In viewDidLoad() add your custom view to the UITabBarController's view object and place it above the tabBar object
override func viewDidLoad() {
super.viewDidLoad()
...
self.view.insertSubview(customView, aboveSubview: tabBar)
Then after your custom view is added as a subView, add constraints so it's positioned correctly. This should also be done in viewDidLoad() but only after your view is inserted.
self.view.addConstraints([
NSLayoutConstraint(item: customView, attribute: .leading, relatedBy: .equal, toItem: tabBar, attribute: .leading, multiplier: 1, constant: 0),
NSLayoutConstraint(item: customView, attribute: .trailing, relatedBy: .equal, toItem: tabBar, attribute: .trailing, multiplier: 1, constant: 0),
NSLayoutConstraint(item: customView, attribute: .top, relatedBy: .equal, toItem: tabBar, attribute: .top, multiplier: 1, constant: -50),
NSLayoutConstraint(item: customView, attribute: .bottom, relatedBy: .equal, toItem: tabBar, attribute: .top, multiplier: 1, constant: 0)
])
There's a bunch of creative ways you can setup constraints to do what you want, but the constraints above should attach a view above your tabBar with a height of 50.
Make the view's frame with the height of tab bar and brings it to top, 2. set tabBar hidden is true.

Attaching an UIimage behind a UItexView in tableview

I have attached a UItextView in tableview cell. Now I want to insert an UIimage behind it like a chat bubble.But everytime I scroll the tableview , the Uitextview becomes smaller in width and longer in height. The text also gets resized from single to multi line. Also ,the same thing happens when I scroll back to an earlier cell ( for example the first row).
I can't figure out why this is happening.
My code is:
(SampleTableViewCell is my custom UItableViewCell class and lblTitle is my UitextView outlet.)
func tableView(tableView: UITableView, willDisplayCell cell: SampleTableViewCell, forRowAtIndexPath indexPath: NSIndexPath)
{
cell.lblTitle.removeConstraints(cell.lblTitle.constraints)
let stringTitle = carName[indexPath.row] as String //carName is an array
cell.lblTitle.text=stringTitle
cell.lblTitle.sizeToFit()
let h = cell.lblTitle.frame.size.height
let w = cell.lblTitle.frame.size.width
let constraints = NSLayoutConstraint(item: cell.lblTitle, attribute: .Width, relatedBy: .Equal, toItem: cell.lblTitle, attribute: .Height, multiplier: 0, constant: w)
cell.lblTitle.addConstraint(constraints)
let kl = UIImage(imageLiteral: "bubble")
let imageView = UIImageView(frame:CGRectMake(0, 0, w, h))
imageView.image = kl
imageView.alpha = 0.4
cell.lblTitle.addSubview(imageView)
cell.lblTitle.sendSubviewToBack(imageView)
}
It is the problem of autolayout.
You are just giving height and width constraint to textView. what about x and y position?
And you haven't set constraints for your imageview!!
If you are giving constraint to one object then you required to give every object related to it.
So set proper constraints and your problem will be solved.
Instead of adding bubble imageview and constraints programmatically,using storyboard you can add imageview in the tableview cell first and then UITextView of same size as imageview and add constraints.Then you can add your logic at cellForRowAtIndexPath delegate for lblTitle's text and bubble image. Make sure to set lblTitle's background to ClearColor.

Display ADBannerView with UITableViewController inside UITabBarController

EDIT
Thanks to #LeoNatan I have now got a complete working solution. If anyone finds this and would like the solution, it's available on GitHub.
Original Question
I'm trying to get iAds (or any other view for that matter, although it may be specific to ADBannerView) to be displayed just above a UITabBar. I've gone about a few different ways of doing this, but haven't come up with a solution that satifies the following:
Works on iOS 7 and 8
Works with and without the iAd displayed
Works in landscape and portrait
Works on iPhone and iPad
UITableViews insets correctly update
The only solution I have so far that has worked has been to have my UITableView inside a UIViewController, and adding the UITableView and ADBannerView to the view property of the UIViewController. I moved away from this for 2 reasons:
The UITableView did not extend its edges below the bottom UITabBar
I need to subclass UITableViewController, not UIViewController
I have a bannerView property on my AppDelegate and a shouldShowBannerView property to decide whether or not to show the iAd, and share a single instance. The AppDelegate then sends out notifications when iAds should be displayed or hidden (i.e., when an iAd is loaded and when the user has paid to remove the iAds). The "base" of the code works as such:
func showiAds(animated: Bool) {
if !self.showingiAd {
let delegate = UIApplication.sharedApplication().delegate as AppDelegate
if let bannerView = delegate.bannerView {
println("Showing iAd")
self.showingiAd = true
if (bannerView.superview != self.view) {
bannerView.removeFromSuperview()
}
// let bannersSuperview = self.view.superview! // Bottom inset incorrect
let bannersSuperview = self.view // Banner is shown at the top screen. Crashes on iOS 7 (at bannersSuperview.layoutIfNeeded())
// let bannersSuperview = self.tableView // The is the same as self.view (duh)
// let bannersSuperview = self.tabBarController!.view // Bottom inset incorrect
// Added the view and the left/right constraints allow for the proper height
// to be returned when bannerView.frame.size.height is called (iOS 7 fix mainly)
bannersSuperview.addSubview(bannerView)
bannersSuperview.addConstraints([
NSLayoutConstraint(item: bannerView, attribute: .Left, relatedBy: .Equal, toItem: bannersSuperview, attribute: .Left, multiplier: 1, constant: 0),
NSLayoutConstraint(item: bannerView, attribute: .Right, relatedBy: .Equal, toItem: bannersSuperview, attribute: .Right, multiplier: 1, constant: 0),
])
bannersSuperview.layoutIfNeeded()
let bannerViewHeight = bannerView.frame.size.height
var offset: CGFloat = -self.bottomLayoutGuide.length
if (UIDevice.currentDevice().systemVersion as NSString).floatValue < 8 {
// Seems to be needed for some reason
offset -= bannerViewHeight
}
let bannerBottomConstraint = NSLayoutConstraint(item: bannerView, attribute: .Bottom, relatedBy: .Equal, toItem: bannersSuperview, attribute: .Bottom, multiplier: 1, constant: offset + bannerViewHeight)
// self.bannerBottomConstraint = bannerBottomConstraint
bannersSuperview.addConstraint(bannerBottomConstraint)
bannersSuperview.layoutSubviews()
// bannerSuperview.setNeedsLayout()
bannersSuperview.layoutIfNeeded()
// Previously, this values was the height of the banner view, so that it starts off screen.
// Setting this to 0 and then doing an animation makes it slide in from below
bannerBottomConstraint.constant = offset
bannersSuperview.setNeedsUpdateConstraints()
UIView.animateWithDuration(animated ? 10 : 0, animations: { () -> Void in
// Calling layoutIfNeeded here will animate the layout constraint cosntant change made above
bannersSuperview.layoutIfNeeded()
})
} else {
println("Cannot show iAd when bannerView is nil")
}
}
}
func hideiAds() {
if self.showingiAd {
self.showingiAd = false
let delegate = UIApplication.sharedApplication().delegate as AppDelegate
if let bannerView = delegate.bannerView {
if bannerView.superview == self.view {
bannerView.removeFromSuperview()
}
}
}
}
I then check in my viewWillAppear: and viewDidDisappear: methods if an iAds is/should be displayed and calling showiAds(false) and hideiAds() as required.
No matter what I do, I don't seem to be able to get it to work. A couple of other things I've tried but scrapped the code for:
Adding the iAd in the UITabBarController, which then alerts the UITableViewControllers that the iAd was shown/hidden. Modifying the content/scroll indicator insets did not work well, and was ofter reset by the UITableViewController to fit above/below the navigation/tab bar.
(as above) setting the content/scroll indicator insets myself, but I could not get it consistent without attempting to emulate (using (top|bottom)LayoutGuide) in viewDidLayoutSubviews, but this seems very costly?
I did, at one point, have it working by adding the ADBannerView to some view from within the UITableViewController, but it would crash on iOS 7 (something about tableView must call super -layoutSubviews)
EDIT
I have created a UIViewController subclass with the intent of using it to house UITableViewControllers via a Container View. Here is what I have so far, followed by a couple of issues:
class AdvertContainerViewController: UIViewController {
var tableViewController: UITableViewController?
var showingiAd = false
var bannerBottomConstraint: NSLayoutConstraint?
private var bannerTopOffset: CGFloat {
get {
var offset: CGFloat = 0
if let tabBar = self.tabBarController?.tabBar {
offset -= CGRectGetHeight(tabBar.frame)
}
if let bannerView = AppDelegate.instance.bannerView {
let bannerViewHeight = bannerView.frame.size.height
offset -= bannerViewHeight
}
return offset
}
}
override func viewDidLoad() {
super.viewDidLoad()
if self.childViewControllers.count > 0 {
if let tableViewController = self.childViewControllers[0] as? UITableViewController {
self.tableViewController = tableViewController
tableViewController.automaticallyAdjustsScrollViewInsets = false
self.navigationItem.title = tableViewController.navigationItem.title
}
}
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
if AppDelegate.instance.shouldShowBannerView {
self.showiAds(false)
}
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
let delegate = AppDelegate.instance
NSNotificationCenter.defaultCenter().addObserver(self, selector: "showiAds", name: "BannerViewDidLoadAd", object: delegate)
NSNotificationCenter.defaultCenter().addObserver(self, selector: "hideiAds", name: "RemoveBannerAds", object: delegate)
}
override func viewDidDisappear(animated: Bool) {
super.viewDidDisappear(animated)
if self.showingiAd {
self.hideiAds()
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
println("View did layout subviews")
if self.showingiAd {
if let bannerView = AppDelegate.instance.bannerView {
let bannerViewHeight = CGRectGetHeight(bannerView.frame)
if let bottomConstraint = self.bannerBottomConstraint {
let bannerTopOffset = self.bottomLayoutGuide.length + bannerViewHeight
if bottomConstraint.constant != bannerTopOffset {
println("Setting banner top offset to \(bannerTopOffset)")
bottomConstraint.constant = -bannerTopOffset
bannerView.superview?.setNeedsUpdateConstraints()
bannerView.superview?.updateConstraintsIfNeeded()
}
}
println("Bottom layout guide is \(self.bottomLayoutGuide.length)")
let insets = UIEdgeInsetsMake(self.topLayoutGuide.length, 0, self.bottomLayoutGuide.length + bannerViewHeight, 0)
self.updateTableViewInsetsIfRequired(insets)
}
}
}
private func updateTableViewInsetsIfRequired(insets: UIEdgeInsets) {
if let tableView = self.tableViewController?.tableView {
if !UIEdgeInsetsEqualToEdgeInsets(tableView.contentInset, insets) {
println("Updating content insets to \(insets.top), \(insets.bottom)")
tableView.contentInset = insets
}
if !UIEdgeInsetsEqualToEdgeInsets(tableView.scrollIndicatorInsets, insets) {
println("Updating scroll insets to \(insets.top), \(insets.bottom)")
tableView.scrollIndicatorInsets = insets
}
}
}
func showiAds() {
self.showiAds(true)
// self.showiAds(false)
}
func showiAds(animated: Bool) {
if !self.showingiAd {
let delegate = UIApplication.sharedApplication().delegate as AppDelegate
if let bannerView = delegate.bannerView {
println("Showing iAd")
self.showingiAd = true
if (bannerView.superview != self.view) {
bannerView.removeFromSuperview()
}
let bannersSuperview = self.view.superview!
// Added the view and the left/right constraints allow for the proper height
// to be returned when bannerView.frame.size.height is called (iOS 7 fix mainly)
bannersSuperview.addSubview(bannerView)
bannersSuperview.addConstraints([
NSLayoutConstraint(item: bannerView, attribute: .Left, relatedBy: .Equal, toItem: bannersSuperview, attribute: .Left, multiplier: 1, constant: 0),
NSLayoutConstraint(item: bannerView, attribute: .Right, relatedBy: .Equal, toItem: bannersSuperview, attribute: .Right, multiplier: 1, constant: 0),
])
bannersSuperview.layoutIfNeeded()
let bannerBottomConstraint = NSLayoutConstraint(item: bannerView, attribute: .Top, relatedBy: .Equal, toItem: bannersSuperview, attribute: .Bottom, multiplier: 1, constant: 0)
self.bannerBottomConstraint = bannerBottomConstraint
bannersSuperview.addConstraint(bannerBottomConstraint)
bannersSuperview.layoutSubviews()
bannersSuperview.layoutIfNeeded()
let topInset = self.navigationController?.navigationBar.frame.size.height ?? 0
let insets = UIEdgeInsetsMake(topInset, 0, -self.bannerTopOffset, 0)
// Previously, this values was the height of the banner view, so that it starts off screen.
// Setting this to 0 and then doing an animation makes it slide in from below
bannerBottomConstraint.constant = self.bannerTopOffset
bannersSuperview.setNeedsUpdateConstraints()
UIView.animateWithDuration(animated ? 0.5 : 0, animations: { () -> Void in
// Calling layoutIfNeeded here will animate the layout constraint cosntant change made above
self.updateTableViewInsetsIfRequired(insets)
bannersSuperview.layoutIfNeeded()
})
} else {
println("Cannot show iAd when bannerView is nil")
}
}
}
func hideiAds() {
if self.showingiAd {
self.showingiAd = false
let delegate = UIApplication.sharedApplication().delegate as AppDelegate
if let bannerView = delegate.bannerView {
if bannerView.superview == self.view {
bannerView.removeFromSuperview()
}
}
}
}
}
Issues so far:
Using self.view as the superview causes a crash on rotate Auto Layout still required after sending -viewDidLayoutSubviews to the view controller. Gathered.AdvertContainerViewController's implementation needs to send -layoutSubviews to the view to invoke auto layout.
I'm not calculating the content insets correctly; when the iAd is shown, the top jumps up slightly and the bottom in below the top of the banner
The table view doesn't show the scroll indicators. This seems to be a known issue but I cannot find a solution
At the request of Leo Natan I have create a repo on GitHub that I will update with any attempts I make, and explain issues here. Currently, the issues are as follows:
First Tab:
Top of table moves down when iAd is shown (iOS 8)
Table cannot be scrolled (iOS 7)
Top of table view jumps when iAd shows (iOS 7)
Rotation often breaks the offset of the iAd, hiding it behind the tab bar (iOS 7 and 8)
Second Tab:
There are no scroll bars (iOS 7 and 8)
Scroll inset it not set (iOS 7)
Rotation often breaks the offset of the iAd, hiding it behind the tab bar (iOS 7 and 8)
The best solution is to use view controller containment. Use a view controller subclass that will house both the ad view and the table view controller's view, and add the table view controller as a child of the container view controller. This should take care of content insets correctly. On each layout of the container controller's view, position the table controller view hierarchy correctly after positioning the ad view. If you wish to hide the ad view, simply hide or remove it from the container hierarchy, and extend the table controller's view hierarchy fully. When working with hierarchies, remember to always use the table controller's view and not the tableView directly.
My answer was adapted into the following GitHub repo:
https://github.com/JosephDuffy/iAdContainer
The best that is that you download the AD suite from Apple site, there are tabbar controller and navigation controller containment example.
Apple provides you an abstract view controller that can handle by itself the ADBanner flow without interrupting its presentation, maximizing the showing time.
You can use this https://developer.apple.com/library/ios/samplecode/iAdSuite/Introduction/Intro.html apple sample and modified it according to your needs. Such as bool variable to take care of when iAds is shown or not.
There in code you can see BannerViewController class that contains all the logic. You can also write ADmob code there to use.

How to animate a UIView with constraints in Swift?

In my contrived example, I have the following single view:
As you can see, it consists of a few simple constraints:
Align horizontal and vertical centers,
Height (set to a constant)
Leading and trailing spaces (set to constant)
What I'm seeking to achieve is to have this redish/pinkish view "come in" from the top. Traditionally, in a constraint-less world, I would simply modify the frame inside of UIView.animateWithDuration, however I'm not sure how I do the similar in a constraint world.
To reiterate my question, how can I make my view start out of scene and animate the view flying in from the top?
I've considered animating the vertical centers constraint (and subsequently calling layoutIfNeeded), but it's not achieving the desired effect.
Thanks for your help.
What you can do is to add the following code in your viewDidAppear method. You firstly make a IBOutlet property of your view's center Y constraint, and then change its constant value.
self.centerYConstraint.constant = 500.0
self.view.layoutIfNeeded()
UIView.animateWithDuration(Double(0.5), animations: {
self.centerYConstraint.constant = 0
self.view.layoutIfNeeded()
})
What you want is in fact rather simple, and I will detail how it should be done without messing with priorities or funky constants. This way we can get an animation that will work on any screen size.
The TL;DR is that you need to configure your constraints and then call layoutIfNeeded inside your animation block to animate the changes. For more see this SO post.
So we need two sets of constraints for hiding and showing the view. In viewDidLoad the view will be hidden, and then in viewDidAppear or wherever we want that view to slide in.
Storyboard/XIB Setup
So the first thing is to configure the constraints that won't be changing in IB (or code, whichever you are comfortable with). Namely; the height of the red view, and its leading and trailing space to the container. So your constraints might look like this (note: I haven't set a width constraint but let the leading and trailing spaces define the width):
Now you will see that IB will warn you that there is no constraint configured for the Y position of your view (hence the red dotted lines)
You can now add the top space to container constraint and set it as a placeholder constraint (checkbox that says "remove at build time"). We do this because we want to control where the view is programatically.
This means that this constraint will not exist once the view is loaded but serves to remove all your warnings as you are telling IB that you know how to deal with this constraint when the view is loaded.
Coding Time
Now we need a method to hide the view, a method to show the view. For this we are going to store the Y positioning constraint on the view and then animate it. Our viewController could look like this:
#IBOutlet weak var redView: UIView!
var redViewYPositionConstraint: NSLayoutConstraint?
override func viewDidLoad() {
super.viewDidLoad()
self.hideRedViewAnimated(false)
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
self.showRedViewAnimated(true)
}
Hiding
Our method to hide the view can simply remove the position constraint and then add one with the red views bottom equal to the view controller's view top:
func hideRedViewAnimated(animated: Bool) {
//remove current constraint
self.removeRedViewYPositionConstraint()
let hideConstraint = NSLayoutConstraint(item: self.redView,
attribute: .Bottom,
relatedBy: .Equal,
toItem: self.view,
attribute: .Top,
multiplier: 1,
constant: 0)
self.redViewYPositionConstraint = hideConstraint
self.view.addConstraint(hideConstraint)
//animate changes
self.performConstraintLayout(animated: animated)
}
Showing
Similarly our show constraint will move the view's center Y to the controllers center Y:
func showRedViewAnimated(animated: Bool) {
//remove current constraint
self.removeRedViewYPositionConstraint()
let centerYConstraint = NSLayoutConstraint(item: self.redView,
attribute: .CenterY,
relatedBy: .Equal,
toItem: self.view,
attribute: .CenterY,
multiplier: 1,
constant: 0)
self.redViewYPositionConstraint = centerYConstraint
self.view.addConstraint(centerYConstraint)
//animate changes
self.performConstraintLayout(animated: animated)
}
Convenience Methods
For completeness the convenience methods I have used look like this:
func performConstraintLayout(animated animated: Bool) {
if animated == true {
UIView.animateWithDuration(1,
delay: 0,
usingSpringWithDamping: 0.5,
initialSpringVelocity: 0.6,
options: .BeginFromCurrentState,
animations: { () -> Void in
self.view.layoutIfNeeded()
}, completion: nil)
} else {
self.view.layoutIfNeeded()
}
}
func removeRedViewYPositionConstraint() {
if redViewYPositionConstraint != nil {
self.view.removeConstraint(self.redViewYPositionConstraint!)
self.redViewYPositionConstraint = nil
}
}
You can also check if the red view is visible by using some CGRect math:
func isRedViewVisible() -> Bool {
return CGRectContainsPoint(self.view.bounds, self.redView.frame.origin)
}
Try this:
class ViewController: UIViewController {
// red view
#IBOutlet weak var myView: UIView!
// vertical align contraint
#IBOutlet weak var verticalConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
verticalConstraint.constant = (myView.bounds.height + self.view.bounds.height)/2
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
self.view.layoutIfNeeded()
UIView.animateWithDuration(0.5) {
self.verticalConstraint.constant = 0
self.view.layoutIfNeeded()
}
}
}
Be careful about firstItem and secondItem order in the constraint. The above code assumes Superview is the firstItem:
Alternative way is to define two constraints:
Center Y Alignment constraint (Superview.CenterY == View.CenterY) priority = 750
Vertical Space Constraint (SuperView.Top == View.Bottom) priority = 500
And adjust the priority to determine which contraint should be adopted.
class ViewController: UIViewController {
// vertical align contraint
#IBOutlet weak var centerYConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
centerYConstraint.priority = 250
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
self.view.layoutIfNeeded()
UIView.animateWithDuration(0.5) {
self.centerYConstraint.priority = 750
self.view.layoutIfNeeded()
}
}
}
For anyone looking for simple animation like the request:
For the slide up animation you need to use [...] the
CGAffineTransformMakeTranslation(x, y).
The reason is that for a slide up animation you need first move the
view off screen and then bring it back to its original position. So in
your viewDidLoad just use:
yourView.transform = CGAffineTransformMakeTranslation(0, 500)
This moves the view off screen, in this case at the bottom. Now in the
viewDidAppear you can show your view with:
UIView.animateWithDuration(0.7, delay: 0.0, usingSpringWithDamping: 0.5,
initialSpringVelocity: 0.5, options: [], animations: {
self.yourView.transform = CGAffineTransformMakeScale(1, 1)
}, completion: nil)
With these lines you can add the constraints that you want on your UIView and place it where you need in your ViewController.
For me the best way for animate Swift 3 & 4
self.constraint.constant = -150
UIView.animate(withDuration: 0.45) { [weak self] in
self?.view.layoutIfNeeded()
}

Height for UITableViewCell containing UITableView (iOS8 and AutoLayout)

I have a UITableView with custom cells that also contain UITableViews. I'm having problems getting the height for these cells.
I'm getting the height in heightForRowAtIndexPath and to do this I populate the child tableView (within the cell) and call layoutIfNeeded. This gives me the correct contentSize for this tableView. The problem is that the overall size for the cell is wrong and doesn't change. So calling cell.bounds.height returns an incorrect value.
let cell:GroupCell = tableView.dequeueReusableCellWithIdentifier("GroupCell") as GroupCell
cell.configure(field as SingleField, delegate: self) // populates data
cell.tableView.layoutIfNeeded()
// cell.tableView.contentSize is correct
return cell.bounds.height // is wrong - bounds haven't changed
Can anyone point me in the right direction ? BTW - I'm targeting iOS8 only.
Try to create subclass of UITableViewCell, add TableView delegates to it, use it like usual.
I think the problem is in UITableView. You've sad table have correct content size, but it have incorrect size. After calculating the correct content size you should change tableView height manually. I think it will help you. Tip: you can observe contentSize property and every time it will be changed you can resize table view. To do this - add height constrain to your table with low priority (750 for example) and than just change it when needed. In this case your table will have both content size and self.frame.heigh equal.
I managed to resolve this as follows:
I put a height constraint (Greater than or equal to) on the UITableView (within the Cell). I set the priority of this to High
I set the priority of the bottom margin to Low
When configuring my custom cell I call setNeedsUpdateConstraints(), which invalidates the height constraint by removing it, and then recalculates it from the content size of the table view before adding it back.
Finally, in heightForRowAtIndexPath cell.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height now returns the correct height
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
let field = fields[indexPath.row]
let cell:GroupCell = tableView.dequeueReusableCellWithIdentifier("GroupCell") as GroupCell
cell.configure(field as SingleField, delegate: self)
return cell.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
}
func configure(field:SingleField,delegate:GroupDelegate){
self.field = field
self.delegate = delegate
nameLabel.text = field.name
tableView.reloadData()
setNeedsUpdateConstraints()
}
override func updateConstraints() {
tableView.removeConstraint(tableViewHeightConstraint)
tableViewHeightConstraint = NSLayoutConstraint(item: tableView, attribute: NSLayoutAttribute.Height, relatedBy: NSLayoutRelation.Equal, toItem: nil, attribute: NSLayoutAttribute.NotAnAttribute, multiplier: 1, constant: tableView.contentSize.height)
tableView.addConstraint(tableViewHeightConstraint)
super.updateConstraints()
}

Resources