Autolayout differs from iOS 7 and iOS 8 - swift

I'm having an issue between an iAd BannerView, created in IB, and its constraints. I have an IBOutlet for bottom constraint of an iAd BannerView with superview. In viewDidLoad() of ViewController I set the outlet to 0 minus banner height to put iAd BannerView outside the bottom screen margin.
#IBOutlet var adBannerView: ADBannerView!
#IBOutlet var adBannerBottomConstraints: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
// Configure answers index
self.arrayAnswIndex = ["A", "B", "C", "D", "E"]
// TableView Cell
var nib = UINib(nibName: "QuestionTableViewCell", bundle: nil)
self.simulazioneTableView.registerNib(nib, forCellReuseIdentifier: self.QuestionCellIdentifier)
var nibAnswer = UINib(nibName: "AnswerTableViewCell", bundle: nil)
self.simulazioneTableView.registerNib(nibAnswer, forCellReuseIdentifier: self.AnswerCellIdentifier)
// NavigationBar
self.navigationBarSettings()
// TabBar
self.tabBarController?.tabBar.hidden = true
// iAd Banner
adBannerView.hidden = true
adBannerBottomConstraints.constant = 0 - self.adBannerView.bounds.size.height
}
When ad is loaded I animate constraint to show the banner with:
func bannerViewDidLoadAd(banner: ADBannerView!) {
if (adBannerView.hidden == true) {
//now show banner
adBannerView.hidden = false
self.adBannerBottomConstraints.constant = self.adBannerBottomConstraints.constant + self.adBannerView.frame.size.height
UIView.animateWithDuration(0.4, animations: {
self.view.layoutIfNeeded()
})
}
}
The problem is that in iOS 8 everything works fine, in iOS 7 instead iAd BannerView is twice its size under bottom margin after viewDidLoad() and so when ad is loaded the banner remains outside the screen.
I have temporarily resolved the issue checking the device version and modifying the constraint accordingly in viewDidLoad().
// iAd Banner
adBannerView.hidden = true
if ((UIDevice.currentDevice().systemVersion as NSString).floatValue >= 8.0) {
adBannerBottomConstraints.constant = 0 - self.adBannerView.bounds.size.height
} else {
adBannerBottomConstraints.constant = 0
}
There is a better way to accomplish my purpose?
Thank you guys!
Andrea
Images:
IB Constraints
iOS 8 ad not loaded
iOS 8 ad loaded
iOS 7 ad not loaded
iOS 7 ad loaded

adBannerBottomConstraints.constant =
0 - self.adBannerView.bounds.size.height
But that's your whole problem right there. This is exactly the sort of hard-coded arithmetic calculation based on assumptions about the actual values of things that constraints mean you should not be doing. The whole point of autolayout is that you do not calculate anything: you set constraints that describe where the view should be. If you want this thing to be below the bottom of the superview, pin its top to the bottom of the superview!
Then, when you want to show it, delete that constraint and pin its bottom wherever it needs to go so that it appears at the bottom.

Related

How to make mapView and segmentedControl move relative to AdMob adaptive banner

