StackView: Overriding custom UIview intrinsicContent size produces very unexpected outcomes - swift

After struggling with programmatic dynamic UI elements for the last few weeks I decided I would give UIstackView a try.
I want custom UIView classes to occupy the stackview with different heights based upon user Input I would remove, add views on the fly.
I found out that stackViews base their 'cell' height upon the UI element's intrinsic content size. However, UIViews do not have one. I searched far and wide and found out that I need to override the View's intrisicContentsize function with one where I can explicitly set the width and height.
However results are very unpredictable and I'm sure there is some little thing that I just do not know dat causes this weird behaviour. Since I'm new to the language and there are a LOT of gotcha's I'm just gonna paste the code here and hope you'll be able to spot what I'm doing wrong.
I've read the docs ofc, a lot of articles, they all point to that override funcion that does not seem to work form me.
This is my mainViewController class;
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
var bv = BackButtonView(frame: CGRect.zero, image: UIImage(named: "backArrow.png")!)
var redView : UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .red
return view
}();
var blueView : UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .blue
return view
}();
let stack : UIStackView = {
let stack = UIStackView();
stack.translatesAutoresizingMaskIntoConstraints = false;
stack.axis = .vertical
stack.distribution = .fillProportionally;
stack.spacing = 8
return stack;
}();
override func viewDidLoad() {
let view = UIView(frame: UIScreen.main.bounds)
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .white
self.view = view
self.view.addSubview(stack)
createLayout()
bv.intrinsicContentSize
stack.addArrangedSubview(bv)
print(bv.frame)
print(bv.intrinsicContentSize)
stack.layoutIfNeeded()
stack.addArrangedSubview(redView)
stack.addArrangedSubview(blueView)
}
private func setConstraints(view: UIView) -> [NSLayoutConstraint] {
return [
view.heightAnchor.constraint(equalToConstant: 50),
]
}
private func createLayout() {
stack.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
stack.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
stack.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
stack.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
}
}
PlaygroundPage.current.needsIndefiniteExecution = true
PlaygroundPage.current.liveView = MyViewController()
Now here's my custom UIClass that is giving me all this trouble:
import UIKit
public class BackButtonView : UIView {
public var size = CGSize(width: 10, height: UIView.noIntrinsicMetric)
override open var intrinsicContentSize: CGSize {
return size
}
var button : UIButton;
public init(frame: CGRect, image: UIImage) {
button = UIButton()
super.init(frame : frame);
self.translatesAutoresizingMaskIntoConstraints = false;
self.backgroundColor = .black
self.addSubview(button)
setupButton(image: image);
print(button.intrinsicContentSize)
}
let backButtonTrailingPadding : CGFloat = -18
lazy var buttonConstraints = [
button.heightAnchor.constraint(equalToConstant: 50),
button.widthAnchor.constraint(equalToConstant: 50),
button.centerYAnchor.constraint(equalTo: self.centerYAnchor),
button.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: backButtonTrailingPadding),
]
private func setupButton(image: UIImage) {
self.button.translatesAutoresizingMaskIntoConstraints = false;
self.button.setImage(image, for: .normal);
self.button.contentMode = .scaleToFill
NSLayoutConstraint.activate(buttonConstraints)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
struct anchors {
var topAnchor = NSLayoutConstraint()
var bottomAnchor = NSLayoutConstraint()
var leadingAnchor = NSLayoutConstraint()
var trailingAnchor = NSLayoutConstraint()
}
}
You can see that the specified width in the size proprrty gets ignored.
Under the above conditions., this is the output
If I now change the my custom UIClass's intrisiContentSize height to anyting else, this happens:
public var size = CGSize(width: UIView.noIntrinsicMetric, height: 10)
override open var intrinsicContentSize: CGSize {
return size
}
result:
Please help me figure out what is and isn't going on
The final screen should look something like this:

Related

Abnormality when drawing a view inside of a stack view

