How to animate a UIView with constraints in Swift? - ios

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

Related

Unable to activate constraint with anchors error with UIimageView

Posting a question for the first time here.
So I have been trying to make an animation of an UIimageView. I did that so far. So the image moves from the middle of the screen to the top. I want to be able to make that animation with constraints. But while trying to add some constraints, I receive this error "Unable to activate constraint with anchors error".
here is the code which I try to add some constraints to banditLogo imageview.
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(banditLogo)
view.translatesAutoresizingMaskIntoConstraints = false // autolayout activation
chooseLabel.alpha = 0
signInButtonOutlet.alpha = 0
self.banditLogo.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 304).isActive = true
self.banditLogo.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 94).isActive = true
self.banditLogo.widthAnchor.constraint(equalToConstant: 224).isActive = true
self.banditLogo.heightAnchor.constraint(equalToConstant: 289).isActive = true
}
and here is the func that makes the animation.
this func is being called in viewDidAppear and animatedImage variable of the function is referred to banditLogo UIimageView.
so when the view screen loads up, the image moves to top of the view.
func logoAnimate(animatedImage: UIImageView!, animatedLabel: UILabel!) {
UIView.animate(withDuration: 1.5, delay: 1, options: [.allowAnimatedContent]) {
animatedImage.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 5).isActive = true
animatedImage.leftAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leftAnchor, constant: 94).isActive = true
} completion: { (true) in
UIView.animate(withDuration: 0.25) {
animatedLabel.alpha = 1
}
}
}
You may find it easier to create a class-level property to hold the image view's top constraint, then change that constraint's .constant value when you want to move it.
Here's a quick example - tapping anywhere on the view will animate the image view up or down:
class AnimLogoViewController: UIViewController {
let banditLogo = UIImageView()
// we'll change this constraint's .constant to change the image view's position
var logoTopConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
if let img = UIImage(systemName: "person.fill") {
banditLogo.image = img
}
view.addSubview(banditLogo)
// I assume this was a typo... you want to set it on the image view, not the controller's view
//view.translatesAutoresizingMaskIntoConstraints = false // autolayout activation
banditLogo.translatesAutoresizingMaskIntoConstraints = false // autolayout activation
// create the image view's top constraint
logoTopConstraint = banditLogo.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 304)
// activate it
logoTopConstraint.isActive = true
// non-changing constraints
self.banditLogo.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 94).isActive = true
self.banditLogo.widthAnchor.constraint(equalToConstant: 224).isActive = true
self.banditLogo.heightAnchor.constraint(equalToConstant: 289).isActive = true
// animate the logo when you tap the view
let t = UITapGestureRecognizer(target: self, action: #selector(self.didTap(_:)))
view.addGestureRecognizer(t)
}
#objc func didTap(_ g: UITapGestureRecognizer) -> Void {
// if the logo image view is at the top, animate it down
// else, animate it up
if logoTopConstraint.constant == 5.0 {
logoTopConstraint.constant = 304.0
} else {
logoTopConstraint.constant = 5.0
}
UIView.animate(withDuration: 1.5, animations: {
self.view.layoutIfNeeded()
})
}
}
I animate views that have constraints by changing constraints, not setting them. Leave the constraints that are static "as is" - that is, use isActive = true. But those you wish to change? Put them in two arrays and activate/deactivte them. Complete the animation like you are by using UIView.animate.
For instance, let's say you wish to move banditLogo from top 304 to top 5, which appears to me to be what you trying to do. Leave all other constraints as is - left (which your code doesn't seem to change), height, and width. Now, create two arrays:
var start = [NSLayoutConstraint]()
var finish = [NSLayoutConstraint]()
Add in the constraints that change. Note that I'm not setting them as active:
start.append(banditLogo.topAnchor.constraint(equalTo: safeAreaView.topAnchor, constant: 305))
finish.append(banditLogo.topAnchor.constraint(equalTo: safeAreaView.topAnchor, constant: 5))
Initialize things in viewDidLoad or any other view controller method as needed:
NSLayoutConstraint.activate(start)
Finally, when you wish to do the animation, deactivate/activate and tell the view to show the animation:
NSLayoutConstraint.deactivate(start)
NSLayoutConstraint.activate(finish)
UIView.animate(withDuration: 0.3) { self.view.layoutIfNeeded() }
Last piece of critique, made with no intent of being offending.
Something in your code posted feels messy to me. Creating a function to move a single view should directly address the view IMHO, not pass the view into it. Maybe you are trying to move several views this way - in which case this is good code - but nothing in your question suggests it. It's okay to do the animation in a function - that way you can call it when needed. I do this all the time for something like this - sliding a tool overlay in and out. But if you are doing this to a single view, just address it directly. The code is more readable to other coders.
Also, my preference for the start is in viewDidLoad unless the VC is part of a navigation stack. But in that case, don't just use viewDidAppear, set things back to start in viewDidDisappear.
EDIT: looking at the comments, I assumed that yes you have already used translatesAutoresizingMaskIntoConstraints = false properly on every view needed.

