What am I trying to achieve:
I have a collectionView which represents events in a timeline. These cells (events) have description label that is constrained to the left edge.
When scrolling to events to the left on the timeline, some events are too wide and are not showing the description label immediately (i must scroll to the very beginning of the cell on timeline to see it)
The idea was to setup constraints between cells label to collectionView frame so that you could see event description immediately, but these are different view hierarchies.
What would be the best way to approach this?
What i have tried:
Setting up the constraint crashes the app:
Terminating app due to uncaught exception 'NSGenericException', reason: 'Unable to activate constraint with anchors <NSLayoutXAxisAnchor:0x600002cbe700 "UILabel:0x11ef0ea20.leading"> and <NSLayoutXAxisAnchor:0x600002cbf240 "UILayoutGuide:0x6000000289a0'UIScrollView-frameLayoutGuide'.leading"> because they have no common ancestor. Does the constraint or its anchors reference items in different view hierarchies? That's illegal.'
Additional Info:
UICollectionViewCompositionalLayout:
UICollectionViewCompositionalLayout { sectionIndex, layoutEnvironment in
let item = data.items[sectionIndex]
let contentWidth = data.contentWidth
return context.coordinator.layout(item, contentWidth)
}
NSCollectionLayoutSection:
var layout: (Item, CGFloat) -> NSCollectionLayoutSection = { item, contentWidth in
let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(contentWidth), heightDimension: .absolute(100))
let itemSizes = item.items.map { NSCollectionLayoutItem(layoutSize: .init(widthDimension: .absolute(CGFloat($0.duration)), heightDimension: .absolute(100)))}
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: itemSizes)
group.interItemSpacing = .fixed(0)
group.contentInsets = .init(top: 0, leading: item.leftGap, bottom: 0, trailing: item.rightGap)
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = 0
let header = NSCollectionLayoutBoundarySupplementaryItem(
layoutSize: .init(widthDimension: .absolute(255), heightDimension: .absolute(100)),
elementKind: UICollectionView.elementKindSectionHeader,
alignment: .leading
)}
}
header.pinToVisibleBounds = true
section.boundarySupplementaryItems = [header]
return section
}
CollectionViewCell:
class MyCollectionViewCell: UICollectionViewCell {
static let identifier: String = "MyCollectionViewCell"
var item: DataState.Item = .dummy {
didSet {
descriptionLabel.text = item.title
descriptionTimeLabel.text = item.timeDescription
}
}
var containerView: UIView = {
let containerView = UIView()
containerView.translatesAutoresizingMaskIntoConstraints = false
containerView.layer.cornerRadius = 15
containerView.clipsToBounds = true
return containerView
}()
var bg: UIView = {
let bg = UIView()
bg.translatesAutoresizingMaskIntoConstraints = false
bg.backgroundColor = .black
return bg
}()
var descriptionLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .systemFont(ofSize: 31, weight: .regular)
return label
}()
var descriptionTimeLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .systemFont(ofSize: 23, weight: .regular)
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(containerView)
containerView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 5).isActive = true
containerView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -5).isActive = true
containerView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -5).isActive = true
containerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 5).isActive = true
containerView.addSubview(bg)
bg.topAnchor.constraint(equalTo: containerView.topAnchor).isActive = true
bg.bottomAnchor.constraint(equalTo: containerView.bottomAnchor).isActive = true
bg.trailingAnchor.constraint(equalTo: containerView.trailingAnchor).isActive = true
bg.leadingAnchor.constraint(equalTo: containerView.leadingAnchor).isActive = true
containerView.addSubview(descriptionLabel)
descriptionLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 10).isActive = true
descriptionLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 10).isActive = true
containerView.addSubview(descriptionTimeLabel)
descriptionTimeLabel.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -10).isActive = true
descriptionTimeLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 10).isActive = true
}
}
Just to remind whole point is to make labels of cell visible immediately as the wide cell being revealed and be continously updated so when the cell is fully visible label's leadingAnchor and containerView leadingAnchor equals 10
maybe you can try this. If this in my case there 2 separate file. 1 for cell, and 1 viewController with collectionView in it.
I will use UICollectionViewDataSource, UICollectionViewDelegate to the viewController. You can set the cell with add 1 more extention UICollectionViewFlowLayout or
You can add setUpUI direct to the UICollectionViewCell and call it to override func awakeFromNib(). here's the code :
mainView.translatesAutoresizingMaskIntoConstraints = false mainView.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.size.width - 40).isActive = true
mainView: UIView! from cell and UIScreen.main.bounds.size.width is the controller collectionView Size.
add reference outlet of collectionView controller to mainView
Don't Forget to check you constraint at cell. and change the autoRezized from "none" to "automatic"
So turns out that if you add constraints to cell as properties and then in: scrollViewDidScroll(UIScrollView) find visibleCells and filter those which are visible just halfway or so. you can then change constants on constraints and update the view.
works great. looks weird.
example:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard let collectionView = scrollView as? UICollectionView else { return } // move elsewhere just to access reference to collectionView and dont cast it everytime offset changes (which could be many times a second)
let scrollOffset = scrollView.contentOffset.x
if let visible = collectionView?.visibleCells as? [MyCell] {
let visibleDisappearing = visible.filter({ $0.frame.minX <= scrollOffset })
let visibleFull = visible.filter({ $0.frame.minX > scrollOffset })
for disappearingCell in visibleDisappearing {
let distance = scrollView.contentOffset.x - disappearingCell.frame.minX + 10 // 10 would be the original distance from edge to label
disappearingCell.descriptionLeadingConstraint.constant = distance
disappearingCell.descriptionTimeLeadingConstraint.constant = distance
disappearingCell.layoutIfNeeded()
}
for fullCell in visibleFull {
fullCell.descriptionLeadingConstraint.constant = 10
fullCell.descriptionTimeLeadingConstraint.constant = 10
fullCell.layoutIfNeeded()
}
}
}
Related
Usually, I would use SwiftUI's ScrollView, but in my edge case scenario, I need to use it as a UIScrollView in SwiftUI's UIViewRepresentable
struct CALayerScrollView: UIViewRepresentable {
func makeUIView(context: Context) -> some UIView {
var view = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width / 2, height: UIScreen.main.bounds.height / 2))
let scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
return scrollView
}()
let scrollViewContainer: UIStackView = {
let view = UIStackView()
view.axis = .vertical
view.spacing = 10
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
let redView: UIView = {
let view = UIView()
view.heightAnchor.constraint(equalToConstant: 500).isActive = true
view.backgroundColor = .red
return view
}()
let blueView: UIView = {
let view = UIView()
view.heightAnchor.constraint(equalToConstant: 200).isActive = true
view.backgroundColor = .blue
return view
}()
let greenView: UIView = {
let view = UIView()
view.heightAnchor.constraint(equalToConstant: 1200).isActive = true
view.backgroundColor = .green
return view
}()
view.addSubview(scrollView)
scrollView.addSubview(scrollViewContainer)
scrollViewContainer.addArrangedSubview(redView)
scrollViewContainer.addArrangedSubview(blueView)
scrollViewContainer.addArrangedSubview(greenView)
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
scrollView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
scrollViewContainer.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor).isActive = true
scrollViewContainer.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor).isActive = true
scrollViewContainer.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
scrollViewContainer.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
// this is important for scrolling
scrollViewContainer.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true
return view
}
func updateUIView(_ uiView: UIViewType, context: Context) { }
}
I've tried setting the viewAxis to .horizontal, but I still does not scroll laterally.
Any advices is appreciated. Thanks
You are setting the stack view axis to Vertical -- but you want Horizontal scrolling... so set it to .horizontal.
You are setting Height for each arranged subview, but you haven't set the Widths... so give them Widths.
You should constrain the scroll view's content to the scroll view's Content Layout Guide.
Because you're setting varying Heights, it's not quite clear if you want only horizontal scrolling... so this example ends up scrolling both directions:
struct TestView: View {
var body: some View {
VStack {
CALayerScrollView()
}
.frame(width: 240, height: 400)
.background(Color.yellow)
}
}
struct CALayerScrollView: UIViewRepresentable {
func makeUIView(context: Context) -> some UIView {
let view = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width / 2, height: UIScreen.main.bounds.height / 2))
let scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
return scrollView
}()
let scrollViewContainer: UIStackView = {
let view = UIStackView()
// Horizontal Stack View
view.axis = .horizontal
view.spacing = 10
// .top Alignment, because we're setting different heights for the subviews
view.alignment = .top
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
let redView: UIView = {
let view = UIView()
view.heightAnchor.constraint(equalToConstant: 500).isActive = true
// also needs a width
view.widthAnchor.constraint(equalToConstant: 200).isActive = true
view.backgroundColor = .red
return view
}()
let blueView: UIView = {
let view = UIView()
view.heightAnchor.constraint(equalToConstant: 200).isActive = true
// also needs a width
view.widthAnchor.constraint(equalToConstant: 400).isActive = true
view.backgroundColor = .blue
return view
}()
let greenView: UIView = {
let view = UIView()
view.heightAnchor.constraint(equalToConstant: 1200).isActive = true
// also needs a width
view.widthAnchor.constraint(equalToConstant: 800).isActive = true
view.backgroundColor = .green
return view
}()
view.addSubview(scrollView)
scrollView.addSubview(scrollViewContainer)
scrollViewContainer.addArrangedSubview(redView)
scrollViewContainer.addArrangedSubview(blueView)
scrollViewContainer.addArrangedSubview(greenView)
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
scrollView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
// we want to constrain the scroll view *content* to the Content Layout Guide
let cg = scrollView.contentLayoutGuide
scrollViewContainer.leadingAnchor.constraint(equalTo: cg.leadingAnchor).isActive = true
scrollViewContainer.trailingAnchor.constraint(equalTo: cg.trailingAnchor).isActive = true
scrollViewContainer.topAnchor.constraint(equalTo: cg.topAnchor).isActive = true
scrollViewContainer.bottomAnchor.constraint(equalTo: cg.bottomAnchor).isActive = true
return view
}
func updateUIView(_ uiView: UIViewType, context: Context) { }
}
Should get you on your way...
I'm having trouble setting constraints to a label programmatically in Swift. I want the label to fill the entire screen. But I dont know how to do.
Thank you for your help.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let label = UILabel()
label.text = "Hello"
label.backgroundColor = UIColor.yellow
self.view.addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
let width: NSLayoutConstraint
width = label.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: 1)
let top: NSLayoutConstraint
top = label.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor)
let bottom: NSLayoutConstraint
bottom = label.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor)
width.isActive = true
bottom.isActive = true
top.isActive = true
}
}
You can use this extension for all of your views
extension UIView {
public func alignAllEdgesWithSuperview() {
guard let superview = self.superview else {
fatalError("add \(self) to a superview first.")
}
self.translatesAutoresizingMaskIntoConstraints = false
let constraints = [
self.leadingAnchor.constraint(equalTo: superview.leadingAnchor),
self.trailingAnchor.constraint(equalTo: superview.trailingAnchor),
self.topAnchor.constraint(equalTo: superview.topAnchor),
self.bottomAnchor.constraint(equalTo: superview.bottomAnchor)
]
NSLayoutConstraint.activate(constraints)
}
}
and the usage
view.addSubview(someView)
someView.alignAllEdgesWithSuperview()
The func addBlackView is adding a black view everytime the func is called. The black view is connected a to uiPangesture the problem is evertyime the func addblackview is called the code is reseting the position of wherever the first black has been moved. You can see what is goin on in the gif below. I just want the 1st black view to not move and stay in the same position if a new black view is Called.
import UIKit
class ViewController: UIViewController {
var image1Width2: NSLayoutConstraint!
var iHieght: NSLayoutConstraint!
var currentView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(currentView)
view.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
button.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16),
button.widthAnchor.constraint(equalToConstant: 100),
button.heightAnchor.constraint(equalToConstant: 80),
])
button.addTarget(self,action: #selector(addBlackView),for: .touchUpInside)
}
let slider:UISlider = {
let slider = UISlider(frame: .zero)
return slider
}()
private lazy var button: UIButton = {
let button = UIButton()
button.backgroundColor = .blue
button.setTitleColor(.white, for: .normal)
button.setTitle("add", for: .normal)
return button
}()
let blackView: UIView = {
let view = UIView()
view.backgroundColor = .black
return view
}()
var count = 0
#objc
private func addBlackView() {
let newBlackView = UIView(frame: CGRect(x: 20, y: 20, width: 100, height: 100)) // whatever frame you want
newBlackView.backgroundColor = .orange
self.view.addSubview(newBlackView)
self.currentView = newBlackView
let recognizer = UIPanGestureRecognizer(target: self, action: #selector(moveView(_:)))
newBlackView.addGestureRecognizer(recognizer)
newBlackView.isUserInteractionEnabled = true
image1Width2 = newBlackView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.1)
image1Width2.isActive = true
iHieght = newBlackView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.1)
iHieght.isActive = true
count += 1
newBlackView.tag = (count)
newBlackView.translatesAutoresizingMaskIntoConstraints = false
newBlackView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
newBlackView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
}
#objc private func moveView(_ recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .changed:
let translation = recognizer.translation(in: self.view)
recognizer.view!.center = .init(x: recognizer.view!.center.x + translation.x,
y: recognizer.view!.center.y + translation.y)
recognizer.setTranslation(.zero, in: self.view)
default:
break
}
}
}
They always go back to the centre because you have constrained the black (orange) views to the centre:
newBlackView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
newBlackView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
You shouldn't even be able to drag any of the views at all, but I guess setting center ignores constraints for some reason. Anyway, when you add a new view, UIKit calls view.setNeedsLayout/layoutIfNeeded somewhere down the line, and this causes all the views to realise "oh wait, I'm supposed to be constrained to the centre!" and snap back. :D
If you want to keep using constraints, try storing the centre X and Y constraints of all the views in an array:
var centerXConstraints: [NSLayoutConstraint] = []
var centerYConstraints: [NSLayoutConstraint] = []
And append to them when you add a new view:
let yConstraint = newBlackView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
let xConstraint = newBlackView.centerXAnchor.constraint(equalTo: view.centerXAnchor)
xConstraint.isActive = true
yConstraint.isActive = true
centerXConstraints.append(xConstraint)
centerYConstraints.append(yConstraint)
Then, rather than changing the center, change the constant of these constraints:
let centerXConstraint = centerXConstraints[recognizer.view!.tag - 1]
let centerYConstraint = centerYConstraints[recognizer.view!.tag - 1]
centerXConstraint.constant += translation.x
centerYConstraint.constant += translation.y
Alternatively, and this is what I would do, just remove all your constraints, and translateAutoresizingMaskIntoConstraints = true. This way you can freely set your center.
I had this custom view who worked like a charm before i introduce a LinkView for a Metadata
After i introduce a LinkView, since it was inside a stackView i had to remove linkView from superview when preparing for reusable (not sure why tried to redraw layout, but seems this not work with LinkView) the problems shows up when scrolling down elements, seems the data get lost at certain point, curious thing is that it only happens with the reusable element that contains the linkView item, is there any reason for this ? How can i fix it ?
Here is the code i use for the cell
final class TimeLineTableViewCell: UITableViewCell {
var cornerRadius: CGFloat = 6
var shadowOffsetWidth = 0
var shadowOffsetHeight = 3
var shadowColor: UIColor = .gray
var shadowOpacity: Float = 0.3
lazy var containerView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .white
view.addSubview(stackViewContainer)
let shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius)
view.layer.cornerRadius = cornerRadius
view.clipsToBounds = true
view.layer.masksToBounds = false
view.layer.shadowColor = shadowColor.cgColor
view.layer.shadowOffset = CGSize(width: shadowOffsetWidth, height: shadowOffsetHeight);
view.layer.shadowOpacity = shadowOpacity
view.layer.shadowPath = shadowPath.cgPath
return view
}()
lazy var stackViewContainer: UIStackView = {
let stack = UIStackView()
stack.axis = .horizontal
stack.alignment = .center
stack.translatesAutoresizingMaskIntoConstraints = false
stack.distribution = .fill
stack.spacing = 10.0
stack.addArrangedSubview(profileImage)
stack.addArrangedSubview(stackViewDataHolder)
return stack
}()
lazy var profileImage: UIImageView = {
let image = UIImage()
let imageView = UIImageView(image: image)
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFit
return imageView
}()
lazy var userName: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
return label
}()
lazy var tweetInfo: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
return label
}()
lazy var tweetText: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
return label
}()
lazy var linkView: LPLinkView = {
let viewer = LPLinkView(frame: CGRect(origin: .zero, size: .init(width: 200, height: 20)))
viewer.translatesAutoresizingMaskIntoConstraints = false
return viewer
}()
lazy var stackViewDataHolder: UIStackView = {
let stack = UIStackView()
stack.axis = .vertical
stack.translatesAutoresizingMaskIntoConstraints = false
stack.distribution = .fillProportionally
stack.addArrangedSubview(userName)
stack.addArrangedSubview(tweetInfo)
stack.addArrangedSubview(tweetText)
return stack
}()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
override func prepareForReuse() {
linkView.removeFromSuperview()
}
func configure(viewModel: ProfileTweetViewModel) {
tweetInfo.configure(model: viewModel.tweetInfo)
userName.configure(model: viewModel.name)
tweetText.configure(model: viewModel.tweet)
if let metadata = viewModel.linkData {
linkView = LPLinkView(metadata: metadata)
stackViewDataHolder.addArrangedSubview(linkView)
//Tried almost all layoyt options but seems a previous view can't be updated since frame is wrong
}
if let url = viewModel.profilePic {
profileImage.downloadImage(from: url)
}
}
}
private extension TimeLineTableViewCell {
struct Metrics {
static let lateralPadding: CGFloat = 8
}
func constraints() {
NSLayoutConstraint.activate([
stackViewContainer.topAnchor.constraint(equalTo: containerView.topAnchor, constant: Metrics.lateralPadding),
stackViewContainer.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -Metrics.lateralPadding),
stackViewContainer.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: Metrics.lateralPadding),
stackViewContainer.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -Metrics.lateralPadding),
profileImage.heightAnchor.constraint(equalTo: profileImage.widthAnchor, multiplier: 1.0),
profileImage.widthAnchor.constraint(equalToConstant: 50.0),
])
}
func commonInit() {
addSubview(containerView)
backgroundColor = .clear
NSLayoutConstraint.activate([
containerView.topAnchor.constraint(equalTo: topAnchor),
containerView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 4),
containerView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -4),
containerView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -4),
])
constraints()
}
}
Thank you for your time.
The issue was related to .fillProportionally in stackView
since the linkView sometimes renders with 0 height, i just had to use .fill property in stackView in order to show it fully
I am trying to understand how to use a UIScrollView programmatically.
When I give my content a size that does not fit on screen though, it does not scroll.
final class ProfileView: UIView {
private var isIpad: Bool {
return UIDevice.current.userInterfaceIdiom == .pad
}
private lazy var headerImageView: UIImageView = {
let iv = UIImageView(frame: .zero)
iv.translatesAutoresizingMaskIntoConstraints = false
iv.heightAnchor.constraint(equalToConstant: 600).isActive = true
iv.backgroundColor = .purple
return iv
}()
private lazy var profileImageView: UIImageView = {
let iv = UIImageView(frame: .zero)
iv.translatesAutoresizingMaskIntoConstraints = false
[iv.heightAnchor, iv.widthAnchor].forEach { $0.constraint(equalToConstant: 170).isActive = true }
iv.layer.cornerRadius = 170 / 2
iv.layer.borderColor = .white
iv.layer.borderWidth = 3
iv.layer.masksToBounds = true
iv.contentMode = .scaleAspectFill
iv.clipsToBounds = true
iv.backgroundColor = .red
return iv
}()
private(set) lazy var nameLabel: UILabel = {
let label = UILabel(frame: .zero)
label.translatesAutoresizingMaskIntoConstraints = false
label.text = "Foo\nBar"
label.numberOfLines = 2
return label
}()
private lazy var contentScrollView: UIScrollView = {
let sv = UIScrollView(frame: .zero)
sv.translatesAutoresizingMaskIntoConstraints = false
return sv
}()
override init(frame: CGRect) {
super.init(frame: frame)
configureLayout()
}
required init?(coder: NSCoder) {
return nil
}
private func configureLayout() {
addSubview(contentScrollView)
[headerImageView, profileImageView, nameLabel].forEach { contentScrollView.addSubview($0) }
let compactConstraints = [
contentScrollView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor),
contentScrollView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor),
contentScrollView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor),
contentScrollView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor),
headerImageView.topAnchor.constraint(equalTo: topAnchor),
headerImageView.leadingAnchor.constraint(equalTo: leadingAnchor),
headerImageView.trailingAnchor.constraint(equalTo: trailingAnchor),
profileImageView.centerYAnchor.constraint(equalTo: headerImageView.bottomAnchor),
profileImageView.centerXAnchor.constraint(equalTo: centerXAnchor),
nameLabel.topAnchor.constraint(equalTo: profileImageView.bottomAnchor, constant: 16),
nameLabel.centerXAnchor.constraint(equalTo: profileImageView.centerXAnchor),
]
NSLayoutConstraint.activate(compactConstraints)
}
}
This gives me the following -
The header image pushes the name label and avatar off screen and scrolling does not work.
I've read a bunch about giving the scroll view a huge offset so it scrolls, but that surely cannot be correct.
You need to create the constraints of all the subviews to the scrollView , add width constraint to the header img = profileview width and finally make bottom of label = bottom of the scrollview to make it infer it's height
let compactConstraints = [
contentScrollView.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor),
contentScrollView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor),
contentScrollView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor),
contentScrollView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor),
headerImageView.topAnchor.constraint(equalTo:contentScrollView.topAnchor),
headerImageView.leadingAnchor.constraint(equalTo:contentScrollView.leadingAnchor),
headerImageView.trailingAnchor.constraint(equalTo:contentScrollView.trailingAnchor),
headerImageView.widthAnchor.constraint(equalTo: self.widthAnchor) // add this
profileImageView.centerYAnchor.constraint(equalTo: headerImageView.bottomAnchor),
profileImageView.centerXAnchor.constraint(equalTo: centerXAnchor),
nameLabel.topAnchor.constraint(equalTo: profileImageView.bottomAnchor, constant: 16),
nameLabel.centerXAnchor.constraint(equalTo: profileImageView.centerXAnchor),
nameLabel.bottomAnchor.constraint(equalTo: contentScrollView.bottomAnchor) // and this
]