SafeArea backwards compatibility - ios

I'm having an issue when trying to layout a view programatically and I cant seem to find a concise, non hacky way to fix it.
I'm using safeAreaInsets to size some elements in my view. This works well until I try it out on a pre iOS 11 device. Obviously, with the lack of safeAreaInsets the sizing of my subviews falls apart and everything becomes a mess. What do I fall back to when using older versions of iOS.
More specifically, what can I implement in the extension below that will work as expected?
extension UIView {
func compatibilityInsets() -> UIEdgeInsets {
if #available(iOS 11.0, *) {
return self.safeAreaInsets
} else {
//what goes here?
return self.olderVersionOfInsets
}
}
}
Here is an exmaple of how I might use this extension method:
var minimumHeaderHeight: CGFloat {
//allows the header height to be 70 below navigation bar
return 70 + view.compatibilityInsets().top
}

safeAreaInsets was added to help avoid content disappearing behind the "notch" in the iPhone X... which only supports iOS 11 IIRC.
So the alternative for iOS 10 and below would be return .zero as there doesn't need to be any safe area defined.
.zero in this case is inferred to be of type UIEdgeInsets so is equivalent to calling UIEdgeInsets.zero.

Related

Get the default height (or maxY) of a NavigationBar on iOS

I would like to know if is there any way for us to obtain the default Navigation Bar height (maxY preferably) of a NavigationController.
Knowing that iOS 11 introduced large titles, is there any way for us then to get the default height (or maxY) of a Navigation Bar with a "small title" and of a Navigation Bar with a "large title"?
The reason I am asking this is because I am making the Navigation Bar's background transparent and introducing my own background to it (which is an Effect View). But the problem I am having is that every time I run the following code
self.navigationController?.navigationBar.frame.maxY
it returns a number ways higher than the expected :/
I tried to run this piece of code on many callbacks -> onViewWillAppear, onViewDidAppear, onViewDidLoad
You can get the height of navigation bar and status bar using this
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let topSpace:CGFloat?
if #available(iOS 11.0, *) {
topSpace = self.view.safeAreaInsets.top
} else {
topSpace = self.topLayoutGuide.length
}
print(topSpace)
}
I have used the native method to get the height of navigation bar including status bar. Use this line of code to get the navigation bar height and use as per your requirement. This worked for me perfectly fine on all devices & different iOS versions.
let navigationBarHeight = UIApplication.shared.statusBarFrame.size.height +
(self.navigationController?.navigationBar.frame.height ?? 0.0)
The best approach I found so far, without having to create a navigation controller instance:
[self.navigationBar sizeThatFits:CGSizeZero].height;
And just to mention, it supports screen orientation change too.
This works for me
let navigationBarHeight = UIApplication.shared.statusBarFrame.size.height +
(self.navigationController?.navigationBar.frame.height ?? 0.0)
Even tough your method may be the best solution for you I mostly try to not use the native navigation bar but hide it and create my own instead. This makes it easier to use custom and more advanced designs in the application.

Safe area layout guides in xib files - iOS 10

