Animating UICollectionView With Auto Constraints Swift - ios

I have a UICollectionView inside an inputAccessoryView for selecting images while creating a post (similar to Twitter).
When the user starts typing I want to animate the UICollectionView down with a UIView animation function.
Preferred Outcome
Code
func animateCollectionView() {
UIView.animate(withDuration: 1, delay: 0, options: .showHideTransitionViews) {
self.collectionView.transform = .init(scaleX: 0, y: 100)
} completion: { finished in
if finished {
print("ANIMATION COMPLETED")
}
}
}
With this, the UICollectionView gets removed immediately and the console is printing after 1 second (as expected). However, the animation is not happening.
Constraints
NSLayoutConstraint.activate([
uploadVoiceNoteButton.heightAnchor.constraint(equalToConstant: 48),
uploadMediaButton.heightAnchor.constraint(equalToConstant: 48),
uploadPollButton.heightAnchor.constraint(equalToConstant: 48),
characterCountView.heightAnchor.constraint(equalToConstant: 48),
characterCountView.widthAnchor.constraint(equalToConstant: 18),
hStackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
hStackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
hStackView.bottomAnchor.constraint(equalTo: bottomAnchor),
separator.heightAnchor.constraint(equalToConstant: Height.separator),
separator.leadingAnchor.constraint(equalTo: leadingAnchor),
separator.trailingAnchor.constraint(equalTo: trailingAnchor),
separator.topAnchor.constraint(equalTo: hStackView.topAnchor),
replyAllowanceButton.heightAnchor.constraint(equalToConstant: 51),
replyAllowanceButton.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
replyAllowanceButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
replyAllowanceButton.bottomAnchor.constraint(equalTo: separator.topAnchor),
collectionViewSeparator.bottomAnchor.constraint(equalTo: replyAllowanceButton.topAnchor),
collectionViewSeparator.leadingAnchor.constraint(equalTo: leadingAnchor),
collectionViewSeparator.trailingAnchor.constraint(equalTo: trailingAnchor),
collectionViewSeparator.heightAnchor.constraint(equalToConstant: Height.separator),
collectionView.topAnchor.constraint(equalTo: topAnchor),
collectionView.leadingAnchor.constraint(equalTo: leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: trailingAnchor),
collectionView.bottomAnchor.constraint(equalTo: collectionViewSeparator.topAnchor, constant: -8),
])

