Why are my subview bounds (0,0,0,0) after setting constraints? - swift

I am using the latest version of swift and writing everything programmatically. I’m trying to create a UIView holderView that resides inside and is constrained to the bounds of the safe area of the top level view. This code returns
(0.0, 0.0, 414.0, 896.0)
(0.0, 0.0, 0.0, 0.0)
which suggests that the holderView is not constrained to the top level view. Can anyone please advise on how to proceed? Code below.
class WelcomeViewCon: UIViewController {
var holderView = UIView()
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
configure()
}
private func configure() {
view.backgroundColor = .systemRed
view.addSubview(holderView)
holderView.backgroundColor = .systemGray
let constraints = holderView.constraintsForAnchoringTo(boundsOf: view)
NSLayoutConstraint.activate(constraints)
print(view.bounds)
print(holderView.bounds)
}
}
extension UIView {
/// Returns a collection of constraints to anchor the bounds of the current view to the given view.
///
/// - Parameter view: The view to anchor to.
/// - Returns: The layout constraints needed for this constraint.
func constraintsForAnchoringTo(boundsOf view: UIView) -> [NSLayoutConstraint] {
return [
topAnchor.constraint(equalTo: view.topAnchor),
leadingAnchor.constraint(equalTo: view.leadingAnchor),
view.bottomAnchor.constraint(equalTo: bottomAnchor),
view.trailingAnchor.constraint(equalTo: trailingAnchor)
]
}
}

It’s just a matter of timing. Constraints do not take effect until after layout. But you are applying constraint during layout (which is totally wrong; this is what updateConstraints is for, or just do it all once in viewDidLoad) and so you cannot measure the results until after the next layout.
Moreover layout happens many times so your code adds the subview and the constraints over and over. Dangerous stuff.

Related

Prevent cell content from "jumping" when applying constraint

I have a subclassed UICollectionViewCell and I want it to expand when tapped.
To achieve this, I put the title into a view ("titleStack") and the body into a separate view ("bodyStack"), and then put both of them into a container UIStackView ("mainStack"). I then constrain the contentView of the cell to the leading, trailing, and top edges of mainStack.
When the cell is selected, a constraint is applied that sets the bottom of the contentView's constraint to be the bottom of bodyStack. When it's unselected, I remove that constraint and instead apply one that sets the contentView's bottom constraint equal to titleStack's bottom constraint.
For the most part this works well, but when deselecting, there's this little jump, as you can see in this video:
What I would like is for titleStack to stay pinned to the top while the cell animates the shrinking portion, but it appears to jump to the bottom, giving it a sort of glitchy look. I'm wondering how I can change this.
I've pasted the relevant code below:
private func setUp() {
backgroundColor = .systemGray6
clipsToBounds = true
layer.cornerRadius = cornerRadius
setUpMainStack()
setUpConstraints()
updateAppearance()
}
private func setUpMainStack() {
contentView.constrain(mainStack, using: .edges, padding: 5, except: [.bottom])
mainStack.add([titleStack, bodyStack])
bodyStack.add([countryLabel, foundedLabel, codeLabel, nationalLabel])
}
private func setUpConstraints() {
titleStack.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
closedConstraint =
titleStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
closedConstraint?.priority = .defaultLow // use low priority so stack stays pinned to top of cell
openConstraint =
bodyStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
openConstraint?.priority = .defaultLow
}
/// Updates the views to reflect changes in selection
private func updateAppearance() {
UIView.animate(withDuration: 0.3) {
self.closedConstraint?.isActive = !self.isSelected
self.openConstraint?.isActive = self.isSelected
}
}
Thanks so much!
I was able to solve this by simply showing and hiding my "bodyStack" as well as using "layoutIfNeeded." I removed closedConstraint and openConstraint and just gave it a normal bottom constraint.
The relevant code:
func updateAppearance() {
UIView.animate(withDuration: 0.3) {
self.bodyStack.isHidden = !self.isSelected
self.layoutIfNeeded()
}
}

How to make mapView and segmentedControl move relative to AdMob adaptive banner