Exploring stackviews I've ran into a problem of incorrect representation if views inside of it. So, to make a long story short...
I've made a custom checkbox:
class CheckBox: UIView, CheckBoxProtocol {
required init(frame: CGRect, color: UIColor) {
super.init(frame: frame)
self.layer.borderWidth = 5
self.layer.borderColor = color.cgColor
self.addSubview(checkmark)
checkmark.tintColor = color
let gesture = UITapGestureRecognizer(target: self, action: #selector(toggle))
self.addGestureRecognizer(gesture)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var isChecked = true
lazy var checkmark: UIImageView = {
let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: bounds.width, height: bounds.height))
imageView.isHidden = false
imageView.contentMode = .scaleAspectFit
imageView.image = UIImage(systemName: "checkmark")
return imageView
}()
#objc func toggle() {
self.isChecked.toggle()
self.checkmark.isHidden = !self.isChecked
}
In the Controller, when I add this view to the subviews it looks fairly normal and works as it should work (check-uncheck)
However when I add checkbox to the stackview it looses its visible frame and its functionality (does not check-uncheck) - you can see it on the screenshot
screenshot
Here is the code from the ViewController:
class SettingsViewController: UIViewController {
override func loadView() {
super.loadView()
self.view.backgroundColor = .white
self.view.addSubview(stackView)
}
override func viewDidLoad() {
super.viewDidLoad()
}
lazy var stackView: UIStackView = {
let stackView = UIStackView(frame: CGRect(x: 150, y: 150, width: 0, height: 0))
stackView.axis = .horizontal
stackView.spacing = 50
stackView.alignment = .fill
stackView.distribution = .fillEqually
[redCheckbox,
greenCheckbox,
blackCheckbox,
greyCheckbox,
brownCheckbox,
yellowCheckbox,
purpleCheckbox,
orangeCheckbox].forEach {stackView.addArrangedSubview($0)}
return stackView
}()
private let frame = CGRect(x: 0, y: 0, width: 30, height: 30)
lazy var redCheckbox: CheckBox = {
let colorFactory = CardViewFactory()
let color = colorFactory.getViewColor(modelColor: CardColor.red)
let checkbox = CheckBox(frame: frame, color: color)
return checkbox
}()
lazy var greenCheckbox: CheckBox = {
let colorFactory = CardViewFactory()
let color = colorFactory.getViewColor(modelColor: CardColor.green)
let checkbox = CheckBox(frame: frame, color: color)
return checkbox
}()
lazy var blackCheckbox: CheckBox = {
let colorFactory = CardViewFactory()
let color = colorFactory.getViewColor(modelColor: CardColor.black)
let checkbox = CheckBox(frame: frame, color: color)
return checkbox
}()
lazy var greyCheckbox: CheckBox = {
let colorFactory = CardViewFactory()
let color = colorFactory.getViewColor(modelColor: CardColor.grey)
let checkbox = CheckBox(frame: frame, color: color)
return checkbox
}()
lazy var brownCheckbox: CheckBox = {
let colorFactory = CardViewFactory()
let color = colorFactory.getViewColor(modelColor: CardColor.brown)
let checkbox = CheckBox(frame: frame, color: color)
return checkbox
}()
lazy var yellowCheckbox: CheckBox = {
let colorFactory = CardViewFactory()
let color = colorFactory.getViewColor(modelColor: CardColor.yellow)
let checkbox = CheckBox(frame: frame, color: color)
return checkbox
}()
lazy var purpleCheckbox: CheckBox = {
let colorFactory = CardViewFactory()
let color = colorFactory.getViewColor(modelColor: CardColor.purple)
let checkbox = CheckBox(frame: frame, color: color)
return checkbox
}()
lazy var orangeCheckbox: CheckBox = {
let colorFactory = CardViewFactory()
let color = colorFactory.getViewColor(modelColor: CardColor.orange)
let checkbox = CheckBox(frame: frame, color: color)
return checkbox
}()
It's because we're working with the lazy property and its life cycle can be a little different. Let's set constraints after the view has loaded. What I would suggest to do:
For each checkbox, change the frame to zero:
lazy var orangeCheckbox: CheckBox = {
let colorFactory = CardViewFactory()
let color = colorFactory.getViewColor(modelColor: CardColor.orange)
let checkbox = CheckBox(frame: .zero, color: colorFactory)
checkbox.translatesAutoresizingMaskIntoConstraints = false
return checkbox
}()
Do the same to the stackView:
lazy var stackView: UIStackView = {
let stackView = UIStackView(frame: .zero)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .horizontal
stackView.spacing = 5
stackView.alignment = .fill
stackView.distribution = .fillEqually
[redCheckbox,
greenCheckbox,
blackCheckbox,
greyCheckbox,
brownCheckbox,
yellowCheckbox,
purpleCheckbox,
orangeCheckbox].forEach {stackView.addArrangedSubview($0)}
return stackView
}()
Add some constraints on viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(stackView)
stackView.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor, constant: -100).isActive = true
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10).isActive = true
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 10).isActive = true
stackView.heightAnchor.constraint(equalToConstant: 40).isActive = true
[redCheckbox,
greenCheckbox,
blackCheckbox,
greyCheckbox,
brownCheckbox,
yellowCheckbox,
purpleCheckbox,
orangeCheckbox].forEach {
$0.heightAnchor.constraint(equalToConstant: 30).isActive = true
$0.widthAnchor.constraint(equalToConstant: 30).isActive = true
}
}
What you can do to the image inside the checkBox to work fine:
translatesAutoresizingMaskIntoConstraints = false
lazy var checkmark: UIImageView = {
let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.isHidden = false
imageView.contentMode = .scaleAspectFit
imageView.image = UIImage(systemName: "checkmark")
return imageView
}()
on your required init:
required init(frame: CGRect, color: UIColor) {
super.init(frame: frame)
self.layer.borderWidth = 5
self.layer.borderColor = color.cgColor
self.addSubview(checkmark)
checkmark.tintColor = color
checkmark.topAnchor.constraint(equalTo: topAnchor).isActive = true
checkmark.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
checkmark.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
checkmark.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
let gesture = UITapGestureRecognizer(target: self, action: #selector(toggle))
self.addGestureRecognizer(gesture)
setNeedsDisplay()
}
To offer some additional info...
You can save yourself a lot of duplicate coding.
Take a look at this...
First, slight modifications to your Checkbox class:
class CheckBox: UIView {//, CheckBoxProtocol {
var checkmark: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
imageView.image = UIImage(systemName: "checkmark")
return imageView
}()
var isChecked = true {
didSet {
checkmark.isHidden = !isChecked
}
}
required init(frame: CGRect, color: UIColor) {
super.init(frame: frame)
commonInit(color)
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit(.white)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit(.white)
}
func commonInit(_ color: UIColor) {
self.layer.borderWidth = 5
self.layer.borderColor = color.cgColor
self.addSubview(checkmark)
checkmark.tintColor = color
checkmark.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
// constrain image view to all 4 sides
checkmark.topAnchor.constraint(equalTo: topAnchor),
checkmark.leadingAnchor.constraint(equalTo: leadingAnchor),
checkmark.trailingAnchor.constraint(equalTo: trailingAnchor),
checkmark.bottomAnchor.constraint(equalTo: bottomAnchor),
])
let gesture = UITapGestureRecognizer(target: self, action: #selector(toggle))
self.addGestureRecognizer(gesture)
}
#objc func toggle() {
self.isChecked.toggle()
}
}
We've used auto-layout to keep the image view the same size as the view itself.
And, by implementing the var isChecked block we have a more "automated" way of setting the image view's hidden state.
In addition, we can now get the "state" of the checkbox in the controller like this:
if thisCheckBox.isChecked {
// do something
}
Now the view controller... we'll define an array of colors, loop through them to create the CheckBox objects, and add them to the stack view:
class SettingsViewController: UIViewController {
var stackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .horizontal
// desired spacing
stackView.spacing = 12
stackView.alignment = .fill
stackView.distribution = .fillEqually
return stackView
}()
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .white
self.view.addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// stack view Top constraint
stackView.topAnchor.constraint(equalTo: g.topAnchor, constant: 150.0),
// centered horizontally
stackView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
// explicit Height
stackView.heightAnchor.constraint(equalToConstant: 30.0),
])
let colors: [UIColor] = [
.red, .green, .black, .gray,
.brown, .yellow, .purple, .orange,
]
// loop through the colors, creating a new
// CheckBox object for each color
// and add it to the stack view
colors.forEach { c in
let checkbox = CheckBox(frame: .zero, color: c)
// 1:1 aspect ratio
checkbox.widthAnchor.constraint(equalTo: checkbox.heightAnchor).isActive = true
stackView.addArrangedSubview(checkbox)
}
}
}
As you can see, we've eliminated the need for all of the individual
lazy var redCheckbox: CheckBox = { ...
lazy var greenCheckbox: CheckBox = { ...
// etc
code blocks.

How to dynamically change UIStackView height based on content size?

I have a view below that I'm trying to dynamically change the height of based on the heights of the subviews in the content view. Currently, I'm setting the scrollView and contentView's contentSize with a static height of "screenHeight + 1000". What steps can I take to dynamically update the height property?
import Foundation
import UIKit
import TinyConstraints
class ViewController: UIViewController {
// MARK: - PROPERTIES
let screenWidth = UIScreen.main.bounds.width
let screenHeight = UIScreen.main.bounds.height
var contentViewSize = CGSize(width: screenWidth, height: screenHeight + 1000)
// MARK: - VIEW METHODS
override func viewDidLoad() {
super.viewDidLoad()
addSubviews()
constrainSubviews()
}
// MARK: - VIEW OBJECTS
lazy var scrollView: UIScrollView = {
let scrollView = UIScrollView(frame: .zero)
scrollView.frame = view.bounds
scrollView.contentSize = contentViewSize
scrollView.autoresizingMask = .flexibleHeight
scrollView.showsHorizontalScrollIndicator = true
scrollView.bounces = true
return scrollView
}()
lazy var contentView: UIView = {
let contentView = UIView()
contentView.frame.size = contentViewSize
return contentView
}()
lazy var topView: UIStackView = { ... }()
lazy var nutritionView: UIStackView = { ... }()
lazy var ingredientsView: UIStackView = { ... }()
lazy var instructionsView: UIStackView = { ... }()
fileprivate func addSubviews() {
view.addSubview(scrollView)
scrollView.addSubview(contentView)
contentView.addSubview(topView)
contentView.addSubview(nutritionView)
contentView.addSubview(ingredientsView)
contentView.addSubview(instructionsView)
}
fileprivate func constrainSubviews() {
let stack = [topView, nutritionView, ingredientsView, instructionsView]
contentView.stack(stack, spacing: screenHeight * 0.03)
contentView.centerXToSuperview()
contentView.topToSuperview(offset: screenHeight * 0.04)
contentView.width(screenWidth * 0.8)
}
}
You should add a scroll view and inside that add and stackview, and inside the stack view add the views, and the scroll view will increase dynamically, like this code.
class ViewController: UIViewController {
let scrollView = UIScrollView()
lazy var contentStackView = UIStackView(arrangedSubviews: [getView(height: 500, color: .red),
getView(height: 600, color: .blue),
getView(height: 70, color: .gray),
getView(height: 80,color: .yellow)])
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.view.backgroundColor = .white
contentStackView.axis = .vertical
setupConstraints()
}
func setupConstraints() {
self.view.addSubview(scrollView)
self.scrollView.addSubview(contentStackView)
scrollView.translatesAutoresizingMaskIntoConstraints = false
contentStackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: self.view.topAnchor),
scrollView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
contentStackView.topAnchor.constraint(equalTo: self.scrollView.topAnchor),
contentStackView.leadingAnchor.constraint(equalTo: self.scrollView.leadingAnchor),
contentStackView.trailingAnchor.constraint(equalTo: self.scrollView.trailingAnchor),
contentStackView.bottomAnchor.constraint(equalTo: self.scrollView.bottomAnchor),
contentStackView.widthAnchor.constraint(equalTo: self.scrollView.widthAnchor)
])
}
func getView(height: Double, color: UIColor) -> UIView {
let view = UIView()
view.backgroundColor = color
view.translatesAutoresizingMaskIntoConstraints = false
view.heightAnchor.constraint(equalToConstant: height).isActive = true
return view
}
}