I would suggest embedding the collection view and the separator view in a "container" UIView.
Give the collection view Leading/Trailing (to the container view) and Height constraints, but no Bottom constraint (and no Top constraint yet).
Give the separator view Leading/Trailing (to the container view), Top to the collection view Bottom, and Height constraints, but no Bottom constraint.
Give the container view a height constraint so collection view and separator view (and maybe a little spacing) will fit.
Add "visible" and "hidden" constraints as var properties:
var cvVisibleConstraint: NSLayoutConstraint!
var cvHiddenConstraint: NSLayoutConstraint!
then, when we're setting all the other constraints, create those two like this:
// collectionView TOP constrained to TOP of container when visible
cvVisibleConstraint = collectionView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 8.0)
// collectionView TOP constrained to BOTTOM of container when hidden
cvHiddenConstraint = collectionView.topAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 8.0)
To show/hide the collection view (and the separator, because it's constrained to the collection view):
containerView.clipsToBounds = true
and:
cvVisibleConstraint.isActive.toggle()
cvHiddenConstraint.isActive = !cvVisibleConstraint.isActive
UIView.animate(withDuration: 0.5, animations: {
self.view.layoutIfNeeded()
})
Here's an example... I'm adding a view to the main view to simulate the input accessory view, but the approach is the same:
class ShowHideVC: UIViewController {
var collectionView: UICollectionView!
let collectionViewSeparator = UIView()
let containerView = UIView()
let myInputAccessoryView = UIView()
var cvVisibleConstraint: NSLayoutConstraint!
var cvHiddenConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
let fl = UICollectionViewFlowLayout()
fl.scrollDirection = .horizontal
fl.itemSize = CGSize(width: 72.0, height: 72.0)
collectionView = UICollectionView(frame: .zero, collectionViewLayout: fl)
[myInputAccessoryView, containerView, collectionViewSeparator, collectionView].forEach { v in
v?.translatesAutoresizingMaskIntoConstraints = false
}
containerView.addSubview(collectionView)
containerView.addSubview(collectionViewSeparator)
myInputAccessoryView.addSubview(containerView)
view.addSubview(myInputAccessoryView)
let g = view.safeAreaLayoutGuide
// collectionView TOP constrained to TOP of container when visible
cvVisibleConstraint = collectionView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 8.0)
// collectionView TOP constrained to BOTTOM of container when hidden
cvHiddenConstraint = collectionView.topAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 8.0)
// setting priorities to (999) avoids auto-layout complaints when toggling active
cvVisibleConstraint.priority = .required - 1
cvHiddenConstraint.priority = .required - 1
NSLayoutConstraint.activate([
myInputAccessoryView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
myInputAccessoryView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0),
myInputAccessoryView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0),
myInputAccessoryView.heightAnchor.constraint(equalToConstant: 200.0),
containerView.topAnchor.constraint(equalTo: myInputAccessoryView.topAnchor, constant: 8.0),
containerView.leadingAnchor.constraint(equalTo: myInputAccessoryView.leadingAnchor, constant: 8.0),
containerView.trailingAnchor.constraint(equalTo: myInputAccessoryView.trailingAnchor, constant: -8.0),
containerView.heightAnchor.constraint(equalToConstant: 100.0),
// start with collection view showing
cvVisibleConstraint,
collectionView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 8.0),
collectionView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -8.0),
collectionView.heightAnchor.constraint(equalToConstant: 78.0),
collectionViewSeparator.topAnchor.constraint(equalTo: collectionView.bottomAnchor, constant: 8.0),
collectionViewSeparator.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 8.0),
collectionViewSeparator.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -8.0),
collectionViewSeparator.heightAnchor.constraint(equalToConstant: 1.0),
// no bottom constraints for collectionView or collectionViewSeparator
])
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "c")
collectionView.dataSource = self
collectionView.delegate = self
// colors so we can see framing
collectionView.backgroundColor = .systemGreen
collectionViewSeparator.backgroundColor = .red
containerView.backgroundColor = .yellow
myInputAccessoryView.backgroundColor = .systemYellow
// comment / un-comment the next line to see what's really going on
containerView.clipsToBounds = true
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
cvVisibleConstraint.isActive.toggle()
cvHiddenConstraint.isActive = !cvVisibleConstraint.isActive
UIView.animate(withDuration: 0.5, animations: {
self.view.layoutIfNeeded()
})
}
}
extension ShowHideVC: UICollectionViewDataSource, UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 10
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let c = collectionView.dequeueReusableCell(withReuseIdentifier: "c", for: indexPath)
c.contentView.backgroundColor = .green
c.contentView.layer.cornerRadius = 8
return c
}
}
It will toggle between showing and hidden on any tap on the screen (animated) and look like this:
Note the last line in viewDidLoad():
// comment / un-comment the next line to see what's really going on
containerView.clipsToBounds = true

Related

ios Swift: ScrollView with dynamic content programmatic layout