I'm adding an adaptive banner ad from AdMob to the bottom of a mapView in my iOS app. No problem adding it, but the banner obscures both a segmentedControl which is placed at the bottom of the mapView. (Both the mapView and the segmentedControl are inside a tabBarController, all created in Storyboard.) The ad also covers the Apple Mags logo and "Legal" information - not sure if Apple will have a problem with that.
Image example - top of segmentedControl barely visible under ad
How can I make the mapView shift up by the same amount of space taken up by the ad, so as to make the segmentedControl and Apple Maps boilerplate visible, and not shift up at all if no ad is loaded? (Or is there another best practice of handling this?)
All my AdMob code is from the official AdMob tutorial:
func getAdaptiveSize() -> GADAdSize {
var frame: CGRect
if #available(iOS 11.0, *) {
frame = view.frame.inset(by: view.safeAreaInsets)
} else {
frame = view.frame
}
let viewWidth = frame.size.width
return adSize
}
func loadAdaptiveBannerAd(){
bannerView.adSize = getAdaptiveSize()
bannerView.load(GADRequest())
}
func addBannerToView(){
bannerView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(bannerView)
NSLayoutConstraint.activate([
bannerView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
bannerView.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
}
override func viewDidLoad() {
super.viewDidLoad()
bannerView.adUnitID = "ca-app-pub-3940256099942544/2435281174"
bannerView.rootViewController = self
bannerView.backgroundColor = .darkGray
addBannerToView()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
loadAdaptiveBannerAd()
}
My hunch is that I have to fix this not by changing the placement of the ad banner itself, but by approaching either the mapView or the superview somehow, but I haven't found the solution.
You didn't show your actual layout, but I'm going to assume it looks very close to this:
The Bottom of your mapView is constrained to the Bottom of the safe-area, and the Bottom of your Segmented control is constrained to the Bottom of the mapView. If you have your SegControl also constrained to the safe-area, change that to the mapView -- that way we only have to manipulate the mapView and the SegControl will move with it.
First step, change the Priority of the Map view's bottom constraint to High (750):
and your constraints will look like this:
Next, connect that constraint to an #IBOutlet:
#IBOutlet var mapBottomConstraint: NSLayoutConstraint!
result:
Now at run-time, when you add your banner view, add a NEW Bottom constraint to your map view:
func addBannerToView(){
bannerView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(bannerView)
NSLayoutConstraint.activate([
bannerView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
bannerView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
// add this line
mapView.bottomAnchor.constraint(equalTo: bannerView.topAnchor)
])
}
Because the newly created constraint will have the default Priority of Required (1000), auto-layout will "pin" the bottom of the map view to the top of the banner. Since we gave the Storyboard-created Bottom constraint a Priority of 750, we've told auto-layout it's allowed to break that constraint without causing conflicts.
If / when you remove the banner view, that "new" constraint will be removed with it, and the Storyboard-created constraint will again be used.

Pushing UIViews up for UIKeyboard

I've added a willShow and willHide observer for the keyboard and am trying to push up the bottom UITextView up to adjust to the UIKeyboard showing. However, my keyboard is getting pushed farther up than just the keyboard frame height. How do I constrain the UITextView bottom anchor to the top of the keyboard?
// Observer method
#objc func handleKeyboardNotification(_ notification: NSNotification) {
if let userInfo = notification.userInfo {
let keyboardFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as AnyObject).cgRectValue
let isKeyboardShowing = (notification.name == UIResponder.keyboardWillShowNotification)
// Push views up if keyboard is showing, otherwise set constant back to 0
messageInputBottomAnchor?.constant = isKeyboardShowing ? -(keyboardFrame?.height)! : 0
UIView.animate(withDuration: 0.5) {
self.view.layoutIfNeeded()
}
}
}
What I did was
I created 2 different constraints on the UITextView, one to the superview and one to the safearea.
Connect both of those constraints over to the view controller and make sure that they are not weak references.
In viewDidLoad, make sure that the safearea constraint is active and the superview constraint isn't.
In your listeners when the keyboard comes up, switch the isActive fields of the constraints so now the superview one is active and safearea is inactive.
In the listener when it's going to resign, switch them back.
The reason for needing to make sure that the references aren't weak is because when you set the isActive field to false, it'll actually remove it and if you try and reference the constraint later, you'll find yourself trying to access a member of a nil object.
I left my UITextView constrained to the safeAreaLayoutGuide bottom but I added a CGFloat to represent the padding at the bottom of the screen. It defaulted as 0 but if iOS 11 was available (to account for the iPhone X and it's bottom padding), I set the CGFloat to UIApplication.shared.keyWindow!.safeAreaInsets.bottom. Then, when I moved the UITextView up or down according to the UIKeyboard, I subtracted the size of the CGFloat.
//Global var
var safeAreaBottom: CGFloat = 0.0
// Verify screen bottom in viewDidLoad
if #available(iOS 11, *) {
safeAreaBottom = UIApplication.shared.keyWindow!.safeAreaInsets.bottom
}
// Push views up if keyboard is showing, otherwise set constant back to 0. Subtract safeAreaBottom if iOS 11 is available to compensate for the bottom padding
textViewBottomAnchorToSafeArea?.constant = isKeyboardShowing ? -((keyboardFrame?.height)! - safeAreaBottom) : 0

Swift iOS -How to Add Subview to Window's Center?

I have an activity indicator that gets presented on an iPhone and iPad. In the iPad in split screen mode it gets presented to whichever side of the view that called it. I would instead like it to get presented in the middle/center the window's screen. If I do it this way wether on the iPhone in portrait or iPad in split screen mode it will always be in the center of the screen.
How do I do this?
MyView: UIViewController{
let actInd = UIActivityIndicatorView(activityIndicatorStyle: .whiteLarge)
#IBAction fileprivate func buttonPressed(_ sender: UIButton) {
guard let window = UIApplication.shared.keyWindow else { return }
//how to add actInd as subview to the window' screen?
actInd.startAnimating()
}
}
It's pretty simple. Turn off the auto-resizing mask. Add the add actInd to window, then set the center anchors.
actInd.translatesAutoresizingMaskIntoConstraints = false
window.addSubview(actInd)
actInd.centerXAnchor.constraint(equalTo: window.centerXAnchor).isActive = true
actInd.centerYAnchor.constraint(equalTo: window.centerYAnchor).isActive = true
Window is subclass of UIView. Just add it as it's subview like you're adding a view to another view. But remember that window is shared throughout your app, so adding it every-time will consume memory, remove it after your job is done.
If you want to center it in the window, you can use autoResizingMask or add constraints to it.

UISearchController SearchBar misaligns while active during rotation