I'm adding an adaptive banner ad from AdMob to the bottom of a mapView in my iOS app. No problem adding it, but the banner obscures both a segmentedControl which is placed at the bottom of the mapView. (Both the mapView and the segmentedControl are inside a tabBarController, all created in Storyboard.) The ad also covers the Apple Mags logo and "Legal" information - not sure if Apple will have a problem with that.
Image example - top of segmentedControl barely visible under ad
How can I make the mapView shift up by the same amount of space taken up by the ad, so as to make the segmentedControl and Apple Maps boilerplate visible, and not shift up at all if no ad is loaded? (Or is there another best practice of handling this?)
All my AdMob code is from the official AdMob tutorial:
func getAdaptiveSize() -> GADAdSize {
var frame: CGRect
if #available(iOS 11.0, *) {
frame = view.frame.inset(by: view.safeAreaInsets)
} else {
frame = view.frame
}
let viewWidth = frame.size.width
return adSize
}
func loadAdaptiveBannerAd(){
bannerView.adSize = getAdaptiveSize()
bannerView.load(GADRequest())
}
func addBannerToView(){
bannerView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(bannerView)
NSLayoutConstraint.activate([
bannerView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
bannerView.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
}
override func viewDidLoad() {
super.viewDidLoad()
bannerView.adUnitID = "ca-app-pub-3940256099942544/2435281174"
bannerView.rootViewController = self
bannerView.backgroundColor = .darkGray
addBannerToView()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
loadAdaptiveBannerAd()
}
My hunch is that I have to fix this not by changing the placement of the ad banner itself, but by approaching either the mapView or the superview somehow, but I haven't found the solution.
You didn't show your actual layout, but I'm going to assume it looks very close to this:
The Bottom of your mapView is constrained to the Bottom of the safe-area, and the Bottom of your Segmented control is constrained to the Bottom of the mapView. If you have your SegControl also constrained to the safe-area, change that to the mapView -- that way we only have to manipulate the mapView and the SegControl will move with it.
First step, change the Priority of the Map view's bottom constraint to High (750):
and your constraints will look like this:
Next, connect that constraint to an #IBOutlet:
#IBOutlet var mapBottomConstraint: NSLayoutConstraint!
result:
Now at run-time, when you add your banner view, add a NEW Bottom constraint to your map view:
func addBannerToView(){
bannerView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(bannerView)
NSLayoutConstraint.activate([
bannerView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
bannerView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
// add this line
mapView.bottomAnchor.constraint(equalTo: bannerView.topAnchor)
])
}
Because the newly created constraint will have the default Priority of Required (1000), auto-layout will "pin" the bottom of the map view to the top of the banner. Since we gave the Storyboard-created Bottom constraint a Priority of 750, we've told auto-layout it's allowed to break that constraint without causing conflicts.
If / when you remove the banner view, that "new" constraint will be removed with it, and the Storyboard-created constraint will again be used.

How to give an NSOutlineView a tiled background image that scrolls?

I have a standard NSOutlineView. I would like it to have a background image, which tiles vertically, and which scrolls together with the outline view cells.
I've somewhat achieved this using the following in my ViewController:
class ViewController: NSViewController {
#IBOutlet weak var outlineView: NSOutlineView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
if let image = NSImage(named: "tile") {
let color = NSColor.init(patternImage: image)
outlineView.backgroundColor = color
}
}
}
That works, except when you scroll past the top or bottom of the view (with the stretch provided by the containing scroll view).
I've tried putting the background image on the scroll view, but then it is static and doesn't scroll with the outline view's content.
I've also tried subclassing various objects in the view hierarchy and overriding their draw(_ dirtyRect: NSRect) method and doing:
self.wantsLayer = true
self.layer?.backgroundColor = ...etc
but got no success from that either.
Can anyone provide any suggestions?
I ended up creating a new custom NSView:
class MyView: NSView {
override func draw(_ dirtyRect: NSRect) {
if let image = NSImage(named: "Tile") {
let color = NSColor.init(patternImage: image)
color.setFill()
dirtyRect.fill()
}
super.draw(dirtyRect)
}
}
Then in my ViewController class I added an instance of the custom view, and used autolayout constraints to pin the new view to my outlineView's clip view starting 2000points above it, and ending 2000 below. This means no matter how far you over-scroll into the stretch area, you still see the tiled background.
class MyViewController: NSViewController {
#IBOutlet weak var outlineView: NSOutlineView!
override func viewDidLoad() {
super.viewDidLoad()
guard let clipView = self.outlineView.superview else { return }
let newView = MyView(frame: .zero) // Frame is set by autolayout below.
newView.translatesAutoresizingMaskIntoConstraints = false
clipView.addSubview(newView, positioned: .below, relativeTo: self.outlineView)
// Add autolayout constraints to pin the new view to the clipView.
// See https://apple.co/3c6EMcH
newView.leadingAnchor.constraint(equalTo: clipView.leadingAnchor).isActive = true
newView.widthAnchor.constraint(equalTo: clipView.widthAnchor).isActive = true
newView.topAnchor.constraint(equalTo: clipView.topAnchor, constant: -2000).isActive = true
newView.bottomAnchor.constraint(equalTo: clipView.bottomAnchor, constant: 2000).isActive = true
}
}
I've removed other code from the above so hopefully I've left everything needed to illustrate the solution.

View is not autoresizing to meet constraints

I have a hard time wrapping my head around this problem. I have included a simple example where I set constraints to be wrapping the device. However, the view frame is not resizing based on different devices.
Iphone XR -> See how frame is different than holderView
https://imgur.com/ZyoCNGQ
Iphone 8 -> Frame is same size as holderView
https://imgur.com/S6xnzcZ
Ideally the holder height and width should change with different devices, however, that is not happening.
Your layout seems to look fine.
If they look fine in the simulator, they are fine.
I suspect that you are printing your frames before the layout gets properly set.
Try moving the debugging print statements to viewDidLayoutSubviews or viewDidAppear, from viewDidLoad.
Also, if you want to wrap the entire device, you need to set constraints around the superView, not safeAreaLayoutGuide.
Although I've done it programmatically, the following is basically the layout you have:
class ViewController: UIViewController {
var holderView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
holderView = UIView()
holderView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(holderView)
NSLayoutConstraint.activate([
holderView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
holderView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
holderView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
holderView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
])
holderView.backgroundColor = .orange
print(holderView.frame)
print(view.frame)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print(holderView.frame)
print(view.frame)
}
}
In iPhone XR,
(0.0, 0.0, 0.0, 0.0)
(0.0, 0.0, 414.0, 896.0)
(0.0, 44.0, 414.0, 818.0)
(0.0, 0.0, 414.0, 896.0)
is printed. You can see that holderView's frame is (0,0,0,0). But it gets eventually set. Since you used a storyboard, I think the initial frame size is set to the canvas frame size.