Need to create custom view, just 2 buttons and some content between. Problem is about create correct layout using scrollView and subviews with dynamic content.
For example, if there will be only one Label.
What is my mistake?
Now label isn't visible, and view looks like:
Here is code:
view inits this way:
let view = MyView(frame: .zero)
view.configure(with ...) //here configures label text
selv.view.addSubView(view)
public final class MyView: UIView {
private(set) var titleLabel: UILabel?
override public init(frame: CGRect) {
let closeButton = UIButton(type: .system)
closeButton.translatesAutoresizingMaskIntoConstraints = false
(button setup)
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.showsVerticalScrollIndicator = false
scrollView.alwaysBounceVertical = false
let contentLayoutGuide = scrollView.contentLayoutGuide
let titleLabel = UILabel()
titleLabel.translatesAutoresizingMaskIntoConstraints = false
(label's font and alignment setup)
let successButton = UIButton(type: .system)
successButton.translatesAutoresizingMaskIntoConstraints = false
(button setup)
super.init(frame: frame)
addSubview(closeButton)
addSubview(scrollView)
addSubview(successButton)
scrollView.addSubview(titleLabel)
self.textLabel = textLabel
let layoutGuide = UILayoutGuide()
addLayoutGuide(layoutGuide)
NSLayoutConstraint.activate([
layoutGuide.leadingAnchor.constraint(equalToSystemSpacingAfter: leadingAnchor, multiplier: 2),
trailingAnchor.constraint(equalToSystemSpacingAfter: layoutGuide.trailingAnchor, multiplier: 2),
layoutGuide.topAnchor.constraint(equalToSystemSpacingBelow: topAnchor, multiplier: 2),
bottomAnchor.constraint(equalToSystemSpacingBelow: layoutGuide.bottomAnchor, multiplier: 2),
closeButton.leadingAnchor.constraint(greaterThanOrEqualTo: layoutGuide.leadingAnchor),
layoutGuide.trailingAnchor.constraint(greaterThanOrEqualTo: closeButton.trailingAnchor),
closeButton.centerXAnchor.constraint(equalTo: layoutGuide.centerXAnchor),
closeButton.topAnchor.constraint(equalTo: layoutGuide.topAnchor),
closeButton.heightAnchor.constraint(equalToConstant: 33),
scrollView.topAnchor.constraint(equalTo: closeButton.bottomAnchor),
scrollView.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: layoutGuide.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: successButton.topAnchor),
scrollView.contentLayoutGuide.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor),
scrollView.contentLayoutGuide.trailingAnchor.constraint(equalTo: layoutGuide.trailingAnchor),
successButton.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor),
layoutGuide.trailingAnchor.constraint(equalTo: successButton.trailingAnchor),
successButton.heightAnchor.constraint(equalToConstant: 48),
layoutGuide.bottomAnchor.constraint(equalTo: successButton.bottomAnchor),
titleLabel.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 16),
titleLabel.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 16),
titleLabel.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: -16),
titleLabel.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: -16),
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func configure(with viewModel: someViewModel) {
titleLabel?.text = viewModel.title
}
}
If I'll add scrollView frameLayoutGuide height:
scrollView.frameLayoutGuide.heightAnchor.constraint(equalToConstant: 150),
, then all looks as expected, but I need to resize this label and all MyView height depending on content.
A UIScrollView is designed to automatically allow scrolling when its content is larger than its frame.
By itself, a scroll view has NO intrinsic size. It doesn't matter how many subviews you add to it... if you don't do something to set its frame, its frame size will always be .zero.
If we want to get the scroll view's frame to grow in height based on its content we need to give it a height constraint when the content size changes.
If we want it to scroll when it has a lot of content, we also need to give it a maximum height.
So, if we want MyView height to be max of 1/2 the screen (view) height, we constrain its height (in the controller) like this:
myView.heightAnchor.constraint(lessThanOrEqualTo: view.safeAreaLayoutGuide.heightAnchor, multiplier: 0.5)
and then constrain the scroll view height in MyView like this:
let svh = scrollView.heightAnchor.constraint(equalToConstant: scrollView.contentSize.height)
svh.priority = .required - 1
svh.isActive = true
Here is a modification to your code - lots of comments in the code so you should be able to follow.
First, an example controller:
class MVTestVC: UIViewController {
let myView = MyView()
let sampleStrings: [String] = [
"Short string.",
"This is a longer string which should wrap onto a couple lines.",
"Now let's use a really, really long string. This will make the label taller, but still not enough to require vertical scrolling.",
"We want to see what happens when we DO need scrolling.\n\nSo, let's use a long string, with some embedded newlines.\n\nThis will make the label tall enough that it would exceed one-half the screen height, so we can see that we do, in fact, get vertical scrolling.",
]
var strIndex: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .gray
myView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(myView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// 20-points on each side
myView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
myView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
// centered vertically
myView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
// max 1/2 screen (view) height
myView.heightAnchor.constraint(lessThanOrEqualTo: g.heightAnchor, multiplier: 0.5),
])
myView.backgroundColor = .white
myView.configure(with: sampleStrings[0])
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
strIndex += 1
myView.configure(with: sampleStrings[strIndex % sampleStrings.count])
}
}
and the modified MyView class:
public final class MyView: UIView {
private let titleLabel = UILabel()
private let scrollView = UIScrollView()
// this will be used to set the scroll view height
private var svh: NSLayoutConstraint!
override public init(frame: CGRect) {
super.init(frame: frame)
let closeButton = UIButton(type: .system)
closeButton.translatesAutoresizingMaskIntoConstraints = false
//(button setup)
closeButton.setTitle("X", for: [])
closeButton.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.showsVerticalScrollIndicator = false
scrollView.alwaysBounceVertical = false
titleLabel.translatesAutoresizingMaskIntoConstraints = false
//(label's font and alignment setup)
titleLabel.font = .systemFont(ofSize: 24.0, weight: .light)
titleLabel.numberOfLines = 0
let successButton = UIButton(type: .system)
successButton.translatesAutoresizingMaskIntoConstraints = false
//(button setup)
successButton.setTitle("Success", for: [])
successButton.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
addSubview(closeButton)
addSubview(scrollView)
addSubview(successButton)
scrollView.addSubview(titleLabel)
let layoutGuide = UILayoutGuide()
addLayoutGuide(layoutGuide)
let contentLayoutGuide = scrollView.contentLayoutGuide
NSLayoutConstraint.activate([
layoutGuide.leadingAnchor.constraint(equalToSystemSpacingAfter: leadingAnchor, multiplier: 2),
trailingAnchor.constraint(equalToSystemSpacingAfter: layoutGuide.trailingAnchor, multiplier: 2),
layoutGuide.topAnchor.constraint(equalToSystemSpacingBelow: topAnchor, multiplier: 2),
bottomAnchor.constraint(equalToSystemSpacingBelow: layoutGuide.bottomAnchor, multiplier: 2),
closeButton.leadingAnchor.constraint(greaterThanOrEqualTo: layoutGuide.leadingAnchor),
layoutGuide.trailingAnchor.constraint(greaterThanOrEqualTo: closeButton.trailingAnchor),
closeButton.centerXAnchor.constraint(equalTo: layoutGuide.centerXAnchor),
closeButton.topAnchor.constraint(equalTo: layoutGuide.topAnchor),
closeButton.heightAnchor.constraint(equalToConstant: 33),
scrollView.topAnchor.constraint(equalTo: closeButton.bottomAnchor),
scrollView.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: layoutGuide.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: successButton.topAnchor),
successButton.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor),
layoutGuide.trailingAnchor.constraint(equalTo: successButton.trailingAnchor),
successButton.heightAnchor.constraint(equalToConstant: 48),
layoutGuide.bottomAnchor.constraint(equalTo: successButton.bottomAnchor),
// constrain the label to the scroll view's Content Layout Guide
titleLabel.topAnchor.constraint(equalTo: contentLayoutGuide.topAnchor, constant: 16),
titleLabel.leadingAnchor.constraint(equalTo: contentLayoutGuide.leadingAnchor, constant: 16),
titleLabel.trailingAnchor.constraint(equalTo: contentLayoutGuide.trailingAnchor, constant: -16),
titleLabel.bottomAnchor.constraint(equalTo: contentLayoutGuide.bottomAnchor, constant: -16),
// label needs a width anchor, otherwise we'll get horizontal scrolling
titleLabel.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: -32),
])
layer.cornerRadius = 12
// so we can see the framing
scrollView.backgroundColor = .red
titleLabel.backgroundColor = .green
}
public override func layoutSubviews() {
super.layoutSubviews()
// we want to update the scroll view's height constraint when the text changes
if let c = svh {
c.isActive = false
}
// on initial layout, the scroll view's content size will still be zero
// so force another layout pass
if scrollView.contentSize.height == 0 {
scrollView.setNeedsLayout()
scrollView.layoutIfNeeded()
}
// constrain the scroll view's height to the height of its content
// but with a less-than-required priority so we can use a maximum height
svh = scrollView.heightAnchor.constraint(equalToConstant: scrollView.contentSize.height)
svh.priority = .required - 1
svh.isActive = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//public func configure(with viewModel: someViewModel) {
// titleLabel.text = viewModel.title
//}
public func configure(with str: String) {
titleLabel.text = str
// force the scroll view to update its layout
scrollView.setNeedsLayout()
scrollView.layoutIfNeeded()
// force self to update its layout
self.setNeedsLayout()
self.layoutIfNeeded()
}
}
Each tap anywhere on the screen will cycle through a few sample strings to change the text in the label, giving us this:

iOS: How to scroll navigation bar as view controller scrolls

I want to scroll the navigation bar as the user scrolls on the view controller. This should be similar to how the YouTube app's home page is working. When the user scrolls down, the navigation bar should be made visible. The navigation bar should move as much as the scroll amount.
I'm aware of hidesBarOnSwipe and setNavigationBarHidden, but these do not give precise control of the y-axis. I'm also reading that Apple does not support directly modifying the navigation bar frame.
So, how does YouTube do this? I'm looking for an MVP demonstrating navigation bar position change along with a UIScrollView offset change.
Without additional detail about what you want to do, I'll make some guesses.
First, the top of the YouTube app's home page is almost certainly not a UINavigationBar -- it doesn't behave like one, there is no pushing/popping of controllers going on, it's in a tab bar controller setup, etc.
So, let's assume it's a view with subviews - we'll call it a "sliding header view" - and your goal is:
don't let the header view's top scroll down
"push it up" when scrolling up
"pull it down" when scrolling down
We can accomplish this by constraining the Top of that header view to the Top of the scroll view's Frame Layout Guide.
when we start to scroll, we'll save the current .contentOffset.y
when we scroll, we'll get the relative scroll y distance
if we're scrolling Up, we'll change the Top Constraint .constant value to move the header view up
if we're scrolling Down, we'll change the Top Constraint .constant value to move the header view down
Here's how it will look at the start:
as we scroll up just a little:
after we've scrolled up farther:
as we scroll down just a little:
after we've scrolled down farther:
Here's the example code for that:
Simple two-label "header" view
class SlidingHeaderView: UIView {
// simple view with two labels
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
backgroundColor = .systemBlue
let v1 = UILabel()
v1.translatesAutoresizingMaskIntoConstraints = false
v1.text = "Label 1"
v1.backgroundColor = .yellow
addSubview(v1)
let v2 = UILabel()
v2.translatesAutoresizingMaskIntoConstraints = false
v2.text = "Label 2"
v2.backgroundColor = .yellow
addSubview(v2)
NSLayoutConstraint.activate([
v1.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12.0),
v1.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12.0),
v2.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12.0),
v2.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12.0),
v1.topAnchor.constraint(equalTo: topAnchor, constant: 8.0),
v2.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8.0),
v2.topAnchor.constraint(equalTo: v1.bottomAnchor, constant: 4.0),
v2.heightAnchor.constraint(equalTo: v1.heightAnchor),
])
}
}
example view controller
class SlidingHeaderViewController: UIViewController {
let scrollView: UIScrollView = {
let v = UIScrollView()
v.contentInsetAdjustmentBehavior = .never
return v
}()
let slidingHeaderView: SlidingHeaderView = {
let v = SlidingHeaderView()
return v
}()
let contentView: UIView = {
let v = UIView()
v.backgroundColor = .systemYellow
return v
}()
// Top constraint for the slidingHeaderView
var slidingViewTopC: NSLayoutConstraint!
// to track the scroll activity
var curScrollY: CGFloat = 0
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
[scrollView, slidingHeaderView, contentView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
}
// add contentView and slidingHeaderView to the scroll view
[contentView, slidingHeaderView].forEach { v in
scrollView.addSubview(v)
}
// add scroll view to self.view
view.addSubview(scrollView)
let safeG = view.safeAreaLayoutGuide
let contentG = scrollView.contentLayoutGuide
let frameG = scrollView.frameLayoutGuide
// we're going to change slidingHeaderView's Top constraint relative to the Top of the scroll view FRAME
slidingViewTopC = slidingHeaderView.topAnchor.constraint(equalTo: frameG.topAnchor, constant: 0.0)
NSLayoutConstraint.activate([
// scroll view Top to view Top
scrollView.topAnchor.constraint(equalTo: safeG.topAnchor, constant: 0.0),
// scroll view Leading/Trailing/Bottom to safe area
scrollView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 0.0),
scrollView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: 0.0),
scrollView.bottomAnchor.constraint(equalTo: safeG.bottomAnchor, constant: 0.0),
// constrain slidingHeaderView Top to scroll view's FRAME
slidingViewTopC,
// slidingHeaderView to Leading/Trailing of scroll view FRAME
slidingHeaderView.leadingAnchor.constraint(equalTo: frameG.leadingAnchor, constant: 0.0),
slidingHeaderView.trailingAnchor.constraint(equalTo: frameG.trailingAnchor, constant: 0.0),
// no Height or Bottom constraint for slidingHeaderView
// content view Top/Leading/Trailing/Bottom to scroll view's CONTENT GUIDE
contentView.topAnchor.constraint(equalTo: contentG.topAnchor, constant: 0.0),
contentView.leadingAnchor.constraint(equalTo: contentG.leadingAnchor, constant: 0.0),
contentView.trailingAnchor.constraint(equalTo: contentG.trailingAnchor, constant: 0.0),
contentView.bottomAnchor.constraint(equalTo: contentG.bottomAnchor, constant: 0.0),
// content view Width to scroll view's FRAME
contentView.widthAnchor.constraint(equalTo: frameG.widthAnchor, constant: 0.0),
])
// add some content to the content view so we have something to scroll
addSomeContent()
// because we're going to track the scroll offset
scrollView.delegate = self
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if slidingHeaderView.frame.height == 0 {
// get the size of the slidingHeaderView
let sz = slidingHeaderView.systemLayoutSizeFitting(CGSize(width: scrollView.frame.width, height: .greatestFiniteMagnitude), withHorizontalFittingPriority: .required, verticalFittingPriority: .defaultLow)
// use its Height for the scroll view's Top contentInset
scrollView.contentInset = UIEdgeInsets(top: sz.height, left: 0, bottom: 0, right: 0)
}
}
func addSomeContent() {
// create a vertical stack view with a bunch of labels
// and add it to our content view so we have something to scroll
let stack = UIStackView()
stack.axis = .vertical
stack.spacing = 32
stack.backgroundColor = .gray
stack.translatesAutoresizingMaskIntoConstraints = false
for i in 1...20 {
let v = UILabel()
v.text = "Label \(i)"
v.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
v.heightAnchor.constraint(equalToConstant: 48.0).isActive = true
stack.addArrangedSubview(v)
}
contentView.addSubview(stack)
NSLayoutConstraint.activate([
stack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16.0),
stack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16.0),
stack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16.0),
stack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -16.0),
])
}
}
extension SlidingHeaderViewController: UIScrollViewDelegate {
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
curScrollY = scrollView.contentOffset.y
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let diffY = scrollView.contentOffset.y - curScrollY
var newY: CGFloat = slidingViewTopC.constant - diffY
if diffY < 0 {
// we're scrolling DOWN
newY = min(newY, 0.0)
} else {
// we're scrolling UP
if scrollView.contentOffset.y <= -slidingHeaderView.frame.height {
newY = 0.0
} else {
newY = max(-slidingHeaderView.frame.height, newY)
}
}
// update slidingHeaderView Top constraint constant
slidingViewTopC.constant = newY
curScrollY = scrollView.contentOffset.y
}
}
Everything is done via code - no #IBOutlet or #IBAction connections needed.