Any reasons why my UIView does not change if I set different constrains?

I have an UIView inside my normal UIView. This is my code:
override func viewDidAppear(_ animated: Bool) {
print(canvasUnder.frame.origin.y)
let getRelativePosition = view.frame.size.height * 0.25
canvasUnder.frame.origin.y = canvasUnder.frame.origin.y + getRelativePosition
self.view.layoutIfNeeded()
print(canvasUnder.frame.origin.y)
print(getRelativePosition)
}
It does however stays at the original position. This is my print:
240.0
320.0
80.0
How can this be? Thank you. edit: this is what I want: My UIView that I want to change has a height of * 0.25 of the root view. I want that my UIView is right off the boundaries of my root view, so it needs to be 25% of the root views height, followed by a constrained movement that will push my UIView down.
Edit 2: I managed to to this, this way:
override func viewDidLayoutSubviews() {
print("called")
let getRelativePosition = view.frame.size.height * 0.25
self.CanvasUnder.frame.origin.y = self.CanvasUnder.frame.origin.y + getRelativePosition
}
However this method get called each and every time something changed. I just want that my canvasUnder is just right off the screen when the view is presented. Then, whenever I want, I want to animate that UIView to pop up. I want to use this code:
let getRelativePosition = view.frame.size.height * 0.25
self.CanvasUnder.frame.origin.y = self.CanvasUnder.frame.origin.y - getRelativePosition
UIView.animate(withDuration: 1.0, animations: {
self.view.layoutIfNeeded()
})
Or putting the change of frame inside the animate function will not work. It just keeps triggering the viewDidLayoutSubviews method which will hide again my UIView.
So how can I just hide that view right under my root view, and pop it up with an slide up animation which will take 1 second?
"this is what I want: My UIView that I want to change has a height of
* 0.25 of the root view. I want that my UIView is right off the boundaries of my root view, so it needs to be 25% of the root views
height, followed by a constrained movement that will push my UIView
down."
This is exactly what auto layout and constraints are for, so you don't have to constantly be calculating sizes.
Use constraints to "pin" your view to left, right and bottom of its superview, then set it's Height Equal to Superview Height, with a multiplier (ratio) of 1:4
That will keep its height at 25% of the "root view" and will keep it "stuck" to the bottom.
No code needed :)
To animate the view in-and-out, add an IBOutlet to the Bottom constraint, and use this code...
class TestViewController: UIViewController {
#IBOutlet weak var trayBottomView: UIView!
#IBOutlet weak var trayBottomConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
// hide the "tray" view
trayBottomView.isHidden = true
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// constraints and frame sizes are fully calculated by AutoLayout here, so...
// move the tray offscreen (below the view), and "un-hide" it
self.trayBottomConstraint.constant = -self.trayBottomView.frame.height
self.trayBottomView.isHidden = false
// this first part will just put the tray view into position, below the screen
UIView.animate(withDuration: 0.01, animations: {
self.view.layoutIfNeeded()
}, completion: { (finished: Bool) in
// now, set the tray Bottom constraint to 0, so it will end up "sitting" on the bottom of the screen
self.trayBottomConstraint.constant = 0
// animate it into view - use delay to "wait a bit" before sliding the view up
// duration of 0.75 (3/4 of a second) may be too slow, just tweak as desired
UIView.animate(withDuration: 0.75, delay: 0.25, options: UIViewAnimationOptions.curveEaseInOut, animations: {
self.view.layoutIfNeeded()
}, completion: nil)
})
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func buttonTapped(_ sender: Any) {
// if the tray Bottom Constraint is Zero, that means it is visible, so
// set it to -(its own height) to position it offscreen, below the view
// otherwise, it is already offscreen, so set it to Zero to bring it back up
if self.trayBottomConstraint.constant == 0 {
self.trayBottomConstraint.constant = -self.trayBottomView.frame.height
} else {
self.trayBottomConstraint.constant = 0
}
// animate it in or out of view
// duration of 0.75 (3/4 of a second) may be too slow, just tweak as desired
UIView.animate(withDuration: 0.75, delay: 0.0, options: UIViewAnimationOptions.curveEaseInOut, animations: {
self.view.layoutIfNeeded()
})
}
}
Any reasons why my UIView does not change if I set different constraints?
You are not changing constraints. You are saying canvasUnder.frame.origin.y. That is not a change of constraint. It is a change of frame. But you cannot directly change the frame if the view is positioned by constraints! The constraints are what positions the view, not the frame.
How can this be?
Because you change the frame, and it does change just at that little moment. But by the time you see the view, the constraints have changed it back again!
When you call self.view.layoutIfNeeded() it is triggering your view to relayout. It is being moved either by auto layout, or layout code in your viewWillLayoutSubviews/viewDidLayoutSubviews functions.
Rather then updating the frame directly, either update the constraint, eg:
canvasUnderHeightConstraint.constant = view.frame.size.height * 0.25
Or, a better solution would be to use the multiplier field of the constraint to get your desired result.
NSLayoutConstraint(item: view,
attribute: .height,
relatedBy: .equal,
toItem: canvasUnder,
attribute: .height,
multiplier: 0.25,
constant: 0)