After a sheet is presented in SwiftUI, the text color of interactive UI elements changes to systemBlue?

I have a sheet that is presented when a button is pressed:
struct Button: View {
#State isPresented = false
var body: some View {
Button(action: { self.isPresented.toggle() },
label: { Text("Text Label") }
).sheet(isPresented: $isPresented, content: {
//sheet contents in here
})
}
}
The class seen below is essentially the view in UIKit (I removed some of the code from the functions because it doesn't really have anything to do with the problem, but I kept the function names in there with descriptions so you can interpret what it's doing)
class CustomCalloutView: UIView, MGLCalloutView {
lazy var leftAccessoryView = UIView()
lazy var rightAccessoryView = UIView()
weak var delegate: MGLCalloutViewDelegate?
//MARK: Subviews -
let personImg: UIImageView = {
let img = UIImage(systemName: "person.fill")
var imgView = UIImageView(image: img)
//imgView.tintColor = UIColor(ciColor: .black)
imgView.translatesAutoresizingMaskIntoConstraints = false
return imgView
}()
let clockImg: UIImageView = {
let img = UIImage(systemName: "clock")
var imgView = UIImageView(image: img)
//imgView.tintColor = UIColor(ciColor: .black)
imgView.translatesAutoresizingMaskIntoConstraints = false
return imgView
}()
//Initialization of the view
required init() {
super.init(frame: CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: UIScreen.main.bounds.width * 0.75, height: 130)))
setup()
}
//other initializer
required init?(coder decoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//essentially just positioning the view
override var center: CGPoint {
set {
var newCenter = newValue
newCenter.y -= bounds.midY
super.center = newCenter
}
get {
return super.center
}
}
//setting it up
func setup() {
// setup this view's properties
self.backgroundColor = UIColor.clear
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(CustomCalloutView.calloutTapped)))
// And the subviews
self.addSubview(personImg)
self.addSubview(clockImg)
// Add Constraints to subviews
//Positioning the clock image
clockImg.topAnchor.constraint(equalTo: self.separatorLine.bottomAnchor, constant: spacing).isActive = true
clockImg.leftAnchor.constraint(equalTo: self.leftAnchor, constant: spacing).isActive = true
clockImg.rightAnchor.constraint(equalTo: self.timeLabel.leftAnchor, constant: -spacing / 2).isActive = true
clockImg.heightAnchor.constraint(equalToConstant: 20.0).isActive = true
//Positioning the person image
personImg.topAnchor.constraint(equalTo: self.timeLabel.bottomAnchor, constant: spacing / 2).isActive = true
personImg.leftAnchor.constraint(equalTo: self.leftAnchor, constant: spacing).isActive = true
personImg.rightAnchor.constraint(equalTo: self.peopleLabel.leftAnchor, constant: -spacing / 2).isActive = true
personImg.heightAnchor.constraint(equalToConstant: 20.0).isActive = true
}
func presentCallout(from rect: CGRect, in view: UIView, constrainedTo constrainedRect: CGRect, animated: Bool)
//presenting the view
}
func dismissCallout(animated: Bool) {
//dismissing the view
}
#objc func calloutTapped() {
//respond to the view being tapped
}
}
When the sheet is presented and then dismissed, it causes other UI elements that are coded in UIKit (like text buttons, for example) to have their text turn color to the systemBlue UIColor...any idea how to fix this?
There is not enough code provided to test, but the reason might be in
struct Button: View { // << this custom view named as standard Button !!!
#State isPresented = false
it is named the same as standard SwiftUI component Button so this can confuse rendering engine.
Try to rename this (and others if you practice this) to something unique to your app, like MyButton or CustomButton, SheetButton, etc.

UIScrollView not scrolling w/ programmatic autolayout constraints

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
]