How to make UIView which is inside scrollview adapt to screen orientation when user changes screen from portrait to landscape in swift

How to make UIView which is inside scrollview adapt to screen orientation when user changes screen from portrait to landscape in swift?
var scrollView: UIScrollView = {
var scroll = UIScrollView()
scroll.translatesAutoresizingMaskIntoConstraints = false
return scroll
}()
view.addSubview(scrollView)
scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 100).isActive = true
scrollView.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0).isActive = true
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0).isActive = true
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0).isActive = true
scrollView.heightAnchor.constraint(equalToConstant: 500).isActive = true
for i in 0..<arr.count {
var contentView = UIView()
contentView.frame = CGRect(x: i * Int(view.bounds.size.width) + 10, y: 0, width: Int(view.bounds.size.width) - 20 , height: Int(view.frame.height))
scrollView.contentSize = CGSize(width: (view.frame.size.width * CGFloat((Double(i)+1))) ,height: scrollView.frame.size.height)
}
Image
You really want to be using auto-layout instead of trying to calculate frame sizes. Let it do all the work for you.
Based on your code, it looks like you want each "contentView" to be the width of the scrollView's frame, minus 20 (so you have 10-pts of space on each side).
You can quite easily do this by embedding your contentViews in a UIStackView.
Here's a simple example:
class ViewController: UIViewController {
var scrollView: UIScrollView = {
var scroll = UIScrollView()
scroll.translatesAutoresizingMaskIntoConstraints = false
return scroll
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(scrollView)
// use a stack view to hold and arrange the scrollView's subviews
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.spacing = 20
// add the stackView to the scrollView
scrollView.addSubview(stackView)
// respect safe area
let safeG = view.safeAreaLayoutGuide
// use scrollView's Content Layout Guide to define scrollable content
let layoutG = scrollView.contentLayoutGuide
// use scrollView's Frame Layout Guide to define content height (since you want horizontal scrolling)
let frameG = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: safeG.topAnchor, constant: 100),
// you're setting leading and trailing, so no need for centerX
//scrollView.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0),
scrollView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 0),
scrollView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: 0),
// let's constrain the scrollView bottom to the view (safe area) bottom
//scrollView.heightAnchor.constraint(equalToConstant: 500),
scrollView.bottomAnchor.constraint(equalTo: safeG.bottomAnchor, constant: -10.0),
// constrain Top and Bottom of the stackView to scrollView's Content Layout Guide
stackView.topAnchor.constraint(equalTo: layoutG.topAnchor),
stackView.bottomAnchor.constraint(equalTo: layoutG.bottomAnchor),
// 10-pts space on leading and trailing
stackView.leadingAnchor.constraint(equalTo: layoutG.leadingAnchor, constant: 10.0),
stackView.trailingAnchor.constraint(equalTo: layoutG.trailingAnchor, constant: -10.0),
// constrain stackView's height to scrollView's Frame Layout Guide height
stackView.heightAnchor.constraint(equalTo: frameG.heightAnchor),
])
// add some views to the stack view
let arr: [UIColor] = [
.red, .green, .blue, .yellow, .purple,
]
for i in 0..<arr.count {
let contentView = UIView()
contentView.backgroundColor = arr[i]
stackView.addArrangedSubview(contentView)
// constrain each "contentView" width to scrollView's Frame Layout Guide width minus 20
contentView.widthAnchor.constraint(equalTo: frameG.widthAnchor, constant: -20).isActive = true
// don't do this
//contentView.frame = CGRect(x: i * Int(view.bounds.size.width) + 10, y: 0, width: Int(view.bounds.size.width) - 20 , height: Int(view.frame.height))
// don't do this
//scrollView.contentSize = CGSize(width: (view.frame.size.width * CGFloat((Double(i)+1))) ,height: scrollView.frame.size.height)
}
}
}
Run that and see if that's what you're going for.
You need to add your subviews to the scroll view and setup their constraints - using of an auto-layout. Don't use contentView.frame = CGRect(...) and scrollView.contentSize = CGSize(...).
For example you can change your for-in to this:
Note: this is only example, change your for-in loop to your needs.
for i in 0..<arr.count {
// we need to distinguish the first and last subviews (because different constraints)
let topAnchor = i == 0 ? scrollView.topAnchor : scrollView.subviews.last!
let isLast = i == arr.count - 1
// here we will use a specific height for all subviews except the last one
let subviewHeight = 60
var contentView = UIView()
contentView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(contentView)
if isLast {
NSLayoutConstraint.activate([
contentView.topAnchor.constraint(equalTo: topAnchor),
contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
contentView.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor)
]
} else {
NSLayoutConstraint.activate([
contentView.topAnchor.constraint(equalTo: topAnchor),
contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
contentView.heightAnchor.constraint(equalToConstant: subviewHeight),
contentView.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor)
]
}
}

