Safe area layout guides in xib files - iOS 10 - ios

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

Related

Xamarin iOS large tittle scroll velocity snapping issue

I am using Xamarin.Forms version 5.0 to develop an app for iOS 15. I am struggling to smooth the large title scroll transition. My problem is outlined on this question: iOS 11 large navigation bar title unexpected velocity
According to the above link, It seems like a fairly simple solution on Swift but I am struggling to do this using Xamarin.iOS.
Using a custom renderer, I have the following settings:
ExtendedLayoutIncludesOpaqueBars = True;
EdgesForExtendedLayout = UIRectEdge.Top;
AutomaticallyAdjustsScrollViewInsets = true;
NavigationController.NavigationBar.PrefersLargeTitles = true;
NavigationItem.LargeTitleDisplayMode = UINavigationItemLargeTitleDisplayMode.Automatic;
NavigationController.NavigationBar.Translucent = true;
I have tried modifying the constraints of my ScrollView by setting them to the constraints of the View. View.Superview is null for this page and would not allow me to change those constraints.
I have tried all sorts of different combinations of settings.
I have tried changing my ScrollView to a TableView.
I know I could make a custom navigation bar that could simulate this transition but I am trying to use native iOS settings and UI designs.
One of the answers on this question suggest the only way to get the smooth scroll transition is to use a UITableViewController instead of a UIViewController with a UITableView inside. Is this possible on Xamarin? I haven't found a way to use UITableViewController when designing the UI in Xaml. Even if I can change to a UITableViewController, will I be able to set the constraints correctly in the renderer?
Are there any other things I should try?
Thank you for any responses.
Try to find the ScrollView and change its constraints .
[assembly: ExportRenderer(typeof(ContentPage), typeof(MyBarRenderer))]
namespace FormsApp.iOS
{
internal class MyBarRenderer : PageRenderer
{
public override void ViewDidLoad()
{
base.ViewDidLoad();
ExtendedLayoutIncludesOpaqueBars = true;
foreach (UIView view in View.Subviews)
{
if (view is UIScrollView sc)
{
NSLayoutConstraint.ActivateConstraints(new NSLayoutConstraint[] {
sc.TopAnchor.ConstraintEqualTo(View.TopAnchor),
sc.LeftAnchor.ConstraintEqualTo(View.LeftAnchor),
sc.BottomAnchor.ConstraintEqualTo(View.BottomAnchor),
sc.RightAnchor.ConstraintEqualTo(View.RightAnchor),
});
}
}
}
}
}
Refer to
iOS 11 large title navigation bar snaps instead of smooth transition

SafeArea backwards compatibility

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.

uiView misaligning inside ViewController for iOS10, but not iOS11

I recently started using Swift and Xcode and I'm not sure why the bottom constraint for the main view in my viewcontroller is shifted up for iOS 10 devices, whereas it works correctly on iOS11.
I've had something similar to another application where the top view constraint was shifted down for iOS10 but I managed to fix it by overriding viewDidLayoutSubviews() and resetting constraints for devices lower than 11.
Unfortunately, the trick doesn't seem to work this time. If I override the bottom constraint, the bottom anchor is corrected but the top one gets pulled up and part of the view gets hidden by the navigation bar.
I've been trying to find a solution via previous questions (most of them were for the top gap and I haven't managed to modify them to work for the bottom gap I'm getting) and Google, but no luck.
I've attached a screenshot of the issue for clarity. iOS11 on the left, iOS10 on the right.
The override i've tried:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let margins = view.layoutMarginsGuide
NSLayoutConstraint.activate([
graphView.leadingAnchor.constraint(equalTo: margins.leadingAnchor),
graphView.trailingAnchor.constraint(equalTo: margins.trailingAnchor)
])
if #available(iOS 11, *) {
// safe area constraints already set
} else {
let standardSpacing: CGFloat = 0.0
NSLayoutConstraint.activate([
contentView.bottomAnchor.constraint(equalTo: bottomLayoutGuide.bottomAnchor, constant: standardSpacing)])
}
And the result:
You need to set these 4 constraints to your Main View

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

Resources