I started adapting my app for iPhone X and found an issue in Interface Builder.
The safe area layout guides are supposed to be backwards compatible, according to official Apple videos. I found that it works just fine in storyboards.
But in my XIB files, the safe area layout guides are not respected in iOS 10.
They work fine for the new OS version, but the iOS 10 devices seem to simply assume the safe area distance as zero (ignoring the status bar size).
Am I missing any required configuration? Is it an Xcode bug, and if so, any known workarounds?
Here is a screenshot of the issue in a test project (left iOS 10, right iOS 11):
There are some issues with safe area layout and backwards compatibility. See my comment over here.
You might be able to work around the issues with additional constraints like a 1000 priority >= 20.0 to superview.top and a 750 priority == safearea.top. If you always show a status bar, that should fix things.
A better approach may be to have separate storyboards/xibs for pre-iOS 11 and iOS-11 and up, especially if you run into more issues than this. The reason that's preferable is because pre-iOS 11 you should layout constraints to the top/bottom layout guides, but for iOS 11 you should lay them out to safe areas. Layout guides are gone. Laying out to layout guides for pre-iOS 11 is stylistically better than just offsetting by a min of 20 pixels, even though the results will be the same IFF you always show a status bar.
If you take this approach, you'll need to set each file to the correct deployment target that it will be used on (iOS 11, or something earlier) so that Xcode doesn't give you warnings and allows you to use layout guides or safe areas, depending. In your code, check for iOS 11 at runtime and then load the appropriate storyboard/xibs.
The downside of this approach is maintenance, (you'll have two sets of your view controllers to maintain and keep in sync), but once your app only supports iOS 11+ or Apple fixes the backward compatibility layout guide constraint generation, you can get rid of the pre-iOS 11 versions.
By the way, how are you displaying the controller that you're seeing this with? Is it just the root view controller or did you present it, or..? The issue I noticed has to do with pushing view controllers, so you may be hitting a different case.
Currently, backward compatibility doesn't work well.
My solution is to create 2 constraints in interface builder and remove one depending on the ios version you are using:
for ios 11: view.top == safe area.top
for earlier versions: view.top == superview.top + 20
Add them both as outlets as myConstraintSAFEAREA and myConstraintSUPERVIEW respectively. Then:
override func viewDidLoad() {
if #available(iOS 11.0, *) {
view.removeConstraint(myConstraintSUPERVIEW)
} else {
view.removeConstraint(myConstraintSAFEAREA)
}
}
For me, a simple fix for getting it to work on both versions was
if #available(iOS 11, *) {}
else {
self.edgesForExtendedLayout = []
}
From the documentation: "In iOS 10 and earlier, use this property to report which edges of your view controller extend underneath navigation bars or other system-provided views. ".
So setting them to an empty array makes sure the view controller does not extend underneath nav bars.
Docu is available here
I have combined some of the answers from this page into this, which works like a charm (only for top layout guide, as requested in the question):
Make sure to use safe area in your storyboard or xib file
Constraint your views to the safe areas
For each view which has a constraint attached to the SafeArea.top
Create an IBOutlet for the view
Create an IBOutler for the constraint
Inside the ViewController on viewDidLoad:
if (#available(iOS 11.0, *)) {}
else {
// For each view and constraint do:
[self.view.topAnchor constraintEqualToAnchor:self.topLayoutGuide.bottomAnchor].active = YES;
self.constraint.active = NO;
}
Edit:
Here is the improved version I ended up using in our codebase. Simply copy/paste the code below and connect each view and constraints to their IBOutletCollection.
#property (strong, nonatomic) IBOutletCollection(NSLayoutConstraint) NSArray *constraintsAttachedToSafeAreaTop;
#property (strong, nonatomic) IBOutletCollection(UIView) NSArray *viewsAttachedToSafeAreaTop;
if (#available(iOS 11.0, *)) {}
else {
for (UIView *viewAttachedToSafeAreaTop in self.viewsAttachedToSafeAreaTop) {
[viewAttachedToSafeAreaTop.topAnchor constraintEqualToAnchor:self.topLayoutGuide.bottomAnchor].active = YES;
}
for (NSLayoutConstraint *constraintAttachedToSafeAreaTop in self.constraintsAttachedToSafeAreaTop) {
constraintAttachedToSafeAreaTop.active = NO;
}
}
The count of each IBOutletCollection should be equal. e.g. for each view
there should be its associated constraint
I ended up deleting the constraint to safe area which I had in my xib file.
Instead I made an outlet to the UIView in question, and from code I hooked it up like this, in viewDidLayoutSubviews.
let constraint = alert.viewContents.topAnchor.constraint(equalTo: self.topLayoutGuide.bottomAnchor, constant: 0)
constraint.priority = 998
constraint.isActive = true
This ties a small "alert" to top of screen but makes sure that the contents view within the alert is always below the top safe area(iOS11ish)/topLayoutGuide(iOS10ish)
Simple and a one-off solution. If something breaks, I'll be back 🙄.
This also works:
override func viewDidLoad() {
super.viewDidLoad()
if #available(iOS 11.0, *) {}
else {
view.heightAnchor.constraint(equalToConstant: UIScreen.main.bounds.height - 80).isActive = true
view.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width - 20).isActive = true
}
}
I added a NSLayoutConstraint subclass to fix this problem (IBAdjustableConstraint), with a #IBInspectable variable, looks like this.
class IBAdjustableConstraint: NSLayoutConstraint {
#IBInspectable var safeAreaAdjustedConstant: CGFloat = 0 {
didSet {
if OS.TenOrBelow {
constant += safeAreaAdjustedConstantLegacy
}
}
}
}
And OS.TenOrBelow
struct OS {
static let TenOrBelow = UIDevice.current.systemVersion.compare("10.9", options: NSString.CompareOptions.numeric) == ComparisonResult.orderedAscending
}
Just set that as the subclass of your constraint in IB and you will be able to make < iOS11 specific changes. Hope this helps someone.
I used this one, add the top safe area layout and connect with outlet
#IBOutlet weak var topConstraint : NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
if !DeviceType.IS_IPHONE_X {
if #available(iOS 11, *) {
}
else{
topConstraint.constant = 20
}
}
}
Found the simpliest solution - just disable safe area and use topLayoutGuide and bottomLayoutGuide + add fixes for iPhone X. Maybe it is not beautiful solution but requires as less efforts as possible

