Complex UIView lays out differently if I have IBOutlet for constraint - ios

In the screen shots below, the only change I have made is to connect an IBOutlet for a constraint. Unconnected, screen lays out correctly. Connected, the screen lays out incorrectly. I have not seen this happen before and don't know what to try to fix it. I need to be able to modify the constant value of the constraint in order to resize a subview depending on the presence or absence of a particular item.
The IBOutlet is declared as:
#IBOutlet weak var tabContentBottomConstraint: NSLayoutConstraint!
So far, I have not implemented any code that reads or modifies this constraint. I added it to my view controller in preparation for using it, but have not gone any further because when I run the code after adding the IBOutlet, the view sizes incorrectly. I have added and removed several times, always with the same result.
Here is the debugger info on the view sizings prior to connecting the outlet:
And here is the debugger info after connecting the IBOutlet with no other code change whatsoever:
The difference in y offset is huge and pushes the view off the screen. As I said above, no code reads or writes to that IBOutlet.
The rolePageDrawerView is item2 in the constraint, and item1 is the view controller's view.safeAreaLayoutGuide. rolePageDrawerView is embedded 3 levels deep into child views of view.safeAreaLayoutGuide.

I found a way to work around this weird behavior by programmatically searching out the constraint with the following code:
guard let tabContentBottomConstraint = view.constraints.filter({ $0.secondItem! as! NSObject == rolePageTabContentView }).first else {
fatalError("unable to find the constraint for drawer sizing")
}
self.tabContentBottomConstraint = tabContentBottomConstraint
I left the constraint as a weak reference even though it is no longer injected since the view itself will have a reference to it to keep it alive. It works.
HOWEVER, I'm leaving the question open for a better solution. The above search is very fragile some the layout change in the future. It would be much better if the framework was injecting the constraint without breaking the layout. Still open for better solutions.

Related

Value of type 'UIView' has no member 'constant'

#objc func handleKeyboardDidShow (notification: NSNotification)
{
let keyboardRectAsObject = notification.userInfo![UIResponder.keyboardFrameEndUserInfoKey] as! NSValue
var keyboardRect = CGRect.zero
keyboardRectAsObject.getValue(&keyboardRect)
self.changePassView.constant = -1 * keyboardRect.height/2
UIView.animate(withDuration: 0.5,animations: {
self.view.layoutIfNeeded()
})
}
Can anyone help me? Why I am getting this error as I am a beginner to iOS and learning this language.
self.changePassView.constant = -1 * keyboardRect.height/2
You're getting the error because you're trying to access a property called constant in self.changePassView, which is apparently a UIView. It looks like you're trying to change the value of the view's bottom constraint, but a layout constraint is a separate object that you'll have to get before you can set it's value. This might help: How to get constraint "bottomSpace" from UIView Programmatically?
UIViews contain information about their own appearance and content.
NSLayoutConstraints are often added to UIViews to modify how they're distributed across the screen.
You're receiving an error because you're attempting to access a property, constant, that is not found in UIViews. You're close though. You need a NSLayoutConstraint.
Step 1: Set Constraints
You can do this programmatically (read more here) or with the help of storyboards / nibs. Here's a step-by-step for the storyboard/nib approach:
Add a constraint in your storyboard or nib with auto layout.
Create an NSLayoutConstraint variable in your UIView class.
Back in your storyboard, find your newly-created constraint. It'll show up in your view navigator on the left, next to your UIView.
Select the constraint your looking for, and navigate to the rightmost tab of Xcode's right-hand panel.
Drag a "New Referencing Outlet" from the right panel to your UIViewController class in your view navigator.
If done correctly, an option for your new NSLayoutConstraint will appear. Select it.
Now the variable in your code is references the constraint you made visually. If these steps are confusing and you're very much a beginner (I can't quite tell) please reference this thorough guide to storyboards.
Step 2: Putting it Together
Access your NSLayoutConstraint's constant value. Your code should look like something like this once all of the above steps are complete:
// In your class's definitions.
// This is what you'll have to link programmatically or in your storyboard/nib
#IBOutlet var changePassViewBottomConstraint: NSLayoutConstraint!
// Later on in your function:
self.changePassViewBottomConstraint.constant = -1 * keyboardRect.height/2

Anchoring custom views to existing view in Mapbox NavigationViewController in Swift