UIScrollview doesn't work once subviews added

I'm new to working with ScrollViews, and I'm doing it all programatically. I must be missing something super simple, but when I have no subviews, the scrollview shows up properly and scrolls up and down. But any time I add any subviews, the whole thing refuses to show up at all.
class DetailedPostScrollView: UIScrollView {
let topLabel: UILabel = {
let label = UILabel()
label.text = "this is the top"
return label
}()
let bottomLabel: UILabel = {
let label = UILabel()
label.text = "this is the bottom"
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
contentSize = CGSize(width: frame.width, height: 2000)
alwaysBounceVertical = true
addSubviewUsingAutoLayout(topLabel, bottomLabel)
topLabel.centerXAnchor.constrain(to: self.centerXAnchor)
topLabel.widthAnchor.constrain(to: 200)
topLabel.heightAnchor.constrain(to: 50)
topLabel.topAnchor.constrain(to: self.topAnchor, with: 100)
bottomLabel.centerXAnchor.constrain(to: self.centerXAnchor)
bottomLabel.widthAnchor.constrain(to: 200)
bottomLabel.heightAnchor.constrain(to: 50)
bottomLabel.bottomAnchor.constrain(to: self.bottomAnchor)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
And in my viewController, I instantiate and add the scroll view
let detailedPostScrollView = DetailedPostScrollView(frame: UIScreen.main.bounds)
detailedPostScrollView.backgroundColor = UIColor.purple
view.addSubviewUsingAutoLayout(detailedPostScrollView)
Again, I"m sure it's something super simple but I checked out all the questions and tutorial videos and couldn't see where I"m going wrong. Thanks for your help.
Edit: It seems to work fine when I do it all programmatically from a viewcontroller, as follows :
let scrollView: UIScrollView = {
let sv = UIScrollView()
sv.contentSize = CGSize(width: view.frame.width, height: 2000)
sv.backgroundColor = UIColor.purple
return sv
}()
view.addSubviewUsingAutoLayout(scrollView)
scrollView.topAnchor.constrain(to: view.topAnchor, with: 100)
scrollView.leadingAnchor.constrain(to: view.leadingAnchor)
scrollView.trailingAnchor.constrain(to: view.trailingAnchor)
scrollView.bottomAnchor.constrain(to: view.bottomAnchor)
let topLabel: UILabel = {
let label = UILabel()
label.text = "this is the top"
return label
}()
scrollView.addSubviewUsingAutoLayout(topLabel)
topLabel.centerXAnchor.constrain(to: scrollView.centerXAnchor)
topLabel.widthAnchor.constrain(to: 200)
topLabel.heightAnchor.constrain(to: 50)
topLabel.topAnchor.constrain(to: scrollView.topAnchor)
Something is off when I create a custom scrollview and instantiate that in my vc.
Try this :
class ViewController : UIViewController , UIScrollViewDelegate{
var scrollView : UIScrollView! ;
var containerView = UIView();
var contentSize : CGSize {
get {
return CGSize(width: view.frame.width, height: 2000);
}
}
override func viewDidLoad() {
super.viewDidLoad();
setupScrollView();
setupItems();
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews();
self.scrollView.frame = self.view.bounds;
containerView.frame = CGRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height);
}
private func setupScrollView() {
scrollView = UIScrollView();
scrollView.delegate = self;
scrollView.backgroundColor = .white;
scrollView.contentSize = contentSize;
self.view.addSubview(scrollView);
containerView.backgroundColor = .purple;
self.scrollView.addSubview(containerView);
}
private func setupItems() {
let topButton = UIButton();
topButton.translatesAutoresizingMaskIntoConstraints = false;
topButton.setTitle("TOP BUTTON", for: .normal);
topButton.setTitleColor(.white, for: .normal);
self.containerView.addSubview(topButton);
let bottomTextField = UITextField();
bottomTextField.translatesAutoresizingMaskIntoConstraints = false;
bottomTextField.borderStyle = .roundedRect;
bottomTextField.placeholder = "BOTTOM TEXT FIELD";
self.containerView.addSubview(bottomTextField);
topButton.centerXAnchor.constraint(equalTo: containerView.centerXAnchor).isActive = true;
topButton.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 50).isActive = true;
topButton.widthAnchor.constraint(equalToConstant: 100).isActive = true;
topButton.heightAnchor.constraint(equalToConstant: 50).isActive = true;
bottomTextField.centerXAnchor.constraint(equalTo: containerView.centerXAnchor).isActive = true;
bottomTextField.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -50).isActive = true;
bottomTextField.widthAnchor.constraint(equalToConstant: 200).isActive = true;
bottomTextField.heightAnchor.constraint(equalToConstant: 40).isActive = true;
}
}
Set the scroll view frame to view bound in viewDidLayoutViews instead of using the layout , and add a container view as a subview for scroll view and set the size to same content size for scroll view again in viewDidLayoutViews so when ever user rotate the phone he get the correct width , and finally add all your views to container view .