I have a UICollectionViewController embedded inside a UINavigationController which in turn embedded inside a UITabBarController.
I want to add a UIView to the UICollectionViewController just above the tab bar (shown by red rectangle).
I have the UIView created separately as a nib file.
import UIKit
class BottomView: UIView {
#IBOutlet var view: UIView!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
fileprivate func commonInit() {
Bundle.main.loadNibNamed("BottomView", owner: self, options: nil)
view.frame = self.frame
view.autoresizingMask = [.flexibleHeight, .flexibleWidth]
addSubview(view)
}
}
And I initialize and add it in the UICollectionViewController like so.
class CollectionViewController: UICollectionViewController {
fileprivate var bottomView: BottomView!
override func viewDidLoad() {
super.viewDidLoad()
let yPos = view.bounds.height - (tabBarController!.tabBar.frame.size.height + 44)
bottomView = BottomView(frame: CGRect(x: 0, y: yPos, width: view.bounds.width, height: 44))
collectionView?.addSubview(bottomView)
}
// ...
}
I figured if I substract the combined height of the bottom view plus the tab bar from the entire view's height, I should be able to get the correct y position value. But it's not happening. The view is getting added but way off screen.
How do I calculate the correct y position without hardcoding it?
Example demo project
I would suggest adding the BottomView to the UICollectionViewController's view rather than to the collection view itself. This is part of the problem you're having.
You're also trying to set the frame of the BottomView in the viewDidLoad() method using values from view.bounds. The CGRect will return (0.0, 0.0, 0.0, 0.0) at this point because the layout has yet to take place, which is most likely why your positioning is off. Try moving your layout logic to the viewWillLayoutSubviews() method and see if that helps.
A better approach would be by setting auto layout constrains rather than defining a frame manually, this will take a lot of the leg work out for you.
Here's a quick example:
self.bottomView.translatesAutoresizingMaskIntoConstraints = false
self.view.insertSubview(self.bottomView, at: 0)
self.bottomView.bottomAnchor.constraint(equalTo: self.view.layoutMarginsGuide.bottomAnchor).isActive = true
self.bottomView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
self.bottomView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
self.bottomView.heightAnchor.constraint(equalToConstant: 100.0).isActive = true
You can apply autolayout logic in your viewDidLoad() and it should work correctly.
You can find some more information on setting autolayout constraints programatically here:
https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/AutolayoutPG/ProgrammaticallyCreatingConstraints.html
Sounds what you want to achieve is exactly the footer view for the UICollectionView.
A footerView is like a view that will stick to the bottom of the collectionView and wont move with the cells.
This will help you add a footer View: https://stackoverflow.com/a/26893334/3165112
Hope that helps!
Related
I am trying to create a view using xib and dynamically presenting in the viewController. I am passing the frame parameters while setting up the view :
let frame = CGRect(x: 0, y: 171, width: self.view.bounds.width, height: self.view.bounds.height - 327)
self.xibView = xibView(frame: frame)
self.view.addSubview(self.xibView)
self.xibView.delegate = self
in the Xib view My structure looks like this :
I have added stackView so that the content resizes automatically based on frame size that we provide. On the code, we are doing :
class xibView: UIView {
// some other declarations
#IBOutlet weak var sharpBtn: UIView!
#IBOutlet weak var contentView: UIView!
public var delegate: xibViewDelegate?
override init(frame: CGRect){
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit(){
// write view stuff here
Bundle.main.loadNibNamed("xibView", owner: self, options: nil)
addSubview(contentView)
contentView.translatesAutoresizingMaskIntoConstraints = true
}
.
.
.
}
Here contentView is the view below safe area that we have in the hierarchy.
When I am trying to run it, The descriptor for the view gives me :
<UIView: 0x7fd5c54077a0; frame = (0 0; 835 867); autoresize = RM+BM; layer = <CALayer: 0x600001708500>>
This is the size of freeform xib that I have created. I am not sure if there is something that I am missing to make the view resize to the frame that we provide.
Can anyone help?
For the update I was able to fix the following issue by assigning the frame to the view in layoutSubviews function.
override func layoutSubviews() {
super.layoutSubviews()
// we need to adjust the frame of the subview to no longer match the size used
// in the XIB file BUT the actual frame we got assinged from the superview
self.contentView.frame = self.bounds
}
and then setting the translatesAutoresizingMaskIntoConstraints to true in the init function :
contentView.translatesAutoresizingMaskIntoConstraints = true
I have a custom view and xib. I use this custom view in one of my storyboard's controller views.
I have a use case where I want to be able to hide the custom view (and bring its height to zero). Right now, I set the height in the interface builder and set constraints to the superview's edges:
As you can see, I want its height to be 84 everywhere.
Now here is my custom view's class:
import UIKit
#IBDesignable
class BannerView: UIView {
#IBOutlet var contentView: UIView!
override init(frame: CGRect) {
super.init(frame: frame)
initialize()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initialize()
}
override func awakeFromNib() {
super.awakeFromNib()
initialize()
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
initialize()
contentView?.prepareForInterfaceBuilder()
}
func initialize() {
guard let view = loadViewFromNib() else { return }
view.frame = self.bounds
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
addSubview(view)
contentView = view
}
func loadViewFromNib() -> UIView? {
let bundle = Bundle(for: type(of: self))
let nib = UINib(nibName: "BannerView", bundle: bundle)
return nib.instantiate(withOwner: self, options: nil).first as? UIView
}
func hide() {
// Hide the view and set its height to zero here
}
}
But, now I'm confused... should I also be setting a height constraint on the custom view when I load it into one of my storyboards? Or should its height be 84 everywhere and I shouldn't have to specify it any further?
Also, how would I hide the custom view and set its height to zero in the above hide() function?
There are several ways to do this... here's one.
Give the content of your xib constraints to make its height 84-pts. You haven't shown your xib's layout, but I'll assume you know how to do that.
Then, when you add BannerView to your main view (I'm guessing that's what you're doing), embed it in a Vertical UIStackView with these properties:
Now, when you set bannerView.isHidden = true, the stack view automatically removes it from the height calculations, resulting in the stack view having a height of Zero.
Setting bannerView.isHidden = false will then re-display the banner view along with its height.
As you want the view's height to be 84 everywhere I think you should add a height constraint outlet and set the value 84.
Set the constraint value to 0 to hide the view (I highly suggest not to hide some view by doing this).
I have a custom xib that has a horizontal stack view with 3 child views. I use this view in other xibs. When the user clicks on one of these child views, I scale it using:
view.transform = CGAffineTransform(scaleX: 2, y: 2)
When the view is scaled, it's actually meant to appear out of the parent view's bounds. However, when I click the child view, it scales it fine, but the view is cut off.
I've double checked that all of my views in the xib have clipsToBounds set to false, but it's still cutting it off.
Here is what my xib class looks like:
class CustomView: UIView {
#IBOutlet var contentView: UIView!
#IBOutlet weak var stackView: UIStackView!
override init(frame: CGRect) {
super.init(frame: frame)
initialize()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initialize()
}
func initialize() {
Bundle.main.loadNibNamed("CustomView", owner: self, options: nil)
addSubview(contentView)
contentView.frame = self.bounds
contentView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
}
... some other things that I've left out that aren't important ...
}
Is there something I'm missing? How do I make the child views not get clipped/cut off?
What about the view hosting the XIB? (CustomView in this case). Set clipsToBounds=false in your initialize. Otherwise, take a look at Xcode's view hierarchy debugger and see if there's another view that causing the clipping.
So I have a custom UIView class
class MessageBox: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
createSubViews()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
createSubViews()
}
func createSubViews() {
let testView = UIView(frame: self.frame)
testView.backgroundColor = UIColor.brown
self.addSubview(testView)
}
}
I added a UIView inside the storyboard and gave it some constraints:
100 from the top (superview), 0 from the left and right, height is 180
But when I run the app the brown subview I created in the code is way to big. I printed self.frame in my custom view and it turns out that the frame is (0,0,1000,1000). But why? I set constraints, it should be something like (0,0,deviceWith, 180).
What did I do wrong?
EDIT: That's my Storyboard setup:
Short and simple answer:
You're doing it too early.
Detailed answer:
When a view is initialized from an Interface Builder file (a xib or a storyboard) its frame is initially set to the frame it has in Interface Builder. You can look at it as a temporary placeholder.
When using Auto Layout the constraints are resolved (= the view's actual frame is computed) inside the view's layoutSubviews() method.
Thus, there are two possible solutions for your problem:
(preferrable) If you use Auto Layout, use it throughout your view.
Either add your testView in Interface Builder as well and create an outlet for it
or create your testView in code as you do, then set its translatesAutoResizingMaskIntoConstraints property to false (to sort of "activate Auto Layout") and add the required constraints for it in code.
Set your testView's frame after the MessageBox view's frame itself has been set by the layout engine. The only place where you can be sure that the system has resolved the view's frame from the constraints is when layoutSubviews() is called.
override func layoutSubviews() {
super.layoutSubviews()
testView.frame = self.frame
}
(You need to declare your testView as a property / global variable, of course.)
Try to use the anchors for your view:
MessageBox.centerXAnchor.constraintEqualToAnchor(self.view.centerXAnchor).active
= true
MessageBox.centerYAnchor.constraintEqualToAnchor(self.view.centerYAnchor).active
= true
MessageBox.widthAnchor.constraintEqualToConstant(150).active = true
MessageBox.heightAnchor.constraintEqualToConstant(100).active = true
This method have to be used inside your class
override func layoutSubviews() {
super.layoutSubviews()
testView.frame = self.frame
}
this also works when you add a custom class to a UIView in the storyboard and that uses autolayout.
thanks Mischa !
try to add a height and width constraint relative to the superview height, with some multiplier.
I'm trying to make UIViews which each contain different statements of text (In UITextViews). There can be a varying number of views and each statement can be different in length. I make these views using
let newView = DragView(heightOfView: ???, viewNumber: i, heightFromTop: currentHeightForThings)
In the DragView class I then access the statement using the viewNumber and put the statement in the text label in a nib file I've made.
My issue is I have nothing to put in heightOfView. The height I want is the height of the textLabel which varies depending on how many lines are in the textView for the statement. However I can't access this height because the textLabel isn't built yet.
Thanks in advance, I'm new to swift but want to learn fast so I apologise if I'm missing something obvious!
Heres the code I have in the class DragView
class DragView: UIView {
#IBOutlet var dragView: UIView!
#IBOutlet weak var statementLabel: UITextView!
var dropTarget: UIView?
var viewNumber: Int!
init(heightOfView: Int, viewNumber:Int, heightFromTop: Int) {
self.viewNumber = viewNumber
let startingPosition = CGRect(x: Int(widthCentre) - dragViewWidth / 2, y: heightFromTop, width: dragViewWidth, height: heightOfView)
super.init(frame: startingPosition)
NSBundle.mainBundle().loadNibNamed("DragView", owner: self, options: nil)
self.addSubview(self.dragView)
let movingView = MovingView(frame: startingPosition)
self.addSubview(movingView)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
Here the movingView is a subview I add over the view to move the view.
You can override the DragView's sizeThatFits method to have it return a height based on the statementLabel's height. You will have to first call sizeToFit on the textView which will set the height for that view, then return a height based on that.
override func sizeThatFits(size: CGSize) -> CGSize {
self.statementLabel.sizeToFit()
return CGSize(
width: self.frame.width,
height: self.statementLabel.frame.height)
}
Additionally, I would recommend looking into sizeThatFits and layoutSubviews if you are going to be doing programatic layout. The sizing and positioning of subviews should be taking place in layoutSubviews rather than init.