Right anchor of UIScrollView does not apply - ios

I have a UIViewController? SingleEventController displaying Events. Because it has dynamic information to show, I chose to create a new class UIScrollView EventScrollView. I tried to realize that based on this very well explained
Answer.
I tried to apply the Pure Auto Layout Approach: Create another UIView contentView in it, which contains all the information and is anchored to all four anchors of the scrollView.
The contentSize of the scrollView is determined in the viewDidLayoutSubviews function in the SingleEventController.
My struggle is, that the rightAnchor seems to be faulty. All the information-Elements go over the border of the view, the word-wrapping for the larger UILabels does not work, and every Item layed out at the right anchor is gone. (For example the usernames in the UITableView nopePeopleTV which is a subview of the contentView.)
This looks like this:
Code in the SingleEventController:
view.addSubview(scrollView)
scrollView.anchor(top: view.topAnchor, left: view.leftAnchor, bottom: buttonViewDividerView.topAnchor, right: view.rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
override func viewDidLayoutSubviews() {
let heightOfAllObjects = scrollView.calculateHeightOfAllObjects()
scrollView.contentSize = CGSize(width: self.view.frame.width, height: heightOfAllObjects + scrollView.heightOfAllPaddings)
}
Code in EventScrollView:
addSubview(contentView)
contentView.addSubview(titleLabel)
contentView.addSubview(locationLabel)
...
contentView.addSubview(nopePeopleTV)
contentView.anchor(top: topAnchor, left: leftAnchor, bottom: bottomAnchor, right: rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
titleLabel.anchor(top: contentView.topAnchor, left: contentView.leftAnchor, bottom: nil, right: contentView.rightAnchor, paddingTop: 10, paddingLeft: padding, paddingBottom: 0, paddingRight: padding, width: 0, height: 0)
locationLabel.anchor(top: titleLabel.bottomAnchor, left: contentView.leftAnchor, bottom: nil, right: nil, paddingTop: 0, paddingLeft: padding, paddingBottom: 0, paddingRight: 0, width: 200, height: 0)
nopePeopleTV.anchor(top: maybePeopleTV.bottomAnchor, left: contentView.leftAnchor, bottom: nil, right: contentView.rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
layoutIfNeeded()
contentView.layoutIfNeeded()
What am I doing wrong?
My anchor function looks like this:
func anchor(top: NSLayoutYAxisAnchor?, left: NSLayoutXAxisAnchor?, bottom: NSLayoutYAxisAnchor?, right: NSLayoutXAxisAnchor?, paddingTop: CGFloat, paddingLeft: CGFloat, paddingBottom: CGFloat, paddingRight: CGFloat, width: CGFloat, height: CGFloat) {
translatesAutoresizingMaskIntoConstraints = false
if let top = top {
topAnchor.constraint(equalTo: top, constant: paddingTop).isActive = true
}
if let left = left {
leftAnchor.constraint(equalTo: left, constant: paddingLeft).isActive = true
}
if let bottom = bottom {
bottomAnchor.constraint(equalTo: bottom, constant: -paddingBottom).isActive = true
}
if let right = right {
rightAnchor.constraint(equalTo: right, constant: -paddingRight).isActive = true
}
if width != 0 {
widthAnchor.constraint(equalToConstant: width).isActive = true
}
if height != 0 {
heightAnchor.constraint(equalToConstant: height).isActive = true
}
}

The problem is that you are letting the content view width be dictated from the inside out by the width of the subviews. You need to give the content view a width constraint matching the width of the scroll view itself.
I'll demonstrate the difference with an artificial but complete code-only example:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let sv = UIScrollView()
sv.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(sv)
NSLayoutConstraint.activate([
sv.topAnchor.constraint(equalTo: self.view.topAnchor),
sv.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
sv.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
sv.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
])
let cv = UIView() // content view
cv.translatesAutoresizingMaskIntoConstraints = false
sv.addSubview(cv)
NSLayoutConstraint.activate([
sv.topAnchor.constraint(equalTo: cv.topAnchor),
sv.leadingAnchor.constraint(equalTo: cv.leadingAnchor),
sv.trailingAnchor.constraint(equalTo: cv.trailingAnchor),
sv.bottomAnchor.constraint(equalTo: cv.bottomAnchor),
])
let lab = UILabel()
lab.translatesAutoresizingMaskIntoConstraints = false
lab.numberOfLines = 0
lab.text = Array(repeating: "xxx", count: 100).joined(separator: " ")
cv.addSubview(lab)
NSLayoutConstraint.activate([
lab.topAnchor.constraint(equalTo: cv.topAnchor, constant:30),
lab.leadingAnchor.constraint(equalTo: cv.leadingAnchor),
lab.trailingAnchor.constraint(equalTo: cv.trailingAnchor),
])
}
}
Here's the result:
We have a very long label, but it doesn't wrap: it keeps extending to the right, off the screen. That's because the content view itself has its right edge off the screen. That's because we have done nothing to prevent this from happening. The long label is itself making the content view as wide as the label's entire text.
Now I add one final line of code to viewDidLoad:
cv.widthAnchor.constraint(
equalTo:sv.frameLayoutGuide.widthAnchor).isActive = true
The result is this:
The content view is now the width of the scroll view, so the label wraps within the scroll view.
We do not, however, do the same thing to the height of the content view; we want it to grow vertically in accordance with its subviews. And that way, we will be able to scroll vertically (which is what you want) but not horizontally (which is also what you want).

Related

Large Title UINavigationBar not collapsing with UITableView scroll

I have a large title navigation bar for my view controller that is supposed to animate and collapse when the user scrolls down. However, the navigation bar stays large and never animates down.
Below is the intended outcome (not my view controller), however my navigation bar doesn't collapse.
Here is my current view setup. From some research, I believe the issue is because the UITableView has to be the first subview for the animation to happen. I haven't found a solution for any way around this.
override func viewDidLoad() {
super.viewDidLoad()
let bottomPadding: CGFloat = 12.5
view.addSubview(tableView)
tableView.anchor(top: view.topAnchor, left: view.leftAnchor, bottom: view.bottomAnchor, right: view.rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
setupTableView()
view.insertSubview(animationView, belowSubview: tableView)
animationView.anchor(top: view.topAnchor, left: view.leftAnchor, bottom: view.safeAreaLayoutGuide.bottomAnchor, right: view.rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: bottomPadding, paddingRight: 0, width: 0, height: 0)
animationView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
animationView.layer.cornerRadius = bottomPadding*2
animationView.clipsToBounds = true
animationView.layer.masksToBounds = true
}

UIScrollView not scrolling - ambiguous content size

I am trying to create a Scroll View. The view itself is correctly
displayed but it does not scroll, even though I added a view to it and
set the content size.
NOTE: I am not using storyboards but Constraints (AutoLayout) only.
Code:
let scrollView: UIScrollView = {
let sv = UIScrollView()
sv.isScrollEnabled = true
sv.backgroundColor = .brown
return sv
}()
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
}
func setupViews() {
self.view.addSubview(scrollView)
scrollView.anchor(top: self.view.topAnchor, left: self.view.leftAnchor, bottom: self.view.bottomAnchor, right: self.view.rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
scrollView.contentSize = CGSize(width: self.view.frame.width, height: 2000)
print(scrollView.contentSize)
let view = UIView()
view.backgroundColor = .blue
scrollView.addSubview(view)
view.anchor(top: scrollView.topAnchor, left: scrollView.leftAnchor, bottom: scrollView.bottomAnchor, right: scrollView.rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
}
.anchor() is a custom extension I made in order to make constraining views easier.
Any help would be appreciated.
It's better not to mix autolayout with frame layout do
1- Comment
scrollView.contentSize = CGSize(width: self.view.frame.width, height: 2000)
2- set equal width constraint between the view inside the scrollview to the vc's view and a height constraint of 2000
For no confuse with vc's view change
let view = UIView()
to
let contentView = UIView()
then add these 2 constarints
contentView.widthAnchor.constraint(equalTo:self.view.widthAnchor).isActive = true
contentView.heightAnchor.constraint(equalToConstant:2000).isActive = true

Make one view bigger than others inside UIStackView()

I have a UIStackView which consist of 5 elements. I want the centered one bigger than others (like in below pic).
How I create UIStackView()
stackView.axis = UILayoutConstraintAxis.horizontal
stackView.distribution = UIStackViewDistribution.fillEqually
stackView.alignment = UIStackViewAlignment.bottom
stackView.spacing = 0.0
stackView.addArrangedSubview(supportedServicesView)
stackView.addArrangedSubview(incidentView)
stackView.addArrangedSubview(contactUsView)
stackView.addArrangedSubview(moreView)
stackView.addArrangedSubview(moreView2)
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)
stackView.anchor(nil, left: self.view.leftAnchor, bottom: self.view.bottomAnchor, right: self.view.rightAnchor, topConstant: 0, leftConstant: 0, bottomConstant: 90, rightConstant: 0, widthConstant: 0, heightConstant: 0)
How I create my custom UIViews subview positions;
override func updateConstraints() {
logoImage.anchor(self.topAnchor, left: self.leftAnchor, bottom: nil, right: self.rightAnchor, topConstant: 0, leftConstant: 0, bottomConstant: 0, rightConstant: 0, widthConstant: 0, heightConstant: 0)
label.anchor(self.logoImage.bottomAnchor, left: self.leftAnchor, bottom: nil, right: self.rightAnchor, topConstant: 10, leftConstant: 0, bottomConstant: 0, rightConstant: 0, widthConstant: 0, heightConstant: 0)
super.updateConstraints()
}
EDIT:
when I add width anchor to centered view, It gets higher width but because heights are same, It doesn't look like bigger.
contactUsView.widthAnchor.constraint(equalToConstant: self.view.frame.width / 5).isActive = true
EDIT 2: When I give height constraint to any view inside UIStackView, Stackviews position (height only) changes to value which I gave to Views height anchor.
I have just implemented this example in a playground.
The UIStackView uses the intrinsic content size to calculate how to place its arranged subviews in the stack view taking the axis, distribution, spacing etc into consideration.
So if you add both a height and width constraint you should see it working. See the example and screenshot of output below.
//: Playground - noun: a place where people can play
import UIKit
import PlaygroundSupport
let stackview = UIStackView(frame: CGRect(x: 0, y: 0, width: 500, height: 150))
stackview.backgroundColor = .white
let colours: [UIColor] = [
.blue,
.green,
.red,
.yellow,
.orange
]
for i in 0...4 {
let view = UIView(frame: CGRect.zero)
view.backgroundColor = colours[i]
view.translatesAutoresizingMaskIntoConstraints = false
if i == 2 {
view.heightAnchor.constraint(equalToConstant: 130).isActive = true
} else {
view.heightAnchor.constraint(equalToConstant: 80).isActive = true
}
view.widthAnchor.constraint(equalToConstant: 75)
stackview.addArrangedSubview(view)
}
stackview.axis = .horizontal
stackview.distribution = .fillEqually
stackview.alignment = .bottom
stackview.spacing = 0.5
PlaygroundPage.current.liveView = stackview
You can drop this code straight into a playground and tweak the settings for spacing, distribution etc to get the desired output.

UIView wont appear

I have a function in my viewController that lays out my views using SnapKit. The funcion looks like this
#objc func setupView(){
//will add the scroll view to the subview
view.addSubview(scrollView)
scrollView.addSubview(imageContainer)
scrollView.addSubview(eventInfoVIew)
// scrollView.addSubview(currentEventImage)
scrollView.contentInsetAdjustmentBehavior = .never
scrollView.backgroundColor = .green
eventInfoVIew.backgroundColor = .red
imageContainer.backgroundColor = .blue
scrollView.anchor(top: view.topAnchor, left: view.leftAnchor, bottom: view.bottomAnchor, right: view.rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
imageContainer.anchor(top: scrollView.topAnchor, left: view.leftAnchor, bottom: nil, right: view.rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: view.frame.width, height: view.frame.height - 200)
eventInfoVIew.anchor(top: imageContainer.bottomAnchor, left: view.leftAnchor, bottom: scrollView.bottomAnchor, right: view.rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
scrollView.scrollIndicatorInsets = view.safeAreaInsets
scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: view.safeAreaInsets.bottom, right: 0)
}
However when I run the app one of the views isn't there and it makes no sense because to the best of my knowledge I laid out the constraints properly. The eventInfoView is the one giving me problems.
Sorry forgot to add image of current screen
Have you try debugging with the view inspector? What happens with your views? A screenshot of the view hierarchy would help a lot.
Without this, my hunch is that you are not using enough constraints and therefore your layout is ambiguous. Don't forget that UIScrollView requires 6 constraints to layout properly instead of 4.
Edit: If you want to check if the scrollView is the issue, try adding this code inside
scrollView.snp.makeConstraints { (make) in
make.edges.equalTo(view)
make.width.height.equalTo(view)
}

Adaptive Layout with Trait Collection in Swift iOS

I'm trying to layout a UIView on screen to be at the top at an aspect ratio of 16:9 when device is in portrait mode, and to be full screen when device is in landscape mode (like a video player screen). I decided to use size classes. Here is how i was able to achieve it but i'm not satisfied with the the implementation and there are some bugs. Here is my code:
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
if traitCollection.verticalSizeClass == .compact {
localPlayerView.anchor(top: view.topAnchor, left: view.leftAnchor, bottom: view.bottomAnchor, right: view.rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
}
}
override func viewWillLayoutSubviews() {
if traitCollection.verticalSizeClass == .compact {
localPlayerView.anchor(top: view.topAnchor, left: view.leftAnchor, bottom: view.bottomAnchor, right: view.rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
}
if traitCollection.horizontalSizeClass == .compact {
localPlayerView.removeFromSuperview()
view.addSubview(localPlayerView)
let height = view.frame.width * 9 / 16
localPlayerView.anchor(top: view.topAnchor, left: view.leftAnchor, bottom: nil, right: view.rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: height)
collectionView.anchor(top: localPlayerView.bottomAnchor, left: view.leftAnchor, bottom: view.bottomAnchor, right: view.rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
localPlayerView.layoutIfNeeded()
}
}
I had to remove the localPlayerView from superview and add it back to get the desired result because, it was still constraining to full superview width when i switch back to portrait.
How can I improve this code to achieve the same result?
Everytime you give the localPlayerView new constraints defining its size and position, you have to remove/deactivate the old constraints. In your code it seems that you are always just adding new constraints, which will result in unsatisfiable constraints and then problems in layout - morever, if you remove the player from the superview and then add it again, that will remove those constraints - that only confirms my hypothesis. You have to be careful to always have non-conflicting constraints.
Just a broad guidelines as how you can do it. Define two properties in the viewController:
var portraitConstraints: [NSLayoutConstraint] = []
var landscapeConstraints: [NSLayoutConstraint] = []
Then, once the view is loaded (e.g., in viewDidLoad) populate them accordingly:
portraitConstraints = [
// here create all the constraints that are needed to properly define position and size of the player when in portrait
]
landscapeConstraints = [
// all the constraints for landscape
]
NSLayoutConstraint.activate(portraitConstraints) // activate these as default
Then in the traitCollectionDidChange detect if you are in landscape or in portrait (I omit this code, you can find SO questions about that), and just activate the proper constraints (and deactivate old ones):
let inLandscape = // code to detect current orientation
if inLandscape {
NSLayoutConstraint.deactivate(portraitConstraints)
NSLayoutConstraint.activate(landscapeConstraints)
} else {
NSLayoutConstraint.deactivate(landscapeConstraints)
NSLayoutConstraint.activate(portraitConstraints)
}

Resources