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

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

Related

Change constraint when device is rotated

I'm using the following layout as a custom popup UIView in Xcode 13 (the white background is transparent):
When the screen orientation is changed to landscape mode, the constraint at the top and bottom are still 100pts. Because of that the middle part (yellow, UIView with UIStackView with UITableView,... inside) is really small and a warning shows up in console about the top (red) and bottom (blue) bar:
Unable to simultaneously satisfy constraints.
I know what this warning means. To fix it I created the following function...
private let constraintPortrait:CGFloat = 100
private let constraintLandscape:CGFloat = 10
private func fixConstraints() {
if (UIDevice.current.orientation == .landscapeLeft || UIDevice.current.orientation == .landscapeRight) && UIDevice.current.userInterfaceIdiom == .phone {
topConstraint.constant = constraintLandscape
bottomConstraint.constant = constraintLandscape
} else {
topConstraint.constant = constraintPortrait
bottomConstraint.constant = constraintPortrait
}
}
... and call it both in viewDidLoad and viewDidLayoutSubviews. This was working great but every now and then the warning still popped up, so I added prints to viewDidLoad,... and noticed that the warning is actually printed before my constraint fix is called. I renamend viewDidLayoutSubviews to viewWillLayoutSubviews (UIViewController lifecycle here) and Abracadabra!, the warning was gone.
People usually recommend to use viewDidLayoutSubviews when you want to do stuff after the device was rotated but hardly ever mention viewWillLayoutSubviews and while searching for a reason for that I found this answer, saying not to use the latter to change constraints because it might cause another autolayout pass.
Question:
What should I use instead to prevent the conflicts (without changing the fixed constraints for portrait mode!)? Is there a way to change the top and bottom constraint automatically and solely in the Interface Builder, without using any code and only when actually necessary (-> always keep the 100pts in portrait mode, even with a long table, but switch to 10pts instantly in landscape mode when there isn't enough space)?
viewWillLayoutSubviews is correct. Any layout changes you perform here, including changes of constraints, will be animated automatically in coordination with the rotation animation.
But how will you know that this call to viewWillLayoutSubviews is due to rotation? Implement this method:
https://developer.apple.com/documentation/uikit/uicontentcontainer/1621466-viewwilltransition
Or, on an iPhone, this method:
https://developer.apple.com/documentation/uikit/uicontentcontainer/1621511-willtransition
I like the former because it works on both iPad and iPhone. These are called before viewWillLayoutSubviews, so you can set an instance property to signal to yourself that the size is officially changing. You can work out what's happening by comparing the bounds size height to the bounds size width, and change the constraints accordingly.

UITextField: adjustsFontSizeToFitWidth behaviour/ issue

I have an issue with the usage of textField’s adjustsFontSizeToFitWidth. I have a textField with font size of 50 and a maximum width of 300 (This is a less than or equal to constraint). TextField is placed centre vertically and horizontally using auto layout. Now when I run the app, the text is being shrinked from the beginning itself. I know this may be because the system thinks the bounds of Texfield is not enough… but I gave the control to auto layout to figure that out and I assume it should work. Below is the snapshot… you can see the big placeholder and then suddenly the text shrinks… Any thoughts? Am I doing something wrong, I was trying to avoid manual calculation of width..
SourceCode Sample
Set leading/trailing space to text field like 15 pt from left safe area and 15 from Button so that it could automatically increase the font size. As now it shrinks the font to minimal 17 (which is set in storyboard) as the width is not enough.
Okay!! Finally I found a way, the idea was to check if the width is greater than the defined width limit... when the width is more I set adjustsFontSizeToFitWidth = true else set to false ... for some reason the solution didnt work when I clear the text... then I tried programatical approach and it started working .... dont know what was the difference... anyways I updated the source..... if there is a better solution let me know.
#IBAction func textChanged(_ txtField: UITextField) {
if txtField.frame.width >= (view.frame.width * widthMultiplier).rounded() {
txtField.adjustsFontSizeToFitWidth = true
} else {
txtField.adjustsFontSizeToFitWidth = false
}
}

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

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

UITextView scroll bug iOS9 xcode 7.1.1

I have a simple UITextView that occupies all the screen.
When the text length is bigger than the Textview height, it automatically scrolls to the bottom (see image).
I have already tried
self.textView.scrollRangeToVisible(NSMakeRange(0, 0))
and
self.textView.scrollsToTop = true
and other things without any result.
I'm using iOS 9 with the latest swift and Xcode 7.1.1.
I've found a solution.
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.textView.setContentOffset(CGPoint.zero, animated: false)
}
but I still don't understand why it didn't work.
You also try
self.textView.scrollRangeToVisible(NSRange(location:0, length:0))

Resources