it is unpleasant for me but I need some help with my UIScrollViews. They are both arranged subviews of a stackView on my MainVC.
The weird thing is that only one of them is showing content, although I used the same code for both scrollViews. The second problem is that they do not scroll, here is my code:
class HomeVC: UIViewController, UIScrollViewDelegate {
var views = [UIImageView]()
//StackView
let stackView = UIStackView()
let topView = UIScrollView()
let bottomView = UIScrollView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = MyColors.soft_pink
prepare_data()
print(views.count)
}
//MARK: - GUI
func setUpStackView() {
view.addSubview(stackView)
stackView.alignment = .center
stackView.axis = .vertical
stackView.distribution = .equalCentering
stackView.spacing = 5
stackView.addArrangedSubview(topView)
stackView.addArrangedSubview(bottomView)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20).isActive = true
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10).isActive = true
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10).isActive = true
stackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -25).isActive = true
setUpTopView()
setUpBottomView()
}
func setUpTopView() {
topView.delegate = self
topView.layer.cornerRadius = 25
topView.layer.masksToBounds = true
topView.layer.borderWidth = 10
topView.layer.borderColor = UIColor.white.cgColor
topView.contentMode = .scaleAspectFit
topView.showsHorizontalScrollIndicator = false
topView.isPagingEnabled = true
topView.contentSize = CGSize(width: topView.frame.width * CGFloat(views.count),height: topView.frame.height)
for i in 0..<views.count {
topView.addSubview(views[i])
views[i].frame = CGRect(x: topView.frame.width * CGFloat(i), y: 0, width: 350, height: 250)
views[i].layer.cornerRadius = 25
}
topView.translatesAutoresizingMaskIntoConstraints = false
topView.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: 10).isActive = true
topView.trailingAnchor.constraint(equalTo: stackView.trailingAnchor, constant: -10).isActive = true
topView.heightAnchor.constraint(equalToConstant: 250).isActive = true
}
func setUpBottomView() {
bottomView.delegate = self
bottomView.layer.cornerRadius = 25
bottomView.layer.masksToBounds = true
bottomView.layer.borderWidth = 10
bottomView.layer.borderColor = UIColor.white.cgColor
bottomView.contentMode = .scaleAspectFit
bottomView.showsHorizontalScrollIndicator = false
bottomView.isPagingEnabled = true
bottomView.contentSize = CGSize(width: bottomView.frame.width * CGFloat(views.count),height: bottomView.frame.height)
for i in 0..<views.count {
bottomView.addSubview(views[i])
views[i].frame = CGRect(x: bottomView.frame.width * CGFloat(i), y: 0, width: 350, height: 250)
views[i].layer.cornerRadius = 25
}
bottomView.translatesAutoresizingMaskIntoConstraints = false
bottomView.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: 10).isActive = true
bottomView.trailingAnchor.constraint(equalTo: stackView.trailingAnchor, constant: -10).isActive = true
bottomView.heightAnchor.constraint(equalToConstant: 250).isActive = true
}
func prepare_data() {
for x in 1...6 {
let woman = UIImage(named: "woman\(x)")
let womanView = UIImageView(image: woman)
womanView.contentMode = .scaleAspectFill
views.append(womanView)
}
setUpStackView()
}
}
Could someone please be so kind and tell me what I have wrong? Thank you in advance!
Try to debug by printing values. bottomView.frame.width was zero at initialisation, so update subviews in viewDidLayoutSubviews. There are more ways you can look for frame update detection.
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
DispatchQueue.main.async {
self.updateSubviewFrames()
}
}
func updateSubviewFrames() {
print(bottomView.frame)
bottomView.contentSize = CGSize(width: bottomView.frame.width * CGFloat(views.count),height: bottomView.frame.height)
for i in 0..<views.count {
views[i].frame = CGRect(x: bottomView.frame.width * CGFloat(i), y: 0, width: 350, height: 250)
views[i].layer.cornerRadius = 25
}
}
You've done a few things wrong...
First, because it's easy -- the reason you don't see anything in your Top scroll view is because you add your image views (from the views array) to topView, and then you add them to bottomView which removes them from topView!
So, you need one array of views for topView and an array of other views for bottomView.
Next, you are using auto-layout / constraints to size and position your stack view, then trying to use the frames of the stack view's arranged subviews -- for example:
bottomView.contentSize = CGSize(width: bottomView.frame.width * CGFloat(views.count),height: bottomView.frame.height)
but, that's all being done in functions called from viewDidLoad() when auto-layout has not yet configured the view frames.
You're also adding your scroll view's as arranged subviews of the stack view, but then constraining them to the stack view (which is not the way to do it):
topView.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: 10).isActive = true
As a side note: the easiest way to manage a paged scroll view is to embed the "pages" (your image views) in a horizontal stack view, setting the width of each view to the width of the scroll view's Frame Layout Guide (minus desired spacing).
Here's a modified version of your code to take a look at:
class HomeVC: UIViewController, UIScrollViewDelegate {
var topViews = [UIImageView]()
var botViews = [UIImageView]()
//StackView
let stackView = UIStackView()
let topView = UIScrollView()
let bottomView = UIScrollView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemPink // MyColors.soft_pink
prepare_data()
setUpStackView()
setUpTopAndBottomViews()
}
func prepare_data() {
// create 6 image views
// for BOTH Top and Bottom scroll views
// I'll assume you have "woman" and "man" images
for x in 1...6 {
let woman = UIImage(named: "woman\(x)")
let man = UIImage(named: "man\(x)")
let womanView = UIImageView(image: woman)
womanView.contentMode = .scaleAspectFill
topViews.append(womanView)
let manView = UIImageView(image: man)
manView.contentMode = .scaleAspectFill
botViews.append(manView)
}
}
func setUpStackView() {
// setup stack view
view.addSubview(stackView)
// .alignment should be .fill, not .center
//stackView.alignment = .center
stackView.alignment = .fill
stackView.axis = .vertical
// let's use .fillEqually instead of .equalCentering
//stackView.distribution = .equalCentering
stackView.distribution = .fillEqually
stackView.spacing = 5
stackView.addArrangedSubview(topView)
stackView.addArrangedSubview(bottomView)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20).isActive = true
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10).isActive = true
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10).isActive = true
stackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -25).isActive = true
}
func setUpTopAndBottomViews() {
// setup both scroll views with the same properties
[topView, bottomView].forEach { v in
v.delegate = self
v.layer.cornerRadius = 25
v.layer.masksToBounds = true
v.layer.borderWidth = 10
v.layer.borderColor = UIColor.white.cgColor
v.showsHorizontalScrollIndicator = false
v.isPagingEnabled = true
}
// let's use auto-layout here
// if you want horizontal paged scrolling, easiest route is to
// use a horizontal stack view
// create a stack view
let topStack = UIStackView()
topStack.translatesAutoresizingMaskIntoConstraints = false
topStack.spacing = 10
// add stack view to topView
topView.addSubview(topStack)
for i in 0..<topViews.count {
topStack.addArrangedSubview(topViews[i])
topViews[i].layer.cornerRadius = 25
// set view width and height equal to
// topView's Frame Layout Guide
// allowing for 5-pts "padding" on the sides
topViews[i].widthAnchor.constraint(equalTo: topView.frameLayoutGuide.widthAnchor, constant: -10.0).isActive = true
topViews[i].heightAnchor.constraint(equalTo: topView.frameLayoutGuide.heightAnchor).isActive = true
}
// now we'll set constraints on the stack view to
// topView's Content Layout Guide
NSLayoutConstraint.activate([
topStack.topAnchor.constraint(equalTo: topView.contentLayoutGuide.topAnchor),
topStack.leadingAnchor.constraint(equalTo: topView.contentLayoutGuide.leadingAnchor, constant: 5.0),
topStack.trailingAnchor.constraint(equalTo: topView.contentLayoutGuide.trailingAnchor, constant: -5.0),
topStack.bottomAnchor.constraint(equalTo: topView.contentLayoutGuide.bottomAnchor),
])
// same thing with the bottom scroll view
// create a new stack view
let botStack = UIStackView()
botStack.translatesAutoresizingMaskIntoConstraints = false
botStack.spacing = 10
// add stack view to bottomView
bottomView.addSubview(botStack)
for i in 0..<botViews.count {
botStack.addArrangedSubview(botViews[i])
botViews[i].layer.cornerRadius = 25
// set view width and height equal to
// bottomView's Frame Layout Guide
// allowing for 5-pts "padding" on the sides
botViews[i].widthAnchor.constraint(equalTo: bottomView.frameLayoutGuide.widthAnchor, constant: -10.0).isActive = true
botViews[i].heightAnchor.constraint(equalTo: bottomView.frameLayoutGuide.heightAnchor).isActive = true
}
// now we'll set constraints on the stack view to
// bottomView's Content Layout Guide
NSLayoutConstraint.activate([
botStack.topAnchor.constraint(equalTo: bottomView.contentLayoutGuide.topAnchor),
botStack.leadingAnchor.constraint(equalTo: bottomView.contentLayoutGuide.leadingAnchor, constant: 5.0),
botStack.trailingAnchor.constraint(equalTo: bottomView.contentLayoutGuide.trailingAnchor, constant: -5.0),
botStack.bottomAnchor.constraint(equalTo: bottomView.contentLayoutGuide.bottomAnchor),
])
}
}
I have a table view which should display comments with their replies. Whenever the replies are shown, there should be a margin on the left side so it is clear that a particular reply belongs to the comment above.
To achieve that, I tried following:
class CommentsTableViewCell: UITableViewCell {
// label containing name of comment writer
private func initSenderLabel() {
senderLabel.translatesAutoresizingMaskIntoConstraints = false
self.contentView.addSubview(senderLabel)
senderLabel.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: 18).isActive = true
senderLabel.widthAnchor.constraint(equalToConstant: 150).isActive = true
senderLabel.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: 6).isActive = true
senderLabel.heightAnchor.constraint(equalToConstant:14).isActive = true
senderLabel.font = .boldSystemFont(ofSize: 14)
senderLabel.adjustsFontSizeToFitWidth = true
senderLabel.textColor = .gray
}
// actual comment
private func initCommentLabel() {
commentLabel.translatesAutoresizingMaskIntoConstraints = false
self.contentView.addSubview(commentLabel)
commentLabel.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: 18).isActive = true
commentLabel.widthAnchor.constraint(equalToConstant: 209).isActive = true
commentLabel.topAnchor.constraint(equalTo: self.senderLabel.bottomAnchor, constant: 2).isActive = true
commentLabel.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor, constant: -29).isActive = true
commentLabel.font = .boldSystemFont(ofSize: 16)
commentLabel.textColor = .black
commentLabel.lineBreakMode = .byWordWrapping
commentLabel.numberOfLines = 0
}
// button to load replies
private func initLoadReplyButton() {
loadReplyButton.translatesAutoresizingMaskIntoConstraints = false
self.contentView.addSubview(loadReplyButton)
loadReplyButton.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: 50).isActive = true
loadReplyButton.widthAnchor.constraint(equalToConstant: 150).isActive = true
loadReplyButton.topAnchor.constraint(equalTo: commentLabel.bottomAnchor, constant: 0).isActive = true
loadReplyButton.heightAnchor.constraint(equalToConstant: 24).isActive = true
loadReplyButton.addTarget(self, action: #selector(loadReplyButton_touched), for: .touchDown)
loadReplyButton.setTitle(NSLocalizedString(LOAD_REPLIES_BUTTON_TITLE, comment: ""), for: .normal)
loadReplyButton.backgroundColor = .clear
loadReplyButton.setTitleColor(.darkGray, for: .normal)
}
// Sets up the cell using the comment data
func setup(comment: Comment, sender:String) {
// Assign comment
self.commentObject = comment
// Update UI depending if it is a comment or reply
if comment is CommentReply {
loadReplyButton.isHidden = true
contentView.layoutMargins = UIEdgeInsets(top: 0, left: 150, bottom: 0, right: 0)
}
else {
// Remove insets
loadReplyButton.isHidden = false
UIView.setAnimationsEnabled(false)
updateButtonTitle()
UIView.setAnimationsEnabled(true)
}
if !comment.hasReplies {
loadReplyButton.isHidden = true
}
// Update size layout
self.layoutIfNeeded()
}
However, the code above still shows me the replies as it does for the "normal" comments. How can I modify the position of the replies?
In my view hierarchy, I have a UIPageViewController (inside a container view). Underneath that is the UIPageControl and at the bottom is a stack view consisting of a text view and a button. I see the UIPageViewController and the stack view but not the UIPageControl. Any idea what I am doing wrong:
// Page view controller
introPageViewC = IntroPageViewC()
addChild(introPageViewC)
introPageViewC.view.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(introPageViewC.view)
NSLayoutConstraint.activate([
introPageViewC.view.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 0.0),
introPageViewC.view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 0.0),
introPageViewC.view.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 0.0),
introPageViewC.view.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: 0.0),
])
introPageViewC.didMove(toParent: self)
// Page view control
introPageControl.currentPageIndicatorTintColor = UIColor.orange
introPageControl.pageIndicatorTintColor = UIColor.lightGray.withAlphaComponent(0.8)
view.addSubview(introPageControl)
introPageControl.translatesAutoresizingMaskIntoConstraints = false
introPageControl.topAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 10).isActive = true
introPageControl.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
// Stack view
view.addSubview(stackView)
stackView.axis = .vertical
stackView.distribution = .fillEqually
stackView.spacing = 30
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.topAnchor.constraint(equalTo: introPageControl.bottomAnchor, constant: 10).isActive = true
stackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 40).isActive = true
stackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -40).isActive = true
Expected output:
Edit:
Attempt at adding to the stack view:
var allViews = [UIView]()
// Adding to stackview
introPageControl.currentPageIndicatorTintColor = UIColor.orange
introPageControl.pageIndicatorTintColor = UIColor.lightGray.withAlphaComponent(0.8)
allViews.append(introPageControl)
nameTextField = UITextField(frame: CGRect(x: 0, y: 0, width: 120, height: 40))
nameTextField.placeholder = "Mealplan name"
Utilities.styleTextField(nameTextField)
nameTextField.setPadding()
allViews.append(nameTextField)
let nextButton = UIButton(type: .system)
nextButton.frame = CGRect(x: 20, y: 20, width: 100, height: 40)
nextButton.setTitle("Next", for: .normal)
nextButton.titleLabel?.textColor = .white
nextButton.titleLabel?.font = UIFont(name: "NexaBold", size: 16)
Utilities.styleDefaultButton(nextButton)
nextButton.addTarget(self, action: #selector(tapSubmit(_:)), for: .touchUpInside)
allViews.append(nextButton)
errorLabel.frame = CGRect(x: 20, y: 20, width: 100, height: 40)
errorLabel.font = UIFont(name: "NexaBold", size: 16)
errorLabel.textColor = .systemRed
errorLabel.textAlignment = .center
errorLabel.alpha = 0
allViews.append(errorLabel)
for eachView in allViews {
stackView.addArrangedSubview(eachView)
}
introPageControl.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
I believe you meant centerXAnchor?
I think you've got the topAnchor all wrong. Right now, it seems to be constrained to the bottom of the containerView plus 10, which is just off the screen. I think you meant to say -10.
introPageControl.topAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -10).isActive = true
My swift code below goal is to add 2 image views every time. Ass you can in the gif below only one image view is being added. I just need to add 2 image views. The image views are lastImage and lastImage2. you can see only lastImage is being shown. It seems I can only add 1 imageview when func didclickadd is called.
import UIKit
class ViewController: UIViewController {
fileprivate var lastImage:UIImageView?
fileprivate var lastImage2:UIImageView?
fileprivate var mainViewBootom:NSLayoutConstraint?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
setupVIew()
}
override func viewDidAppear(_ animated: Bool) {
scrollView.contentSize = CGSize(width: view.frame.width, height: mainView.frame.height)
view.layoutIfNeeded()
}
//MARK: Components
let scrollView:UIScrollView = {
let sv = UIScrollView(frame: .zero)
return sv
}()
let mainView:UIView = {
let uv = UIView()
uv.backgroundColor = .white
return uv
}()
let btnAdd:UIButton = {
let btn = UIButton(type: .system)
btn.setTitle("Add", for: .normal)
return btn
}()
let textField:UITextField = {
let jake = UITextField()
return jake
}()
//MARK: Setup UI
func setupVIew() {
view.addSubview(scrollView)
view.addSubview(btnAdd)
view.addSubview(textField)
scrollView.translatesAutoresizingMaskIntoConstraints = false
btnAdd.translatesAutoresizingMaskIntoConstraints = false
textField.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
btnAdd.centerXAnchor.constraint(equalTo: view.centerXAnchor),
btnAdd.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -12),
btnAdd.widthAnchor.constraint(equalToConstant: 100),
btnAdd.heightAnchor.constraint(equalToConstant: 45),
//
textField.centerXAnchor.constraint(equalTo: view.centerXAnchor),
textField.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 25),
textField.widthAnchor.constraint(equalToConstant: 100),
textField.heightAnchor.constraint(equalToConstant: 45),
//
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: btnAdd.topAnchor , constant: -12),
])
btnAdd.addTarget(self, action: #selector(didClickedAdd), for: .touchUpInside)
scrollView.addSubview(mainView)
mainView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
mainView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
mainView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
mainView.topAnchor.constraint(equalTo: scrollView.topAnchor),
])
let imgView = UIImageView(frame: CGRect(x: 0, y: 0, width: 150, height: 100))
imgView.backgroundColor = .red
mainView.addSubview(imgView)
let samsam = UIImageView(frame: CGRect(x: 0, y: 200, width: 40, height: 100))
samsam.backgroundColor = .blue
mainView.addSubview(samsam)
imgView.translatesAutoresizingMaskIntoConstraints = false
imgView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
imgView.widthAnchor.constraint(equalToConstant: 150).isActive = true
imgView.heightAnchor.constraint(equalToConstant: 100).isActive = true
samsam.translatesAutoresizingMaskIntoConstraints = false
samsam.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
samsam.topAnchor.constraint(equalTo: imgView.bottomAnchor).isActive = true
samsam.widthAnchor.constraint(equalToConstant: 75).isActive = true
samsam.heightAnchor.constraint(equalToConstant: 100).isActive = true
if lastImage != nil {
imgView.topAnchor.constraint(equalTo: lastImage!.bottomAnchor , constant: 20).isActive = true
}else{
imgView.topAnchor.constraint(equalTo: mainView.topAnchor , constant: 12).isActive = true
}
lastImage = samsam
mainViewBootom = mainView.bottomAnchor.constraint(equalTo: lastImage!.bottomAnchor , constant: 12)
mainViewBootom!.isActive = true
}
#objc func didClickedAdd(){
let imgView = UIImageView(frame: CGRect(x: 20, y: 0, width: 30, height: 20))
imgView.backgroundColor = .orange
mainView.addSubview(imgView)
let ss = UIImageView(frame: CGRect(x: 0, y: 0, width: 40, height: 50))
imgView.backgroundColor = .green
mainView.addSubview(ss)
imgView.translatesAutoresizingMaskIntoConstraints = false
imgView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
imgView.widthAnchor.constraint(equalToConstant: 40).isActive = true
imgView.heightAnchor.constraint(equalToConstant: 60).isActive = true
ss.translatesAutoresizingMaskIntoConstraints = false
ss.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = false
ss.widthAnchor.constraint(equalToConstant: 80).isActive = true
ss.heightAnchor.constraint(equalToConstant: 90).isActive = true
if lastImage != nil {
ss.topAnchor.constraint(equalTo: imgView.topAnchor , constant: 20).isActive = true
imgView.topAnchor.constraint(equalTo: lastImage!.bottomAnchor , constant: 50).isActive = true
}else{
imgView.topAnchor.constraint(equalTo: mainView.topAnchor , constant: 10).isActive = true
ss.bottomAnchor.constraint(equalTo: imgView.bottomAnchor , constant: 25).isActive = true
}
lastImage = imgView
lastImage2 = ss
mainView.removeConstraint(mainViewBootom!)
mainViewBootom = mainView.bottomAnchor.constraint(equalTo: lastImage2!.bottomAnchor , constant: 40)
mainViewBootom!.isActive = true
view.layoutIfNeeded()
scrollView.contentSize = CGSize(width: view.frame.width, height: mainView.frame.height)
view.layoutIfNeeded()
}
}
Couple notes...
With proper constraint setup, auto-layout handles the UIScrollView content size all by itself. No need to ever set scrollView.contentSize = ...
You have several instances of adding a subview (image view) to your mainView, which is a subview of your scroll view, but then you add constraints from that subview to your controller's view. Make sure you are constraining elements to the proper other elements.
Here's your code, with commented changes:
class BenViewController: UIViewController {
fileprivate var lastImage:UIImageView?
// 1) don't need this
// fileprivate var lastImage2:UIImageView?
fileprivate var mainViewBootom:NSLayoutConstraint?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
setupVIew()
}
// 2) don't need this
// override func viewDidAppear(_ animated: Bool) {
// scrollView.contentSize = CGSize(width: view.frame.width, height: mainView.frame.height)
// view.layoutIfNeeded()
// }
//MARK: Components
let scrollView:UIScrollView = {
let sv = UIScrollView(frame: .zero)
return sv
}()
let mainView:UIView = {
let uv = UIView()
uv.backgroundColor = .white
return uv
}()
let btnAdd:UIButton = {
let btn = UIButton(type: .system)
btn.setTitle("Add", for: .normal)
return btn
}()
let textField:UITextField = {
let jake = UITextField()
return jake
}()
//MARK: Setup UI
func setupVIew() {
view.addSubview(scrollView)
view.addSubview(btnAdd)
view.addSubview(textField)
scrollView.translatesAutoresizingMaskIntoConstraints = false
btnAdd.translatesAutoresizingMaskIntoConstraints = false
textField.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
btnAdd.centerXAnchor.constraint(equalTo: view.centerXAnchor),
btnAdd.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -12),
btnAdd.widthAnchor.constraint(equalToConstant: 100),
btnAdd.heightAnchor.constraint(equalToConstant: 45),
//
textField.centerXAnchor.constraint(equalTo: view.centerXAnchor),
textField.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 25),
textField.widthAnchor.constraint(equalToConstant: 100),
textField.heightAnchor.constraint(equalToConstant: 45),
//
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: btnAdd.topAnchor , constant: -12),
])
btnAdd.addTarget(self, action: #selector(didClickedAdd), for: .touchUpInside)
scrollView.addSubview(mainView)
mainView.translatesAutoresizingMaskIntoConstraints = false
// 3) change this:
// NSLayoutConstraint.activate([
// mainView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
// mainView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
// mainView.topAnchor.constraint(equalTo: scrollView.topAnchor),
// ])
//
// to this
NSLayoutConstraint.activate([
mainView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
mainView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
mainView.topAnchor.constraint(equalTo: scrollView.topAnchor),
mainView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
mainView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
])
// end of change 3)
let imgView = UIImageView(frame: CGRect(x: 0, y: 0, width: 150, height: 100))
imgView.backgroundColor = .red
mainView.addSubview(imgView)
let samsam = UIImageView(frame: CGRect(x: 0, y: 200, width: 40, height: 100))
samsam.backgroundColor = .blue
mainView.addSubview(samsam)
imgView.translatesAutoresizingMaskIntoConstraints = false
// 4) change view.centerXAnchor to mainView.centerXAnchor
// imgView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
imgView.centerXAnchor.constraint(equalTo: mainView.centerXAnchor).isActive = true
imgView.widthAnchor.constraint(equalToConstant: 150).isActive = true
imgView.heightAnchor.constraint(equalToConstant: 100).isActive = true
samsam.translatesAutoresizingMaskIntoConstraints = false
// 5) change view.centerXAnchor to mainView.centerXAnchor
// samsam.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
samsam.centerXAnchor.constraint(equalTo: mainView.centerXAnchor).isActive = true
samsam.topAnchor.constraint(equalTo: imgView.bottomAnchor).isActive = true
samsam.widthAnchor.constraint(equalToConstant: 75).isActive = true
samsam.heightAnchor.constraint(equalToConstant: 100).isActive = true
if lastImage != nil {
imgView.topAnchor.constraint(equalTo: lastImage!.bottomAnchor , constant: 20).isActive = true
}else{
imgView.topAnchor.constraint(equalTo: mainView.topAnchor , constant: 12).isActive = true
}
lastImage = samsam
mainViewBootom = mainView.bottomAnchor.constraint(equalTo: lastImage!.bottomAnchor , constant: 12)
mainViewBootom!.isActive = true
}
#objc func didClickedAdd(){
let imgView = UIImageView(frame: CGRect(x: 20, y: 0, width: 30, height: 20))
imgView.backgroundColor = .orange
mainView.addSubview(imgView)
let ss = UIImageView(frame: CGRect(x: 0, y: 0, width: 40, height: 50))
// 6) typo or copy/paste mistake
// imgView.backgroundColor = .green
ss.backgroundColor = .green
mainView.addSubview(ss)
imgView.translatesAutoresizingMaskIntoConstraints = false
// 7) change view.centerXAnchor to mainView.centerXAnchor
// imgView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
imgView.centerXAnchor.constraint(equalTo: mainView.centerXAnchor).isActive = true
imgView.widthAnchor.constraint(equalToConstant: 40).isActive = true
imgView.heightAnchor.constraint(equalToConstant: 60).isActive = true
ss.translatesAutoresizingMaskIntoConstraints = false
// 8) change view.leadingAnchor to mainView.leadingAnchor
// ss.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = false
ss.leadingAnchor.constraint(equalTo: mainView.leadingAnchor).isActive = false
ss.widthAnchor.constraint(equalToConstant: 80).isActive = true
ss.heightAnchor.constraint(equalToConstant: 90).isActive = true
// 9) always need to do this ... but did you mean imgView.bottomAnchor?
ss.topAnchor.constraint(equalTo: imgView.topAnchor , constant: 20).isActive = true
if lastImage != nil {
// 9a) instead of only here
//ss.topAnchor.constraint(equalTo: imgView.topAnchor , constant: 20).isActive = true
imgView.topAnchor.constraint(equalTo: lastImage!.bottomAnchor , constant: 50).isActive = true
}else{
imgView.topAnchor.constraint(equalTo: mainView.topAnchor , constant: 10).isActive = true
}
// 10) always need to do this
// deactivate bottom constraint
mainViewBootom?.isActive = false
lastImage = ss
mainViewBootom = mainView.bottomAnchor.constraint(equalTo: lastImage!.bottomAnchor, constant: 40)
mainViewBootom?.isActive = true
// 11) don't need any of this
// lastImage = imgView
// lastImage2 = ss
// mainView.removeConstraint(mainViewBootom!)
//
//
// mainViewBootom = mainView.bottomAnchor.constraint(equalTo: lastImage2!.bottomAnchor , constant: 40)
//
//
//
//
// mainViewBootom!.isActive = true
// view.layoutIfNeeded()
//
// scrollView.contentSize = CGSize(width: view.frame.width, height: mainView.frame.height)
// view.layoutIfNeeded()
}
}
Use Xcode’s “view debugger” (the button is circled in red in my screen snapshot below) and you’ll see what’s going on:
Your ss view has no background color. Note, that when you created that view, you accidentally reset the imgView background color a second time rather than setting the ss.backgroundColor.
Fix that and you’ll see your both imgView and ss:
The view debugger is your best friend when trying to diagnose issues like this. Now, obviously, the green view probably isn’t where you intended it, but you should now be able to see it and diagnose that issue very easily.
All of this having been said, a few observations:
You’re making life much harder than you need to. If you just set the constraints for the scroll view and a stack view within that scroll view, you then only need to add an arranged subview. For example:
#objc func didTapButton(_ sender: UIButton) {
stackView.addArrangedSubview(randomView())
stackView.addArrangedSubview(randomView())
}
Note, once the stack view and scroll view have been set up (see below), then you don’t need to mess around with contentSize or constraints for these subviews at all (other than the widthAnchor and heightAnchor). The auto layout engine, combined with the constraints between the stack view and the scroll view, will take care of everything for you.
So, a full working example:
class ViewController: UIViewController {
let scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
return scrollView
}()
let stackView: UIStackView = {
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.alignment = .center
stackView.spacing = 10
return stackView
}()
let button: UIButton = {
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("Add", for: .normal)
button.addTarget(self, action: #selector(didTapButton(_:)), for: .touchUpInside)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
configure()
}
}
// MARK: - Actions
extension ViewController {
#objc func didTapButton(_ sender: UIButton) {
stackView.addArrangedSubview(randomView())
stackView.addArrangedSubview(randomView())
}
}
// MARK: - Private utility methods
private extension ViewController {
func configure() {
view.addSubview(scrollView)
view.addSubview(button)
scrollView.addSubview(stackView)
NSLayoutConstraint.activate([
// define frame of `scrollView`
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: button.topAnchor),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
// define frame of `button`
button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
button.bottomAnchor.constraint(equalTo: view.bottomAnchor),
// define contentSize of `scrollView` based upon size of `stackView`
stackView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
stackView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
stackView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
// but define width of `stackView` relative to the _main view_
stackView.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor)
])
button.setContentHuggingPriority(.required, for: .vertical)
}
func randomView() -> UIView {
let widthRange = view.bounds.width * 0.1 ... view.bounds.width * 0.9
let heightRange = view.bounds.width * 0.1 ... view.bounds.width * 0.25
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
view.widthAnchor.constraint(equalToConstant: .random(in: widthRange)),
view.heightAnchor.constraint(equalToConstant: .random(in: heightRange))
])
view.backgroundColor = UIColor(red: .random(in: 0.25...1), green: .random(in: 0.25...1), blue: .random(in: 0.25...1), alpha: 1)
return view
}
}
Even better, I’d personally set up the scroll view, stack view, button, and all the associated constraints in Interface Builder, and then that hairy configure method in my example goes away completely. It’s fun to learn how to create views programmatically, but in real-world projects, it’s rarely the most productive way to do it. Do programmatic views where needed (e.g. adding arranged subviews to the stack view on the click of a button), but otherwise, for those views that should be there when you first run the app, Interface Builder is worth considering.
E.g. It dramatically reduces the amount of code above, leaving us simply with:
class ViewController: UIViewController {
#IBOutlet weak var scrollView: UIScrollView!
#IBOutlet weak var stackView: UIStackView!
#IBAction func didTapButton(_ sender: UIButton) {
stackView.addArrangedSubview(randomView())
stackView.addArrangedSubview(randomView())
}
}
// MARK: - Private utility methods
private extension ViewController {
func randomView() -> UIView { ... }
}
Clearly, it takes a while to get used to designing views and configuring constraints in IB, but it’s worth the effort. It distills our code down the the bare essentials.
In your code, you’re setting frames for these image views and then setting translatesAutoresizingMaskIntoConstraints. There’s absolutely no point in setting the frame in that case, because translatesAutoresizingMaskIntoConstraints says “ignore my frame, use constraints instead.”
I’m assuming you’re doing all of this just to become familiar with scroll views, but it’s worth noting that, especially when adding lots of image views, that the scroll view is an inherently inefficient approach.
For example, let’s say you’ve added 100 image views, but you can see only 8 at a time. Do you really want to hold all 100 image views in memory at the same time? No.
But, UITableView, which is a subclass of UIScrollView, takes care of this. You end up only keeping the currently visible image views in memory. It’s a far better approach.
This is especially true when you start using actual UIImage objects, because they require a lot of memory. We get lulled into a sense of security, looking at reasonably sized PNG/JPG assets, but when they’re loaded into memory, they’re uncompressed and require a disproportionate amount of memory.
I have created a ScrollView and a ImageView:
let scrollView: UIScrollView = {
let scroll = UIScrollView()
scroll.backgroundColor = UIColor.red
scroll.contentSize.height = 1234
scroll.translatesAutoresizingMaskIntoConstraints = false
return scroll
}()
let filmImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.backgroundColor = UIColor.blue
imageView.clipsToBounds = true
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
And I have added these to my view with constraints, as followed:
self.view.addSubview(scrollView)
scrollView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0).isActive = true
scrollView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0).isActive = true
scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0).isActive = true
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true
scrollView.addSubview(filmImageView)
filmImageView.widthAnchor.constraint(equalToConstant: self.view.frame.width).isActive = true
filmImageView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 0).isActive = true
filmImageView.heightAnchor.constraint(equalToConstant: 350).isActive = true
Everything works, except, on the iPhone X only, the imageView doesn't sit to the top of the screen. See the image below. (I made the scrollview background colour red so you can see)
I have tried adding the code to my scrollView.
scroll.contentInset = UIEdgeInsets.zero
scroll.scrollIndicatorInsets = UIEdgeInsets.zero
scroll.contentOffset = CGPoint(x: 0.0, y: 0.0)
Along with:
self.automaticallyAdjustsScrollViewInsets = false
But it makes no change. I have been unable to find anything on the internet on how to get my image to sit to the top of the scrollview for the iPhone X. All other iPhone devices display this fine.
for the scrollView:
setting contentInsets to "never" instead of "always" solved the problem for me