I have been struggling with UISearchController's search bar for quite a while now. I need a search feature to be implemented on a tableview but unlike conventional methods, I didn't put the search bar on a table header view. Instead, I created a UIView and added the search bar as a subview to it. The UIView which acts as a container to search bar has its constraints properly set on the storyboard with auto layout.
Here are my codes for it. Note that I did this programmatically because UISearchDisplayController and UISearchBar has been deprecated as of iOS 8 in favour of UISearchController and has yet to come to UIKit.
searchController = UISearchController(searchResultsController: nil)
searchController.searchResultsUpdater = self
searchController.dimsBackgroundDuringPresentation = false
searchController.searchBar.autoresizingMask = .FlexibleRightMargin
searchController.searchBar.delegate = self
definesPresentationContext = true
self.searchContainerView.addSubview(searchController.searchBar)
However, I did notice one odd behaviour of the search bar during rotation. When it is active on Portrait, I rotate the simulator to Landscape, and press Cancel, the search bar goes back to Portrait Width.
The same happens the other way around too.
I would appreciate any ideas or maybe some hints towards the correct direction to solve this as I have been at this for days at least. Thank you very much
So after so many days of struggling with this:
Portrait to Landscape
Make SearchBar/SearchController active in Portrait
Rotate to Landscape
Press Cancel and SearchBar will go back to Portrait width
Landscape to Potrait
Make SearchBar/SearchController active in Landscape
Rotate to Portrait
Press Cancel and SearchBar will go back to Landscape width
I finally solved it on my own. It seems that when a user presses Cancel on the SearchBar, the ViewController will call viewDidLayoutSubviews thus I tried to reset the width by calling this function in viewDidLayoutSubviews:
func setupSearchBarSize(){
self.searchController.searchBar.frame.size.width = self.view.frame.size.width
}
But that didn't work as well as I thought. So here is what I think happens. When a user activates the SearchController/SearchBar, the SearchController saves the current frame before it resizes itself. Then when a user presses Cancel or dismisses it, it will use the saved frame and resize to that frame size.
So in order to force its width to be realigned when I press Cancel, I have to implement the UISearchControllerDelegate in my VC and detect when the SearchController is dismissed, and call setupSearchBarSize() again.
Here are the relevant codes to solve this question:
class HomeMainViewController : UIViewController, UISearchControllerDelegate, UISearchResultsUpdating, UITableViewDelegate, UITableViewDataSource, UISearchBarDelegate {
#IBOutlet weak var searchContainerView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
searchController = UISearchController(searchResultsController: nil)
searchController.searchResultsUpdater = self
searchController.dimsBackgroundDuringPresentation = false
searchController.searchBar.autoresizingMask = .FlexibleRightMargin
searchController.searchBar.delegate = self
searchController.delegate = self
definesPresentationContext = true
self.searchContainerView.addSubview(searchController.searchBar)
setupSearchBarSize()
}
func setupSearchBarSize(){
self.searchController.searchBar.frame.size.width = self.view.frame.size.width
}
func didDismissSearchController(searchController: UISearchController) {
setupSearchBarSize()
}
override func viewDidLayoutSubviews() {
setupSearchBarSize()
}
}
Here is simpler solution:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: { (context) in
self.searchController.searchBar.frame.size.width = self.view.frame.size.width
}, completion: nil)
}

Ad Banner Moving Out of Position, Showing Out of Order

I have an AdBannerView inside my game, but it keeps showing randomly even though I set it to hidden, it pops from the bottom pushing the view up.
Here's the code I have thus far in GameScene:
var iAd = ADBannerView()
override func didMoveToView(view: SKView) {
iAd.delegate = self
iAd.hidden = true
iAd.autoresizingMask = UIViewAutoresizing.FlexibleTopMargin
view.addSubview(iAd)
}
func bannerViewDidLoadAd(banner: ADBannerView!) {
if (!isStarted){ // <- If game has started
iAd.hidden = false
}
}
func bannerView(banner: ADBannerView!, didFailToReceiveAdWithError error: NSError!) {
print("Ad Fail")
iAd.hidden = true
}
func newGame() {
iAd.hidden = true
}
func gameOver() {
iAd.hidden = false
}
Sometimes the ad shows during gameplay, sometimes it shows at the top, other times at the bottom.
My questions are:
How do I position it at the top?
How do I make it so that it stops appearing during gameplay?
How do I make it stop pushing the view up?
How do I make it stop showing if it failed to load (it currently does show)
MORE INFO: I tried this code in ViewController, but ended up with the same results.
For question 1: You have set the frame property to position the banner ad:
iAd.frame = CGRectMake(0, view.frame.size.height - iAd.frame.size.height, view.frame.size.width, iAd.frame.size.height);
Sounds like you've created an ADBannerView in Interface Builder in addition to including self.canDisplayBannerAds somewhere in your project.
The ADBannerView displaying on the bottom of your devices screen is created by self.canDisplayBannerAds = true. self.canDisplayBannerAds = true can be used for a no hassle way of implementing iAd banners in your application. This will create an ADBannerView for you and show or hide the ADBannerView depending on whether it receives an ad or not from the iAd network.
If you have included self.canDisplayBannerAds = true in your project you need to remove it.
As for hiding the ADBannerView when it fails to receive an advertisement, you need to implement the ADBannerView's delegate methods: ADBannerViewDelegate Protocol Reference. Then, in bannerView(_:didFailToReceiveAdWithError:) you'd set your ADBannerView's alpha property to 0.