Discrepancy in the widths of programmatically added views vs. interface builder views - ios

I have come across a conundrum of sorts in regards unexpected (at least for me) sizes of UIViews.
Consider this UIViewController class. interfaceBuilderView was declared in a Storyboard file and constrained to take up the whole area of the UIView. So, I would expect to have interfaceBuilderView be the same size as programicallyCreatedView when calling *.frame.width. But they aren't.
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var interfaceBuilderView: MyCustomView!
override func viewDidLoad() {
super.viewDidLoad()
let programmicallyCreatedView = MyCustomView(frame: self.view.frame)
//commented out this to get first picture
self.view.addSubview(programmicallyCreatedView)
self.interfaceBuilderView.setAppearance()
self.programmicallyCreatedView.setAppearance()
print(self.view.frame.width)//prints 375
print(self.interfaceBuilderView.frame.width)//prints 600
print(self.programmicallyCreatedView.frame.width)//prints 375
}
}
Now, consider this implementation of the MyCustomView class.
import UIKit
class MyCustomView: UIView {
func setAppearance() {
let testViewWidth: CGFloat = 200.0
let centerXCoor = (self.frame.width - testViewWidth) / 2.0
let testView = UIView(frame: CGRectMake(centerXCoor, 0, testViewWidth, 100))
testView.backgroundColor = UIColor.redColor()
self.addSubview(testView)
}
}
As you can see, I simply draw a red rectagle of width 200.0, and it is supposed to be centered. Here are the results.
Using the Interface Builder created view.
And using the programmatically created view.
As you can see, the programmatically created view achieves the desired results. No doubt because the size printed is the same as the superview (375).
Therefore, my question is simply why is this happening? Furthermore, how can I use a view declared in interface builder and programmatically add other views to it with dimensions and placement that I expect?

A few thoughts:
This code is accessing frame values in viewDidLoad, but the frame values are not yet reliable at that point. The view hasn't been laid out yet. If you're going to mess around with custom frame values, do this in viewDidAppear or viewDidLayoutSubviews.
Nowadays, we really don't generally use frame values anymore. Instead, we define constraints to define the layout programmatically. Unlike custom frame values, you can define constraints when you add the subviews in viewDidLoad.
You have the scene's main view, the MyCustomView and then yet another UIView which is red. That strikes me as unnecessarily confusing.
I would advise that you just add your programmatically created subview in viewDidLoad and specify its constraints. Using the new iOS 9 constraints syntax, you can just specify that it should be centered, adjacent to the top of the view, half the width, and one quarter the height:
override func viewDidLoad() {
super.viewDidLoad()
let redView = UIView()
redView.backgroundColor = UIColor.redColor()
redView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(redView)
NSLayoutConstraint.activateConstraints([
redView.centerXAnchor.constraintEqualToAnchor(view.centerXAnchor),
redView.topAnchor.constraintEqualToAnchor(view.topAnchor),
redView.widthAnchor.constraintEqualToAnchor(view.widthAnchor, multiplier: 0.5),
redView.heightAnchor.constraintEqualToAnchor(view.heightAnchor, multiplier: 0.25)
])
}
Clearly, adjust these constraints as suits you, but hopefully this illustrates the idea. Don't use frame anymore, but rather use constraints.

Related

How do I prevent Keyboard from adding its own height constraint when using view as inputAccessoryView

