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
Related
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.
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
I am using a button as a title view for my UITableViewController which opens a dropdown list of categories. Selecting a category filters content of the table view by the selected category.
The button shows the name of the selected category plus a small arrow, similar to how iBooks used to look (or maybe still looks? I haven't used it in a while). I would therefore like it to have the same behaviour as a standard title and have it be large at first and collapse when the table view is scrolled.
Is there a way to do this?
Thanks
It seems because of the new large titles, IOS11 requires the constraints on the custom view in the navigationItem.titleView to be set.
Do this for example:
customView.widthAnchor.constraint(equalToConstant: 200).isActive = true
customView.heightAnchor.constraint(equalToConstant: 44).isActive = true
self.navigationItem.titleView = customView
Note this must be done for both width and height.
It should work. No need to add a button, at least in my case...
This was suggested by Apple to ensure that you don't have zero-size custom views. See slide 33 in https://developer.apple.com/videos/play/wwdc2017/204/
Looks like touches are broken for navigationItem.titleView. Gestures, tap events and buttons - nothing works
Seems like a bug in iOS 11: https://forums.developer.apple.com/thread/82466
I provisionally implemented this workaround:
private lazy var navBarActionButtonIOS11: UIButton = {
button.addTarget(self.navTitleView, action: #selector(self.navTitleView.didTapView), for: .touchUpInside)
return button
}()
[...]
navigationItem.titleView = navTitleView
if #available(iOS 11.0, *), let navBar = navigationController?.navigationBar {
navBarActionButtonIOS11.removeFromSuperview()
navBar.addSubview(navBarActionButtonIOS11)
navBarActionButtonIOS11.center.x = navBar.center.x
}
Another solution could be to just assign a UIButton to navigationItem.titleView directly.
I hope Apple fixes this soon!
Well, I had same problem. I have UIButtons in UINavigationItem.titleView and those were not reacting to touches at all. Problem is that the view where those buttons are where of size (0,0) because of auto layout. So to fix this problem you need to add additional view into your custom view, lets call it "contentView" and put all your controls inside that contentView. Also, contentView must have defined size with constraints. Quick test is to add width and height constraint to contentView. And all works again.
Hope that this helps someone.
I have a UISplitViewController in my application (MvvmCross / Xamarin iOS) and for some reason I cannot get the content to respect the dimensions of the available view areas.
In the situation shown in the screenshot the master view is hosting a UIViewController with a TableView inside. All the layouts are done with constraints and work fine on their own when running in an iPhone emulator.
As soon as I switching to running on an iPad some custom code I have in my presenter shows this same view in the master panel of a UISplitViewController but in this situation the constraints seem to be ignored and I end up with a view that looks like this:
As you can see the right hand side of the table cell is now way off the viewable area of the master panel of the UISplitViewController.
Both the UITableView and the UITableCell both use View.Frame as their initial size (I've tried View.Bounds as well).
How can I get the cells and / or table to respect the bounds of the UISplitViewController available space?
Thanks to Cheesebarons question I found my solution (cause).
I have a set of methods in a helper class that I use to generate my "default" UIViews.
One of these methods creates my default UITableView:
public static UITableView CreateDefaultTableView(CGRect rect, UITableViewStyle style)
{
var tv = new UITableView(rect, style)
{
AutoresizingMask = UIViewAutoresizing.FlexibleHeight,
SeparatorStyle = UITableViewCellSeparatorStyle.SingleLine,
SeparatorColor = IosConstants.DefaultTableSeparatorColor,
BackgroundColor = IosConstants.DefaultViewBackgroundColor
};
return tv;
}
Changing:
AutoresizingMask = UIViewAutoresizing.FlexibleHeight,
To:
AutoresizingMask = UIViewAutoresizing.All,
Schoolboy error!
It appears that like in native Xcode, in Xamarin studio for ioS if your first sub-view in a view hierarchy is a UITextView, then adding a top-layout constraint to this view will cause a large block of empty space to appear at the top of the scroll view.
This is similar to the Xcode version of the question here
iOS 7 UITextView vertical alignment
I solved this programmatically by adapting
Tanguy-G's answer for native Objective-C by setting AutomaticallyAdjustsScrollViewInsets to false in the constructor of my view controller.
public MyLovelyViewController (IntPtr handle) : base (handle)
{
this.AutomaticallyAdjustsScrollViewInsets = false;
}
EDIT: If you are setting any other aspects of the text view in question programmatically, make sure to call
this.AutomaticallyAdjustsScrollViewInsets = false;
after any other adjustments, otherwise the space will reappear, e.g:
confirmSummary.Editable = false;
confirmSummary.Selectable = false;
this.AutomaticallyAdjustsScrollViewInsets = false;
(I did this in the override of ViewDidLoad)