Adaptive Layout with Trait Collection in Swift iOS - 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)
}

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
}

Right anchor of UIScrollView does not apply

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).

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)
}

Constraint Issue

So I have a cell that is setup to have a collectionView inside of it. The height for that cell is given in this function.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
if indexPath.section == 0 {
return CGSize(width: view.frame.width, height: 300)
}
return CGSize(width: view.frame.width, height: 280)
}
Inside the class for this cell I create a collectionView and again create a collection of cells inside of it.
#objc func setupViews(){
backgroundColor = .red
addSubview(categoryCollectionView)
addSubview(sectionNameLabel)
sectionNameLabel.anchor(top: topAnchor, left: leftAnchor, bottom: nil, right: nil, paddingTop: 0, paddingLeft: 14, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
categoryCollectionView.anchor(top: sectionNameLabel.bottomAnchor, left: leftAnchor, bottom: bottomAnchor, right: rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
categoryCollectionView.delegate = self
categoryCollectionView.dataSource = self
categoryCollectionView.showsHorizontalScrollIndicator = false
categoryCollectionView.register(CategoryEventCell.self, forCellWithReuseIdentifier: cellID)
}
Each cell has a background image and a title that is below the image. However the text is always somewhat cut off. My current constraints are here
#objc func setupViews(){
backgroundColor = .clear
setCellShadow()
addSubview(backgroundImageView)
addSubview(eventNameLabel)
backgroundImageView.anchor(top: topAnchor, left: leftAnchor, bottom: bottomAnchor, right: rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 9, paddingRight: 0, width: 0, height: 0)
eventNameLabel.anchor(top: backgroundImageView.bottomAnchor, left: leftAnchor, bottom: nil, right: nil, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
}
How would I change my constraints or text/element size to make sure everything fits?
My current view looks like so
You are pinning the image to the bottom of its cell, and the label has no say in how to fit in. Instead you should pin the image's to the label's top, and the label's bottom to the cell's bottom. Like this in the visual format language: [image]-[label]-|. You also need to edit the backgroundImageView's content compression resistance.
Here is the fixed version where the label causes the image's height to shrink upwards to fit in:
#objc func setupViews(){
backgroundColor = .clear
setCellShadow()
addSubview(backgroundImageView)
addSubview(eventNameLabel)
// ensure the image compresses, not the eventNameLabel, by lowering its priority
backgroundImageView.setContentCompressionResistancePriority(UILayoutPriority(600), for: .vertical)
backgroundImageView.anchor(top: topAnchor, left: leftAnchor, bottom: eventNameLabel.topAnchor, right: rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
eventNameLabel.anchor(top: backgroundImageView.bottomAnchor, left: leftAnchor, bottom: bottomAnchor, right: nil, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
}

Add UIView behind iPhone X status bar

I have an App, that doesn't have a UINavigationController and it has a UICollectionView with some cells as its feed.
To avoid the cells appearing behind the status bar, I added a UIView behind it.
Before iPhone X it was pretty simple to do this, as the following:
let v = UIView()
v.backgroundColor = .white
view.addSubview(v)
v.anchor(top: view.topAnchor, left: view.leftAnchor, bottom: nil, right: view.rightAnchor, paddingTop: 0, paddinfLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 20)
By doing so, a tiny white layer would appear behind it as expected.
(this anchor method is from an extension, not language-default).
How can I achieve this for iPhone X ?
So far I tried the following code:
if #available(iOS 11, *) {
v.anchor(top: view.safeAreaLayoutGuide.topAnchor, left: view.safeAreaLayoutGuide.leftAnchor, bottom: nil, right: view.safeAreaLayoutGuide.rightAnchor, paddingTop: 0, paddinfLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 20)
let window = UIWindow(frame: UIScreen.main.bounds)
print("window.safeAreaInsets.top:", window.safeAreaInsets.top)
if window.safeAreaInsets.top > CGFloat(0.0) {
print("iPhone X")
v.anchor(top: view.safeAreaLayoutGuide.topAnchor, left: view.safeAreaLayoutGuide.leftAnchor, bottom: nil, right: view.safeAreaLayoutGuide.rightAnchor, paddingTop:-44, paddinfLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: window.safeAreaInsets.top)
} else {
v.anchor(top: view.topAnchor, left: view.leftAnchor, bottom: nil, right: view.rightAnchor, paddingTop: 0, paddinfLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 20)
}
}
but it doesn't work.
What's the best approach in this case?
Thank you
You have to tell your collectionView to adjust the insets:
if #available(iOS 11, *) {
collectionView.contentInsetAdjustmentBehavior = .scrollableAxes
}
This will make sure your content will be inset enough so it's not overlapped by the iPhone X's status bar.

Resources