I'm trying to use a custom view as an accessory view over the keyboard, for various reasons, in this case, it is much preferred over manual keyboard aligning because of some other features.
Unfortunately, this is a dynamic view that defines its own height. The constraints all work fine outside of the context of an accessoryView without errors, and properly resizing
When added as a keyboardAccessoryView it seems to impose a height of whatever the frame is at the time and break other height constraints
It appears as:
"<NSLayoutConstraint:0x600003e682d0 '_UIKBAutolayoutHeightConstraint' Turntable.ChatInput:0x7fb629c15050.height == 0 (active)>"
(where 0 would correspond to whatever height had been used at initialization
It is also labeled accessoryHeight which should make it easy to remove, but unfortunately, before I can do this, I'm getting unsatisfiable constraints and the system is tossing my height constraints
Tried:
in the inputAccessoryView override, I tried to check for the constraints and remove it, but it doesn't exist at this time
setting translatesAutoresizing...Constraints = false
tl;dr
Using a view as a KeyboardAccessoryView is adding its own height constraint after the fact, can I remove this?
Looks like keyboard doesn't like inputAccessoryView with height constraint. However you still can have inputAccessoryView with dynamic height by using frame (it is still possible to use constraints inside your custom inputAccessoryView).
Please check this example:
import UIKit
final class ViewController: UIViewController {
private let textField: UITextField = {
let view = UITextField()
view.frame = .init(x: 100, y: 100, width: 200, height: 40)
view.borderStyle = .line
return view
}()
private let customView: UIView = {
let view = UIView()
view.backgroundColor = .red
view.frame.size.height = 100
view.autoresizingMask = .flexibleHeight // without this line height won't change
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(textField)
textField.inputAccessoryView = customView
textField.becomeFirstResponder()
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.customView.frame.size.height = 50
self.textField.reloadInputViews()
}
}
}

Does self.view has constraints?