I'm using the Mapbox framework on iOS in Swift to create a custom NavigationViewController. I need help getting a reference to the FloatingStackView on the NavigationView.
I've added an additional button (see top left of screenshot) and I'd like to anchor it to the FloatingStackView on the right so that it moves up and down as the top banner changes (similar to the FloatingStackView behavior). The trouble is I can't seem to access the FloatingStackView. From what I've seen in the Mapbox source code the FloatingStackView is a subview of a NavigationView which is the view of a RouteMapViewController which is the mapViewController of a NavigationViewController. Trouble is I can't access the mapViewController as it is inaccessible due to internal protection level.
Here's an example of what I'd like to do:
import UIKit
import MapboxNavigation
class DemoNavigationViewController: NavigationViewController {
override func viewDidLoad() {
super.viewDidLoad()
let mapVC = mapViewController
// Do any additional setup after loading the view.
}
}
Here's the error on the let mapVC line:
'mapViewController' is inaccessible due to 'internal' protection level
My end goal would be to be able to access the FloatingStackView via something like this:
let navigationView = mapViewController.view as? NavigationView
let floatingStackView = navigationView.floatingStackView
If I can get a reference to that floatingStackView I think I can handle the layout.
How can I get around the is inaccessible due to 'internal' protection level error or find another way to access the floating stack view?
From NavigationView class source code
You will not be able to reference it directly if it is marked internal. There is however a hacky way to get to any view and use it for AutoLayout (assuming the view uses autoLayout and has anchors you can use).
As each view has a subViews array you can iterate through the subviews to find the one you want. All subViews are visible, even the ones that come from private instances. The trick is identifying the one you need.
If the Type definition is accessible, you can do something like this
let navView = viewController.subViews.first{$0 is NavigationView}!
let stackView = navView.subViews.first{$0 is UIStackView}!
floatingButton.topAnchor.constraint(equalTo: stackView.topAnchor).isActive true
If you don't have a Type you can match against you'll have to do something even more hacky, such as matching against the description: .first{$0.description conatins "stack"}. Or even by trial and error by indexing the subView array until you get the result you want, and then hoping the order of views is always the same!
Whether you'd want to risk this in production code is only a decision you can make as it subject to breaking if the library changes it's UI design. Depending on the app's user base I might risk matching against type, but probably not the other ways!
Note I've force-unwrapped the views above for brevity - you'd want to be a bit more cautious in production code with that too :-)

Resize subview when superview finishes loading

I know theres countless similar questions on this that either all result in using flexible height/width or setting translatesAutoresizingMaskIntoConstraints to false.
I have add a view using an extension I created:
extension UIView {
func addView(storyboard: String, viewIdentier: String) {
let story = UIStoryboard(name: storyboard, bundle: nil)
let subview = story.instantiateViewController(withIdentifier: viewIdentifier)
subview.view.frame = self.bounds
self.addSubview(subview.view)
}
}
If I use this to initialise a view, in the ViewDidAppear everything works fine. But if its in the view did load then the constraints are all over the place because the contrainView that contains the view has its own constraints that are not yet loaded.
I currently use like this:
#IBOutlet weak var container: UIView!
override func viewDidLoad() {
container.addView(storyboard: "Modules", viewIdentifier: "subview")
}
I don't want to initialise the view in the ViewDidAppear because of reasons. Is there any way of fixing this so it works as expected? Or reload the subview constraints after the superview has loaded?
Also I've really tried using other solutions. I can't make this work so would really appreciate some help.
There is no way to do things in the hard-coded manner your code proposes. You are setting the frame of the subview based on self.bounds. But in viewDidLoad, we do not yet know the final value for self.bounds. It is too soon.
The appropriate place to do something that depends on knowing layout values is after layout has taken place, namely in viewDidLayoutSubviews. Be aware that this can be called many times in the course of the app's lifetime; you don't want to add the subview again every time that happens, so use a Bool flag to make sure that you add the subview only once. You might, on subsequent resizes of the superview, need to do something else in viewDidLayoutSubviews in order to make appropriate adjustments to the subview.
But the correct way to do this kind of thing is to add the subview (possibly in viewDidLoad) and give it constraints so that its frame will henceforth remain correct relative to its superview regardless of when and how the superview is resized. You seem, in your question, to reject that kind of approach out of hand, but it is, nevertheless, the right thing to do and you should drop your resistance to it.

How to access custom view properties after initialize