Swift : show subview with animation on click of button

I want to make subview hidden false when user presses button. This subview has opacity with 60% set at design time. The subview should appear with smooth animation beginning from location where button is present.
Below is code that I've tried :
#IBAction func moreOptions_Clicked(sender: AnyObject) { print("moreOptions clicked")
UIView.animateWithDuration(0.9, delay: 0.2, options: UIViewAnimationOptions.TransitionCurlDown, animations: {
//self.objMoreView.hidden = false
// Show view with animation
}, completion: nil)}
This didn't worked. Any solution?
Replace your .hidden = false by .alpha = 1 and it should work ;)
Explanation :
If you wanna animate the "visibility" of your UI object :
Set your alpha initial value at 0 (your object will be "hidden")
During your animation, set its value to 1, the UIView.animateWithDuration() will animate the 0 -> 1 change by passing by all values during the duration. You will have a animation which show your subview progressively.
If you wanna animate the position or the size of your UI object :
yourObject.frame.size.(height or width)
or
yourObject.frame.origin.(x or y)
Hope I helped you.
You can also do this with constraints.
For instance set a top constraint to your button in your view.
Keep a reference to that constraint :
self.whatEverTopButtonConstraint = NSLayoutConstraint(item: self.whatEverButton, attribute: .Top, relatedBy: .Equal, toItem: self.whatEverSubView, attribute: .Top, multiplier: 1, constant: 0)
self.whatEverSubView.addConstraint(self.whatEverTopButtonConstraint)
then whenever you want to make the button appears, set the constant of this constraint to whatever you need.
Finally just animate it with a nice spring effect. It would result to something like this :
func showButton () {
self.whatEverTopButtonConstraint.constant = 20 //custom the right constraint constant here
UIView.animateWithDuration(0.9, delay: 0.2, usingSpringWithDamping: 0.8, initialSpringVelocity: 1, options: .CurveEaseInOut, animations: {
self.whatEverButton.alpha = 1
self.whatEverSubView.layoutIfNeeded()
}, completion: nil)
}
Of course this code will animate the position only vertically. If you wish to make it move at the same moment horizontally, set a constraint from the right edge for instance, and then just before the call of the animation set its constant ;)

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

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
}

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.

Resources