self.view is on the ViewController. Does it have any constraints? I found it had a top and a left constraint. It has a frame for sure. The autolayout system starts from self.view. I mean until self.view the iOS system is still using frame for positioning the views. After that, our views are able to be added with autolayout. So in the viewcontroller, I use self.frame.size.width for the width constraint for my own view is the correct approach to do? Am I right?
Use SCREEN_WIDTH and It's very easy to get your device size and manage your own view properly:
let SCREEN_WIDTH : CGFloat = UIScreen.main.bounds.width
let SCREEN_HEIGHT : CGFloat = UIScreen.main.bounds.height
For more information you can refer this : link
Example :
You should add your own new view programatically.
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let SCREEN_WIDTH : CGFloat = UIScreen.main.bounds.width
let SCREEN_HEIGHT : CGFloat = UIScreen.main.bounds.height
let newView = UIView()
newView.translatesAutoresizingMaskIntoConstraints = false
newView.backgroundColor = UIColor.black
newView.frame.size = CGSize(width: SCREEN_WIDTH * 0.5 , height: SCREEN_HEIGHT * 0.5)
self.view.addSubview(newView)
self.view.layoutIfNeeded()
}
}
Here I am giving the frame to the constraints of my customized view. I don't want to give this value in a format of frame.size. I want to use somethings like constraint.constant. I mean pure autolayout.
self.view.frame is only accurate once viewDidLayoutSubviews() is called for the first time, typically just before viewDidAppear meaning if you try and access in viewDidLoad or first time viewWillAppear the frame will be wrong.
So you should just check in viewDidLayoutSubviews() (but note that function is called whenever the layout changes (this does not mean you scroll on a table view, it means the actual constraints change bc of a screen rotation for ex, or otherwise)
If you can't wait until that method or have some code that's not idempotent and don't want to have some like hasSetup bool to make sure you only do it once... Then you can either access the screen size directly via UIScreen.main.bounds
OR
I wouldn't necessarily recommend the following but for the sake of completeness I've also gotten away with tricks to get the view to layout earlier like calling view.layoutIfNeeded() earlier in the view lifecycle or on the view controller that initializes:
let myVC = MyViewController()
_ = myVC.view // fixes frame for some reason
Using AutoLayout correctly here
You want to use constraints but the frame shouldn't matter at all if you make relative constraints. Aka instead of saying height = constant you would say "make this the same height as vc.view" or "make the leading edge of this view 20 pixels right of the leading edge of vc.view".
something like:
let myView = UIView()
self.view.addSubview(myView)
myView.translatesAutoresizingMaskIntoConstraints = false
myView.heightAnchor.constraint(equalTo: self.view.heightAnchor)
myView.leadingAnchor.constraint(equalTo: vc.view.leadingAnchor, constant: 20.0) // 20 pixels to right of vc.view leading

How to make autolayout work with custom UIView on XIB?

I have a UIView on a XIB, containing an UIImageView and a UILabel with a small space in between. Horizontal layout is a simple chained |--image--label--| in which -- is some fixed space. The height is fixed to 40, this view is horizontally centred in its view controller, and it has an >= 100 width constraint.
If I change the label text, the width of my composed view updates as expected width the changed width of its label, and it stays nicely centred on the view controller.
Because I need this UIView, containing an image and label, in other places, I've created a custom class consisting of a XIB and Swift file. Let's call it ItemView.
Issue I have is that the empty UIView on my view controller XIB, which I've changed class to ItemView, no longer accepts the >= 40 width constraint. This is of course because the view controller XIB no longer sees the variable width UILabel, but instead just a plain UIView of class ItemView. I get an 'Inequality Constraint Ambiguity' IB error.
The result is that the width of my custom view remains 40. It works a little bit if I specify a larger >= label width; the text is then only cut off when this width is reached. But in that second case my custom view is no longer horizontally centred, but shifted a bit to the left instead.
How do I resolve this? Or, how can I tell IB to treat my custom ItemView in a similar way as a UILabel?
In my ItemView I've done all I could find:
override class var requiresConstraintBasedLayout: Bool
{
return true
}
Call setNeedsLayout() after setting the label text.
Call myLabel.translatesAutoresizingMaskIntoConstraints = false.
Call self.translatesAutoresizingMaskIntoConstraints = false.
And call self.setNeedsUpdateConstraints() in both init()s.
Configure this pop-up in the view's Size inspector:
Now IB won't worry about your size specifications. You know better than IB does, and this is how to tell IB that fact.
Another way: configure this pop-up in the view's Size inspector:
This tells IB that the view will have an intrinsic content size that it doesn't know about.
Either of those will work. As you can see in this screenshot, I've given my custom view a width constraint of greater-than-or-equal-to-40, but IB is not complaining of any error:
How I structured my custom UIView: My XIB's Files Owner is ItemView. ItemView has an #IBOutlet var: UIView! that's connected to the view in the XIB. Then I have:
init()
{
super.init(frame: .zero)
self.initView()
}
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
self.initView()
}
private func initView()
{
Bundle.main.loadNibNamed("ItemView", owner: self, options: nil)
self.view.frame = CGRect(x: 0.0, y: 0.0, width: self.width, height: self.height)
self.addSubview(self.view!)
}
(I seems this extra UIView is needed when creating a custom UIView with a XIB. Would love to hear if it's not needed after all.)
To make it work I needed to call self.invalidateIntrinsicContentSize() after updating the label. And override intrinsicContentSize:
override open var intrinsicContentSize: CGSize
{
return self.view.systemLayoutSizeFitting(self.bounds.size)
}
Note that I'm calling this on self.view, not on self.
Thanks #matt for pointing me in the right direction. Was the first time I've encountered something like this.
The following things I tried we all not necessary:
override class var requiresConstraintBasedLayout: Bool
{
return true
}
Call setNeedsLayout() after setting the label text.
Call myLabel.translatesAutoresizingMaskIntoConstraints = false.
Call self.translatesAutoresizingMaskIntoConstraints = false.
And call self.setNeedsUpdateConstraints() in both init()s.

UITableViewController layout not working

I am trying to position UITableView to the left side of the app, whole height but taking just 1 / 3 of the available width with the code like this:
class ViewController: UIViewController {
var tableController: UITableViewController?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
tableController = UITableViewController();
addChildViewController(tableController!)
self.view.addSubview(tableController!.view)
tableController!.didMove(toParentViewController: self)
}
override func viewDidLayoutSubviews() {
tableController!.view.backgroundColor = UIColor.red
tableController!.view.frame = CGRect(
x: 0,
y: 0,
width: view.bounds.width / 3,
height: view.bounds.height);
//tableController!.view.frame = view.bounds
}
}
And it looks like this:
And I don't get why the lines are not correctly aligned horizontally and it looks like being cut on the right.
If I give the view controller full width / height by uncommenting the last line, it looks better:
Question 1
Are you properly constraining the TableView in the parent view? You can set proportional constraints. Please see this post on the topic: AutoLayout to keep view sizes proportional
Side Note
For iOS 11, constraining views to layout guides will trigger warnings in your IDE. Consider embedding UI components inside another view that takes up the entire width of the parent view using Auto-resizing Masks. See the topic here: Xcode Auto Layout Resizing Issue

UIScrollView not scrolling in Swift