Swift text field border isn't the right width

I have a bottom border that I generated after following the answer here.
This works absolutely great except the border isn't the right width. It's set with constraints to match the width of the button below it but as you can see is coming up short.
What am I missing?
Code :
extension UITextField
{
func setBottomBorder(withColor color: UIColor)
{
self.borderStyle = UITextBorderStyle.none
self.backgroundColor = UIColor.clear
let width: CGFloat = 3.0
let borderLine = UIView(frame: CGRect(x: 0, y: self.frame.height - width, width: self.frame.width, height: width))
borderLine.backgroundColor = color
self.addSubview(borderLine)
}
}
then in the VC :
override func viewDidLoad() {
authorNameOutlet.setBottomBorder(withColor: UIColor.lightGray)
}
Then Xcode shows...
but the simulator shows...
I've tried this both setting the width of the text field to be 0.7 x the superview width (same as the button below it) and also setting the width of the text field to be equal width of the button but neither works.
This is because of AutoLayout.
You can add autoresizingMask to your line.
borderLine.autoresizingMask = [.flexibleWidth, .flexibleTopMargin]
You are working with with a static frame for the border line view. After viewDidLoad your view controller's view gets resized.
Option 1: (Fast and dirty)
Move your code from viewDidLoad() to viewWillAppear(_ animated: Bool). viewWillAppear gets called after the first layout of your view controller's view
Option 2:
Add constraint for your border line view. So that your border line view will resize automatically.
Importent hint:
Do not forget super calls in overrides or you will get strange bugs!
E.g:
override func viewDidLoad() {
super.viewDidLoad()
// your code
}