Horizontal scrolling collection view partially hiding first cell content on initial load

I have a UIView and inside that, I have added a UILabel and UICollectionView. The UICollectionView scrolls horizontally. But the issue is that when I run the app, the UICollectionView offset is set in such a way that the first cell is partly hidden. If I scroll to the right then I can see the contents of the first cell. But when I let go it bounces back to its original hidden state.
As you can see here the first cell has 2 UIViews but only the second one is visible.
When I scroll right, this is how it looks:
The, I... 10, is the cell that is getting hidden. I have added the elements programmatically, so could be a constraint issue. But I can't zero down on what could be causing it.
Here's my UICollectionViewCell:
class UserCountryCell: UICollectionViewCell {
let countryNameLabel: UILabel
let countryUserCountLabel: UILabel
override init(frame: CGRect) {
countryNameLabel = UILabel()
countryUserCountLabel = UILabel()
super.init(frame: frame)
self.addSubview(countryNameLabel)
countryNameLabel.translatesAutoresizingMaskIntoConstraints = false
countryNameLabel.font = UIFont.boldSystemFont(ofSize: 13)
countryNameLabel.textColor = .white
self.addSubview(countryUserCountLabel)
countryUserCountLabel.translatesAutoresizingMaskIntoConstraints = false
countryUserCountLabel.font = UIFont.systemFont(ofSize: 13)
countryUserCountLabel.textColor = .white
NSLayoutConstraint.activate([
countryNameLabel.trailingAnchor.constraint(equalTo: self.leadingAnchor, constant: 5),
countryNameLabel.heightAnchor.constraint(equalToConstant: 30),
countryNameLabel.topAnchor.constraint(equalTo: self.topAnchor, constant: 5),
countryNameLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 5),
// countryNameLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 0),
countryNameLabel.widthAnchor.constraint(equalToConstant: 20),
countryUserCountLabel.leadingAnchor.constraint(equalTo: countryNameLabel.trailingAnchor, constant: 5),
countryUserCountLabel.topAnchor.constraint(equalTo: self.topAnchor, constant: 5),
countryUserCountLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 5),
countryUserCountLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -5),
countryUserCountLabel.heightAnchor.constraint(equalToConstant: 30),
countryUserCountLabel.widthAnchor.constraint(equalToConstant: 40)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configureCell(countryName: String, countryUserCount: Int) {
countryNameLabel.text = countryName
countryUserCountLabel.text = String(countryUserCount)
}
}
OK - couple things...
As I said in my comment, add subviews and constraints to the cell's .contentView, not to the cell itself.
You do have a few mistakes in your constraints. You constrained the countryNameLabel.trailingAnchor to self.leadingAnchor ... that should be .leadingAnchor to .leadingAnchor.
Your .bottomAnchor constants should be negative.
If you want the labels' text to determine their widths, don't assign a .widthAnchor.
Try replacing your init with this:
override init(frame: CGRect) {
countryNameLabel = UILabel()
countryUserCountLabel = UILabel()
super.init(frame: frame)
contentView.addSubview(countryNameLabel)
countryNameLabel.translatesAutoresizingMaskIntoConstraints = false
countryNameLabel.font = UIFont.boldSystemFont(ofSize: 13)
countryNameLabel.textColor = .white
contentView.addSubview(countryUserCountLabel)
countryUserCountLabel.translatesAutoresizingMaskIntoConstraints = false
countryUserCountLabel.font = UIFont.systemFont(ofSize: 13)
countryUserCountLabel.textColor = .white
NSLayoutConstraint.activate([
// needs to be .leadingAnchor to .leadingAnchor
//countryNameLabel.trailingAnchor.constraint(equalTo: self.leadingAnchor, constant: 5),
countryNameLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 5),
countryNameLabel.heightAnchor.constraint(equalToConstant: 30),
countryNameLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 5),
countryNameLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -5),
// if you want the label width to fit its text
// don't set the label's widthAnchor
//countryNameLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 0),
//countryNameLabel.widthAnchor.constraint(equalToConstant: 20),
countryUserCountLabel.leadingAnchor.constraint(equalTo: countryNameLabel.trailingAnchor, constant: 5),
countryUserCountLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 5),
countryUserCountLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -5),
countryUserCountLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -5),
countryUserCountLabel.heightAnchor.constraint(equalToConstant: 30),
// if you want the label width to fit its text
// don't set the label's widthAnchor
//countryUserCountLabel.widthAnchor.constraint(equalToConstant: 40)
])
}