I'm targeting IOS 8+.
I have a form that is used in more than one place. So I decided to create a custom view where I define the various "form" text fields.
I have built my XIB, and the UIView subclass contains the outlets for each textField.
The view is composed of a background image and a scroll with the form fields over it.
Now, my first obstacle was: I need to have this custom view in a container that may or may not have a navigation bar. This made me create a constraint outlet so I could update its value to push down the scroller view. This way I'd have the whole image in the frame, the top being behind the navbar and the scroller bellow the nav bar).
Here's a manual drawing to help understanding the problem.
It's very possible that I'm making a lot of mess and confusion on my way to solve this. :)
The problem is:
After awakeFromNib runs I have no access to the constraint property. I then noticed the same thing happens for the TextFields outlets.
So, how can I access the custom view's properties when I instantiate them programatically?
Something like:
Controller:
let customView = SignupView(frame: f)
view.addSubview(customView)
customView.pushScrollerDownBy(50.0)
Custom view:
func pushScrollerDownBy(yOffset: CGFloat) {
//topScrollerConstraint is the outlet for the textField.
topScrollerConstraint.constant = yOffset //right now topScrollerConstraint is nil.
}
You should check if you have connected your topScrollerConstraint to the file's owner since it will not get instantiated and therefore, error. Here is a recent SO question regarding difference between these two:
What is File’s owner in XIB in this case?

How are NSTableCellViews supposed to be laid out?

I have a fairly basic MainWindow.xib with a source list-style sidebar. I created it by dragging the Source List template into the window, which already contains two NSTableCellViews: HeaderCell and DataCell.
The latter consists of an icon (using NSImageView) and a label (NSTextField). Instead, I want the label and another, smaller label underneath. In IB, this looks as follows:
If I focus on just DataCell, it highlights accordingly:
Thing is, actually running the program, it looks nothing like the template:
Notice how the two NSTextFields just get smashed together into one. My understanding was that view-based NSOutlineViews (and view-based NSTableViews, for that matter) are supposed to be designed as a template from within IB. Instead, the dimensions from the template seem to get mostly ignored.
Here's the code that sets the view's values from the data source:
public class TourSourceListDelegate : NSOutlineViewDelegate
{
public override bool IsGroupItem(NSOutlineView outlineView, MonoMac.Foundation.NSObject item)
{
return (item as TourSourceListDataSource.Item).IsHeader;
}
public override NSView GetView(NSOutlineView outlineView, NSTableColumn tableColumn, MonoMac.Foundation.NSObject item)
{
if (IsGroupItem(outlineView, item))
{
return outlineView.MakeView("HeaderCell", this);
}
else
{
var data = item as TourSourceListDataSource.Item;
var dataView = outlineView.MakeView("DataCell", this);
(dataView.Subviews[0] as NSTextField).StringValue = data.Name;
(dataView.Subviews[1] as NSTextField).StringValue = data.Date_start.ToShortDateString();
return dataView;
}
}
}
I've tried overriding GetRowHeight, but that doesn't seem to resolve the problem (it makes more room, but still doesn't let the views distribute themselves properly), nor does it seem necessary.
I've also tried playing with the various Autosizing, Autoresizes Subviews, etc. toggles in IB, but that doesn't seem to produce intuitive results, and again, it doesn't seem necessary — the view as presented in IB is exactly what I want, just with slightly longer labels in practice.
I haven't tried converting this to AutoLayout yet.
What obvious step am I missing?
Some more info that probably doesn't make a difference: this is a Xamarin.Mac/MonoMac project with Xcode 5.0, MacOSX10.8.sdk, Xamarin Studio 4.0.12, Xamarin.Mac 4.0.12, and Mono 3.2.3 (targeting Mono / .NET 4.0). I've also enabled App Sandboxing.
What's important in interface builder is the view hierarchy. What kind of view is that cell? Are those labels really subviews of the cellview or not? The hierarchy should look something like:
One thing that's fishy that I see is accessing dataView.Subviews[0] and [1]. If you're adding subviews to your cells then should be creating your own NSTableViewCell subclasses, with each view connecting to the subclass' IBOutlet properties. The subclass doesn't need any code in its implementation, just the declaration of its properties in #interface, such as titleField and descriptionField, and an empty #implementation that auto-synthesizes them.
Then makeViewWithIdentifier (or apprently the glue MakeView in Xamarin) when passed the right identifier should create your NSTableViewCell subclass, and at runtime you can verify that using po dataView in the debugger. Then you access the subviews using the properties of your NSTableViewCell subclass' interface instead of assuming which view is in which position with the subview array, using dataView.titleField and dataView.descriptionField.
If your cell view has one text field then you can use NSTableViewCell without subclassing, but do connect up the textField outlet (its connected by default as long as you don't delete & recreate the cell view's label view) so you can access it through the property, again instead of having to dive into the subviews array.
All that said, it's not really clear why you're seeing what you are. It looks like those aren't the subviews you expect, and might even look like the wrong fonts as well as in the wrong positions. Using a custom subclass of NSTableViewCell and verifying its class at runtime is a good way of making sure it's creating the view you expect, but you can also dump the subview within the debugger using po [dataView _subtreeDescription].

Resources