My UIScrollView won't scroll down. I don't know why. I already followed Apple documentation regarding to this issue.
#IBOutlet weak var scroller: UIScrollView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func viewDidLayoutSubviews() {
scroller.scrollEnabled = true
// Do any additional setup after loading the view
scroller.contentSize = CGSizeMake(400, 2300)
}
You need to set the frame of your UIScrollView so that it is less than the contentSize. Otherwise, it won't scroll.
Also, I would recommend that you add the following to your viewDidLoad method:
scroller.contentSize = CGSize(width: 400, height: 2300)
If you are using AutoLayout
Set content size in viewDidAppear which works for me.
override func viewDidAppear(_ animated: Bool) {
scrollView.contentSize = CGSize(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height+300)
}
Alot of the time the code is correct if you have followed a tutorial but what many beginners do not know is that the scrollView is NOT going to scroll normally through the simulator. It is suppose to scroll only when you press down on the mousepad and simultaneously scroll. Many Experienced XCode/Swift/Obj-C users are so use to doing this and so they do not know how it could possibly be overlooked by beginners. Ciao :-)
#IBOutlet weak var scrollView: UIScrollView!
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(scrollView)
// Do any additional setup after the view
}
override func viewWillLayoutSubviews(){
super.viewWillLayoutSubviews()
scrollView.contentSize = CGSize(width: 375, height: 800)
}
This code will work perfectly fine as long as you do what I said up above
Do not give fix height to scroll view and always give top of first subview to scrollview and bottom of last subview to scrollview. By this way scroll view will automatically grow as per the size of contained subviews. No need to give contentSize to the scrollview.It will work for small as well as large size iPhone.
Swift 3.0 version
scroller.contentSize = CGSize(width: scroller.contentSize.width, height: 2000)
If you are using autolayout, then the contentSize property stops working and it will try to infer the content size from the constraints. If that is the case, then your problem could be that you are not defining the necessary constraints to the content view so that the scrollview can infer the content size.
You should define the constraints of your content view to the top and bottom edges of the scrollview.
If you are using Storyboard:
Put your Content view inside the UIScrollView
Add top, bottom, left and right constraints with the scroll view
Add equal heights and widths constraints
For a vertical scroll set the Equal Heights Constraint priority to 250. For a horizontal scroll set the Equal Widths Constraint priority to 250
In my case, I used UIStackView inside UIScrollView.
Added some views-elements from code to stackview.
It won't scroll.
Fixed it by setting stackview's userInteractionEnabled to false.
The problem could be that your scrollView doesn't know its contentSize like stated above, but the fix is easier than what the above answers are. Like Carlos said but I will elaborate more. If you want your scrollView to scroll vertically(up & down), make your contentView which is in the hierarchy of the scrollView equal width to the ViewController and give it a height constraint that works for your project i.e. 700. For the opposite(horizontally) make the height equal to the ViewController and the width some big number that works for your project.
FWIW, I found that I needed to use sathish's solution from above, though it was insufficient to effect the intervention in viewDidAppear alone. I had to instead make the adjustment for every new content assignment:
func display(pattern: Pattern) {
let text : NSAttributedString = pattern.body()
patternTextView.attributedText = text
// Set the size of the view. Autolayout seems to get in the way of
// a correct size calculation
patternTextView.contentSize = CGSize(width: 348, height: 620)
}
The manifest constants (yeah, I know, yuk, but it makes it easier to understand here) are from the autolayout spec.
It worked for me. In Size Inspector
Layout = Translates Mask into constraints.
Autoresizing = all click.
For Swift 5.6 and iOS 15:
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
let subView: UIView = UILabel()
subView.text = String(repeating: "MMMMMMM ", count: 100)
NSLayoutConstraint.activate([
subView.topAnchor.constraint(equalTo: scrollView.topAnchor),
subView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
subView.leftAnchor.constraint(equalTo: scrollView.leftAnchor),
subView.rightAnchor.constraint(equalTo: scrollView.rightAnchor),
// Constrain width so the label text wraps and we scroll vertically.
subView.widthAnchor.constraint(lessThanOrEqualTo: scrollView.widthAnchor),
])
Increase the content Height work for me.
I do not know it is a good solution, but you can try to set headerview to empty UITableView.
let scrollView: UIView = UIView(frame: CGRectMake(0, 0, 400, 2300))
tableView.tableHeaderView = scrollView

Resources