layout with before and after margins with equalToSystemSpacingAfter

I’m trying to change some layouts I have to a number-less layout.
This is what I have for a segmented bar that should be inside a container view with something like this | - margin - segmented - margin -|
segmentedControl.leadingAnchor.constraint(equalToSystemSpacingAfter: margins.leadingAnchor, multiplier: 1),
segmentedControl.trailingAnchor.constraint(equalToSystemSpacingAfter: margins.trailingAnchor, multiplier: 1),
I know that the second line doesn’t make any sense, but I don’t see any equalToSystemSpacingBEFORE just after, and I’m not sure how to do it without having to rely only on layout propagation.
Basically, the leadingAchor works fine with this code, but the trailingAnchor (as the method name implies) adds the margin AFTER the trailing anchor, which is not what I want.
any ideas?
You can constrain the trailingAnchor of your "container" view relative to the trailingAnchor of your segmented control.
Here's a quick example which I believe gives you the layout you want:
class SysSpacingViewController: UIViewController {
let seg: UISegmentedControl = {
let v = UISegmentedControl(items: ["A", "B", "C"])
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let cView: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .white
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemYellow
cView.addSubview(seg)
view.addSubview(cView)
let g = view.safeAreaLayoutGuide
let m = cView.layoutMarginsGuide
NSLayoutConstraint.activate([
cView.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
cView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
cView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
cView.heightAnchor.constraint(equalToConstant: 70.0),
seg.leadingAnchor.constraint(equalToSystemSpacingAfter: m.leadingAnchor, multiplier: 1.0),
m.trailingAnchor.constraint(equalToSystemSpacingAfter: seg.trailingAnchor, multiplier: 1.0),
seg.centerYAnchor.constraint(equalTo: cView.centerYAnchor),
])
}
}
Result:
I think you can use this:
segmentedControl.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 8).isActive = true
segmentedControl.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -8).isActive = true
Please change the name of containerView and constants accordingly.

Resources