updated constraints in traitCollectionDidChange have no effect

With my autolayout, I am trying to implement a special case for the smallest screens, when my icons need to be smaller. I am changing the constraint for the icons in traitCollectionDidChange(). However, the change has no effect. I've tried adding SetNeedsLayout, SetNeedsDisplay, UpdateConstraints, etc, and nothing works.
If I change these constraints when the view is initialized, then the change works -- but that doesn't help me when the user rotates the device and I need the change to happen again. However, it's interesting to note that traitCollectionDidChange() is called at the launch of the program, but if I make the constraint changes within that function, they don't work. They only work if I set the new constraints in the init() function of the view.
It could be that this function is called AFTER the constraints are used, except Apple tells us to use this function for just this purpose, to change layouts etc when rotating. I don't want to use WillTransitionToSize because only the ViewController is called with that function, not views. But I will probably try that next.
traitCollectionDidChange() is definitely being called, at startup and when I rotate. It's just that my changes don't do anything. Any ideas?
Any ideas?
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
// smaller icon constraints for iPhone 5/SE
if let constraint = (self.imageView!.constraints.filter{$0.firstAttribute == .width}.first) {
print("cwide \(constraint.constant)")
if UIScreen.main.bounds.size.width <= 320 {
constraint.constant = 20.0
self.radius = 4
}
else {
constraint.constant = 32.0
self.radius = 8
}
}
self.updateConstraints()
}
I had a similar feature and i turned out that I had forgotten to install the constraints in the Interface Builder.

How to debug layout with Multiline UILabel / autolayout in notification content extension

How to debug the following issue? Is there a way how to work around this issue?
There seems to be a bug in iOS 10.2 and below when laying out a multi-line UILabel.
I have a fairly simple UIView subclass which I use in both app and notification content extension, that looks like this:
In the main app, everything is laid out just fine:
When shown in notification content extension on iOS 10.2 and below, the layout is broken. But only when the text is long enough to be broken into multiple lines. Seems like iOS can't calculate correct height of the whole view:
However, this issue seems to be fixed on iOS 10.3 and newer:
I started experimenting with the subviews, specifically by setting fixed height constraints.
Turns out, it was not the label(s) that caused the issue with calculating overall height but the aspect ratio constraint (width:height) on the topmost view.
Programmatically calculating height based on the view's width and setting a height constraint for the affected view helped to fix the issue:
public override func updateConstraints() {
super.updateConstraints()
if #available(iOS 10.2, *) {
imageContainerHeightConstraint.isActive = false
} else {
// FIX: multiline label / aspect ratio / autolayout bug in iOS < 10.2
let ratio: CGFloat = imageContainerAspectRatioConstraint.multiplier
imageContainerHeightConstraint.constant = round(bounds.width/ratio)
imageContainerHeightConstraint.isActive = true
}
}

iOS 8 Widget Alignment Issue

I'm having trouble aligning an iOS 8 widget all the way to the left. It seems that setting the x origin to 0 still keeps a certain amount of space between the left edge of the screen and my first view.
I'm not sure how Evernote does this, but it seems that they have it figured out. Any suggestions? I also tried setting the x position programmatically to no success.
Here the answer:
// MARK: NCWidgetProviding protocol methods
func widgetMarginInsetsForProposedMarginInsets(defaultMarginInsets: UIEdgeInsets) -> UIEdgeInsets
{
return UIEdgeInsetsZero
}

Resources