UIImageView cuts inside UIScrollView - ios

I'm trying to create UIScrollView With UIStackView that contains multiple UIImageView with this code:
let stackView = UIStackView(frame: .zero)
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.backgroundColor = .green
scrollView.showsHorizontalScrollIndicator = false
scrollView.delegate = self
self.view.addSubview(scrollView)
scrollView.anchor(top: textSV.bottomAnchor, leading: self.view.safeAreaLayoutGuide.leadingAnchor, bottom: anotherView?.topAnchor, trailing: self.view.safeAreaLayoutGuide.trailingAnchor)
scrollView.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width).isActive = true
scrollView.contentInsetAdjustmentBehavior = .never
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.distribution = .equalSpacing
stackView.spacing = 0
scrollView.addSubview(stackView)
stackView.fillSuperview()
for _ in 1...8 {
let pageView = UIImageView(image: UIImage(named: "iphone12mockup"))
pageView.clipsToBounds = true
pageView.contentMode = .scaleAspectFit
pageView.translatesAutoresizingMaskIntoConstraints = false
stackView.addArrangedSubview(pageView)
pageView.anchor(top: stackView.topAnchor, leading: nil, bottom: stackView.bottomAnchor, trailing: nil)
}
The problem is that the UIImageView does not resize to scaleAspectFit and it looks like this(Can't see full image):
EDIT
let img = UIImage(named: "iphone12mockup")
let width = img?.size.width
let pageView = UIImageView(image: img)
pageView.clipsToBounds = true
pageView.contentMode = .scaleAspectFit
pageView.translatesAutoresizingMaskIntoConstraints = false
stackView.addArrangedSubview(pageView)
pageView.anchor(top: stackView.topAnchor, leading: nil, bottom: stackView.bottomAnchor, trailing: nil)
pageView.widthAnchor.constraint(equalToConstant: width!).isActive = true

You want to make use of the scroll view's Content and Frame Layout Guides...
constrain all 4 sides of the stack view to the scroll view's Content Layout Guide
constrain the stack view's Height to the scroll view's Frame Layout Guide
for each image view you add to the stack view:
constrain the image view's Width to the scroll view's Frame Layout Guide
Here is a complete example:
class ViewController: UIViewController, UIScrollViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()
// just trying to include what you've shown
let textSV = UILabel()
textSV.backgroundColor = .yellow
textSV.text = "textSV"
textSV.textAlignment = .center
let anotherView = UILabel()
anotherView.backgroundColor = .cyan
anotherView.text = "anotherView"
anotherView.textAlignment = .center
[textSV, anotherView].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
view.addSubview($0)
}
// respect safe area
let safeG = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
textSV.topAnchor.constraint(equalTo: safeG.topAnchor),
textSV.leadingAnchor.constraint(equalTo: safeG.leadingAnchor),
textSV.trailingAnchor.constraint(equalTo: safeG.trailingAnchor),
textSV.heightAnchor.constraint(equalToConstant: 60.0),
anotherView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor),
anotherView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor),
anotherView.bottomAnchor.constraint(equalTo: safeG.bottomAnchor),
anotherView.heightAnchor.constraint(equalToConstant: 60.0),
])
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.backgroundColor = .green
scrollView.showsHorizontalScrollIndicator = false
scrollView.delegate = self
view.addSubview(scrollView)
// use a stack view to hold and arrange the scrollView's subviews
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
// add the stackView to the scrollView
scrollView.addSubview(stackView)
// use scrollView's Content Layout Guide to define scrollable content
let layoutG = scrollView.contentLayoutGuide
// use scrollView's Frame Layout Guide to define content height (since you want horizontal scrolling)
let frameG = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
// constrain scrollView Top to textSV Bottom
scrollView.topAnchor.constraint(equalTo: textSV.bottomAnchor),
// constrain scrollView Leading/Trailing to safe area
scrollView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor),
// constrain scrollView Bottom to anotherView Top
scrollView.bottomAnchor.constraint(equalTo: anotherView.topAnchor),
// constrain all 4 sides of the stackView to scrollView's Content Layout Guide
stackView.topAnchor.constraint(equalTo: layoutG.topAnchor),
stackView.bottomAnchor.constraint(equalTo: layoutG.bottomAnchor),
stackView.leadingAnchor.constraint(equalTo: layoutG.leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: layoutG.trailingAnchor),
// constrain stackView's height to scrollView's Frame Layout Guide height
stackView.heightAnchor.constraint(equalTo: frameG.heightAnchor),
])
// add imageViews to the stack view
for _ in 1...8 {
let pageView = UIImageView(image: UIImage(named: "iphone12mockup"))
//let pageView = UIImageView(image: UIImage(named: "sample"))
// set image view background color so you can
// see its frame (since the image will be aspect-fit scaled)
pageView.backgroundColor = .systemYellow
pageView.contentMode = .scaleAspectFit
// add it to the stack view
stackView.addArrangedSubview(pageView)
// constrain its Width to scrollView's Frame Layout Guide Width
pageView.widthAnchor.constraint(equalTo: frameG.widthAnchor).isActive = true
}
}
}
It will look like this on startup (on an iPhone 8):
and after scrolling a little to the right:
Note that since you want the image view set to Aspect Fit, I gave the "pageView" image views a background color of .systemYellow so you can see that the imageView frame fills the scroll view frame width and height.
Edit -- if you want the images to be proportional to their height, without "empty space on the sides," you need to set the image view width constraint proportional to its height, based on the image size.
Replace the "add image views" loop with this:
// add imageViews to the stack view
for _ in 1...8 {
guard let img = UIImage(named: "iphone12mockup") else {
fatalError("Could not load image!")
}
let pageView = UIImageView()
pageView.image = img
pageView.contentMode = .scaleToFill
// add it to the stack view
stackView.addArrangedSubview(pageView)
// constrain its Width proportional to the image height
pageView.widthAnchor.constraint(equalTo: pageView.heightAnchor, multiplier: img.size.width / img.size.height).isActive = true
}
and the output will be:
and after scrolling a little to the right:

Related

Issue with scrollview + stackview

Problem:
I want my footer UIStackView to hug its content when views are laid out, and take priority over the UIScrollView . Currently the header UIStackView and main body UIScrollView hug its contents, causing the footer UIStackView to expand, therefore leaving a lot of space below its contents and not looking like it's not pinned to the bottom. I would like the header(UIStackView) and footer(UIStackView) to hug its contents, and the main body(UIScrollView) to expand as needed.
Platform specs:
iOS 15
Xcode 13.1
Context:
I have a UIViewController with the following view hierarchy
UIViewController
-UIView
-UIStackView(header)
-ScrollView(scrollable main body)
-UIView
-UIStackView
-UIStackView(footer)
Requirements for header and footer:
stay on screen all the time
Constraints:
self.view.addSubview(self.headerStackView)
NSLayoutConstraint.activate([
self.headerStackView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
self.headerStackView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
self.headerStackView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor)
])
self.view.addSubview(self.scrollView)
NSLayoutConstraint.activate([
self.scrollView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
self.scrollView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
self.scrollView.topAnchor.constraint(equalTo: self.headerStackView.bottomAnchor)
])
self.view.addSubview(self.footerStackView)
NSLayoutConstraint.activate([
self.footerStackView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
self.footerStackView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
self.footerStackView.topAnchor.constraint(equalTo: self.scrollView.bottomAnchor),
self.footerStackView.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor)
])
You can do this by constraining the Top of the footer view to the Bottom of the scroll view's content, but with .priority = .defaultLow, then constrain the Bottom of the footer view to less-than-or-equal-to the Bottom of the scroll view's frame.
Here's how it can look...
yellow is the Header Stack View
blue is the scroll view
light-gray is the scroll content
green is the footer view
Header and Scroll views are siblings -- subviews of view
Content and Footer views are siblings -- subviews of scrollView
A quick example:
class FooterVC: UIViewController {
let scrollView = UIScrollView()
let headerStack = UIStackView()
let footerStack = UIStackView()
let scrollContentLabel = UILabel()
var numLines: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
// MARK: setup the header stack view with add/remove buttons
let addButton = UIButton()
addButton.setTitle("Add", for: [])
addButton.setTitleColor(.white, for: .normal)
addButton.setTitleColor(.lightGray, for: .highlighted)
addButton.backgroundColor = .systemRed
let removeButton = UIButton()
removeButton.setTitle("Remove", for: [])
removeButton.setTitleColor(.white, for: .normal)
removeButton.setTitleColor(.lightGray, for: .highlighted)
removeButton.backgroundColor = .systemRed
headerStack.axis = .horizontal
headerStack.alignment = .center
headerStack.spacing = 20
headerStack.backgroundColor = .systemYellow
// a couple re-usable objects
var vSpacer: UIView!
vSpacer = UIView()
vSpacer.widthAnchor.constraint(equalToConstant: 16.0).isActive = true
headerStack.addArrangedSubview(vSpacer)
headerStack.addArrangedSubview(addButton)
headerStack.addArrangedSubview(removeButton)
vSpacer = UIView()
vSpacer.widthAnchor.constraint(equalToConstant: 16.0).isActive = true
headerStack.addArrangedSubview(vSpacer)
// MARK: setup the footer stack view
footerStack.axis = .vertical
footerStack.spacing = 8
footerStack.backgroundColor = .systemGreen
["Footer Stack View", "with Two Labels"].forEach { str in
let vLabel = UILabel()
vLabel.text = str
vLabel.textAlignment = .center
vLabel.font = .systemFont(ofSize: 24.0, weight: .regular)
vLabel.textColor = .yellow
footerStack.addArrangedSubview(vLabel)
}
// MARK: setup scroll content
scrollContentLabel.font = .systemFont(ofSize: 44.0, weight: .light)
scrollContentLabel.numberOfLines = 0
scrollContentLabel.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
// so we can see the scroll view
scrollView.backgroundColor = .systemBlue
[scrollContentLabel, footerStack].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(v)
}
[headerStack, scrollView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
}
let g = view.safeAreaLayoutGuide
let cg = scrollView.contentLayoutGuide
let fg = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
// header stack at top of view
headerStack.topAnchor.constraint(equalTo: g.topAnchor),
headerStack.leadingAnchor.constraint(equalTo: g.leadingAnchor),
headerStack.trailingAnchor.constraint(equalTo: g.trailingAnchor),
headerStack.heightAnchor.constraint(equalToConstant: 72.0),
// make buttons equal widths
addButton.widthAnchor.constraint(equalTo: removeButton.widthAnchor),
// scroll view Top to header stack Bottom
scrollView.topAnchor.constraint(equalTo: headerStack.bottomAnchor),
// other 3 sides
scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
// scroll content...
// let's inset the content label 12-points on each side
// to make it easier to see the framing
scrollContentLabel.leadingAnchor.constraint(equalTo: cg.leadingAnchor, constant: 12.0),
scrollContentLabel.trailingAnchor.constraint(equalTo: cg.trailingAnchor, constant: -12.0),
scrollContentLabel.widthAnchor.constraint(equalTo: fg.widthAnchor, constant: -24.0),
// top and bottom to content layout guide
scrollContentLabel.topAnchor.constraint(equalTo: cg.topAnchor, constant: 0.0),
scrollContentLabel.bottomAnchor.constraint(equalTo: cg.bottomAnchor, constant: 0.0),
// footer stack view - leading/trailing to frame layout guide
footerStack.leadingAnchor.constraint(equalTo: fg.leadingAnchor),
footerStack.trailingAnchor.constraint(equalTo: fg.trailingAnchor),
])
var tmpConstraint: NSLayoutConstraint!
// now, we want the footer to stick to the bottom of the content,
// but allow auto-layout to break the constraint when needed
tmpConstraint = footerStack.topAnchor.constraint(equalTo: scrollContentLabel.bottomAnchor)
tmpConstraint.priority = .defaultLow
tmpConstraint.isActive = true
// and we want the footer to stop at the bottom of the scroll view frame
// default is .required, but we'll set it here for emphasis
tmpConstraint = footerStack.bottomAnchor.constraint(lessThanOrEqualTo: fg.bottomAnchor)
tmpConstraint.priority = .required
tmpConstraint.isActive = true
// actions for the buttons
addButton.addTarget(self, action: #selector(addContent(_:)), for: .touchUpInside)
removeButton.addTarget(self, action: #selector(removeContent(_:)), for: .touchUpInside)
// add the first line to the content
addContent(nil)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// we need to set the bottom inset of the scroll view content
// so it can scroll up above the footer stack view
let h: CGFloat = footerStack.frame.height
scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: h, right: 0)
}
#objc func addContent(_ sender: Any?) {
// add another line of text to the content
numLines += 1
scrollContentLabel.text = (1...numLines).map({"Line \($0)"}).joined(separator: "\n")
// scroll newly added line into view
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: {
let r = CGRect(x: 0, y: self.scrollView.contentSize.height - 1.0, width: 1.0, height: 1.0)
self.scrollView.scrollRectToVisible(r, animated: true)
})
}
#objc func removeContent(_ sender: Any?) {
numLines -= 1
numLines = max(1, numLines)
scrollContentLabel.text = (1...numLines).map({"Line \($0)"}).joined(separator: "\n")
}
}
and it will look like this when running:
As we add content (in this case, just adding more lines to a label), it will "push down" the footer view until it hits the bottom of the scroll view's frame... at which point we can scroll behind the footer view.
It turns out thew view within the footer stackview was not constrained properly. Adding the missing constraint fixed the issue.

(Swift 5) UIScrollView scrolls but none of the content scrolls (video included)

I'm trying to learn to build views without storyboard. I tried to build a scrollview. On that scrollview is a UISearchBar, a UIImageView with an image and a UILabel. It works but none of the content moves. The content is all just frozen in place like no matter how far I scroll the search bar will always be on top of the page. and the image on the bottom. I've attached a video to show what I mean. There's also a problem because none of the content is where I want it to be but that's another problem. I realize this is probably because I don't know enough about constraints and autolayout and building views without storyboards.
Here's the video
class HomePageViewController: UIViewController {
var searchedText: String = ""
let label = UILabel()
let searchBar: UISearchBar = {
let searchBar = UISearchBar()
searchBar.placeholder = "Where are you going?"
searchBar.translatesAutoresizingMaskIntoConstraints = false
searchBar.barTintColor = .systemCyan
searchBar.searchTextField.backgroundColor = .white
searchBar.layer.cornerRadius = 5
return searchBar
}()
let homeImage: UIImageView = {
let homeImage = UIImageView()
homeImage.translatesAutoresizingMaskIntoConstraints = false
homeImage.clipsToBounds = true
return homeImage
}()
let scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.backgroundColor = .systemMint
scrollView.contentSize = CGSize(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height * 30)
return scrollView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemPink
// setupLayout()
// tried this here doesn't do anything for me
}
func setupLayout() {
view.addSubview(scrollView)
self.scrollView.addSubview(searchBar)
homeImage.image = UIImage(named: "Treehouse")
self.scrollView.addSubview(homeImage)
label.text = "Inspiration for your next trip..."
self.scrollView.addSubview(label)
// not sure where this label is being added I want it to be underneath the image but it isn't t
let safeG = view.safeAreaLayoutGuide
let viewFrame = view.bounds
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: -10),
scrollView.leftAnchor.constraint(equalTo: view.leftAnchor),
scrollView.rightAnchor.constraint(equalTo: view.rightAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
searchBar.topAnchor.constraint(equalTo: safeG.topAnchor, constant: 50.0),
searchBar.widthAnchor.constraint(equalTo: safeG.widthAnchor, multiplier: 0.9),
searchBar.centerXAnchor.constraint(equalTo: safeG.centerXAnchor),
homeImage.topAnchor.constraint(equalTo: safeG.topAnchor, constant: 150),
homeImage.widthAnchor.constraint(equalTo: safeG.widthAnchor, multiplier: 1.1),
homeImage.centerXAnchor.constraint(equalTo: safeG.centerXAnchor),
homeImage.heightAnchor.constraint(equalToConstant: viewFrame.height/2),
label.topAnchor.constraint(equalTo: homeImage.bottomAnchor, constant: 100)
])
// was doing all this in viewDidLayoutSubviews but not sure if this is better place for it
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
setupLayout()
// tried this in viewDidLoad() and it didn't solve it.
}
}
any help would be appreciated
First, when constraining subviews in a UIScrollView, you should constrain them to the scroll view's Content Layout Guide. You're constraining them to the view's safe area layout guide, so they're never going to go anywhere.
Second, it's difficult to center subviews in a scroll view, because the scroll view can scroll both horizontally and vertically. So it doesn't really have a "center."
You can either put subviews in a stack view, or, quite common, use a UIView as a "content" view to hold the subviews. If you constrain that content view's Width to the scroll view's Frame Layout Guide width, you can then horizontally center the subviews.
Third, it can be very helpful to comment your constraints, so you know exactly what you expect them to do.
Here's a modified version of your posted code:
class HomePageViewController: UIViewController {
var searchedText: String = ""
let label: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let searchBar: UISearchBar = {
let searchBar = UISearchBar()
searchBar.placeholder = "Where are you going?"
searchBar.translatesAutoresizingMaskIntoConstraints = false
searchBar.barTintColor = .systemCyan
searchBar.searchTextField.backgroundColor = .white
searchBar.layer.cornerRadius = 5
return searchBar
}()
let homeImage: UIImageView = {
let homeImage = UIImageView()
homeImage.translatesAutoresizingMaskIntoConstraints = false
homeImage.clipsToBounds = true
return homeImage
}()
let scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.backgroundColor = .systemMint
// don't do this
//scrollView.contentSize = CGSize(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height * 30)
return scrollView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemPink
setupLayout()
}
func setupLayout() {
view.addSubview(scrollView)
//homeImage.image = UIImage(named: "Treehouse")
homeImage.image = UIImage(named: "natureBKG")
label.text = "Inspiration for your next trip..."
// let's use a UIView to hold the "scroll content"
let contentView = UIView()
contentView.translatesAutoresizingMaskIntoConstraints = false
// give it a green background so we can see it
contentView.backgroundColor = .green
contentView.addSubview(searchBar)
contentView.addSubview(homeImage)
contentView.addSubview(label)
scrollView.addSubview(contentView)
let safeG = view.safeAreaLayoutGuide
let svContentG = scrollView.contentLayoutGuide
let svFrameG = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
// constrain scrollView to all 4 sides of view
// (generally, constrain to safe-area, but this is what you had)
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
scrollView.leftAnchor.constraint(equalTo: view.leftAnchor),
scrollView.rightAnchor.constraint(equalTo: view.rightAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
// constrain contentView to all 4 sides of scroll view's Content Layout Guide
contentView.topAnchor.constraint(equalTo: svContentG.topAnchor, constant: 0.0),
contentView.leadingAnchor.constraint(equalTo: svContentG.leadingAnchor, constant: 0.0),
contentView.trailingAnchor.constraint(equalTo: svContentG.trailingAnchor, constant: 0.0),
contentView.bottomAnchor.constraint(equalTo: svContentG.bottomAnchor, constant: 0.0),
// constrain contentView Width equal to scroll view's Frame Layout Guide Width
contentView.widthAnchor.constraint(equalTo: svFrameG.widthAnchor),
// constrain searchBar Top to contentView Top + 50
searchBar.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 50.0),
// constrain searchBar Width to 90% of contentView Width
searchBar.widthAnchor.constraint(equalTo: contentView.widthAnchor, multiplier: 0.9),
// constrain searchBar centerX to contentView centerX
searchBar.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
// constrain homeImage Top to searchBar Bottom + 40
homeImage.topAnchor.constraint(equalTo: searchBar.bottomAnchor, constant: 40.0),
// constrain homeImage Width equal to contentView Width
homeImage.widthAnchor.constraint(equalTo: contentView.widthAnchor, multiplier: 1.0),
// constrain homeImage centerX to contentView centerX
homeImage.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
// constrain homeImage Height to 1/2 of scroll view frame Height
homeImage.heightAnchor.constraint(equalTo: svFrameG.heightAnchor, multiplier: 0.5),
// you probably won't get vertical scrolling yet, so increase the vertical space
// between the homeImage and the label by changing the constant
// from 100 to maybe 400
// constrain label Top to homeImage Bottom + 100
label.topAnchor.constraint(equalTo: homeImage.bottomAnchor, constant: 100.0),
// constrain label centerX to contentView centerX
label.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
// constrain label Bottom to contentView Bottom - 20
label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20.0),
])
}
}

Can't get StackView to view items properly

I'm trying to add 4 items to a UIStackView, this 4 Items are all a simple square UIView, I added them all to a UIStackView but they won't stay square, it's like the UIStackView squeezes them or something. I tried setting the UIStackView to be the same height of the items, and set it's width to be the height of the items * 4 so I can try and get 1:1 ratio, but nothing worked for me.
The UIView is a simple UIView with background color. I tried to set it's widthAnchor and heightAnchor to 50, but I know the UIStackView has it's own way to size the items in it.
I don't really know what to do about this.
This is my UIStackView setup and constraints:
Setup:
private lazy var optionButtonStack: UIStackView = {
let stack = UIStackView(arrangedSubviews: [self.optionButton1, self.optionButton2, self.optionButton3, self.optionButton4])
stack.translatesAutoresizingMaskIntoConstraints = false
stack.distribution = .fillEqually
stack.axis = .horizontal
stack.spacing = 2.5
return stack
}()
Constraints:
private func setupOptionButtonStack() {
addSubview(optionButtonStack)
NSLayoutConstraint.activate([
optionButtonStack.heightAnchor.constraint(equalToConstant: 50),
optionButtonStack.widthAnchor.constraint(equalToConstant: 200),
optionButtonStack.centerXAnchor.constraint(equalTo: self.centerXAnchor),
optionButtonStack.bottomAnchor.constraint(equalTo: buyNowButton.topAnchor, constant: -8),
])
}
This is the UIView in case this is needed:
private let optionButton1: UIView = {
let view = UIView()
view.backgroundColor = .appBlue
view.translatesAutoresizingMaskIntoConstraints = false
view.widthAnchor.constraint(equalToConstant: 50).isActive = true
view.heightAnchor.constraint(equalToConstant: 50).isActive = true
view.tag = 1
return view
}()
Give the button view a single constraint setting its width equal to its height:
view.widthAnchor.constraint(equalTo: view.heightAnchor).isActive = true
and set the stack view alignment at center.

Extracting a childView and repositioning it inside of a new parentView

I’m trying to rip a view from a stackView that is embedded in a scrollView and then reposition said view in the same location but in another view at the same level in the view hierarchy as the scrollView.
The effect I’m trying to achieve is that I’m animating the removal of a view— where the view would be super imposed in another view, while the scrollView would scroll up and new view would be added to the stackView all while the view that was ripped fades out.
Unfortunately, achieving this effect remains elusive as the rippedView is position at (x: 0, y: 0). When I try force a new frame onto this view its tough because Im guessing the pixel perfect correct frame. Here’s a bit of the code from my viewController:
/*
I tried to make insertionView and imposeView have the same dimensions as the scrollView and
the stackView respectively as I thought if the rippedView’s original superView is the same
dimensions as it’s new superView, the rippedView would be positioned in the same place
without me needing to alter its frame.
*/
let insertionView = UIView(frame: scrollView.frame)
let imposeView = UIView(frame: stackView.frame)
rippedView.removeFromSuperview()
insertionView.addSubview(imposeView)
imposeView.addSubview(rippedView)
let newFrame = CGRect(x: 0, y: 450, width: rippedView.intrinsicContentSize.width, height:
rippedView.intrinsicContentSize.height)
rippedView.frame = newFrame
self.view.addSubview(insertionView)
Before removing rippedView, get it's actual frame:
let newFrame = self.view.convert(rippedView.bounds, from: rippedView)
The issue you are hitting is likely due to the stackView's arranged subviews having .translatesAutoresizingMaskIntoConstraints set to false. I believe this happens automatically when you add a view to a stackView, unless you specify otherwise.
A stackView's arranged subviews have coordinates relative to the stackView itself. So the first view will be at 0,0. Since you are adding a "container" view with the same frame as the stackView, you can use the same coordinate space... but you'll need to enable .translatesAutoresizingMaskIntoConstraints.
Try it like this:
#objc func btnTapped(_ sender: Any?) -> Void {
// get a reference to the 3rd arranged subview in the stack view
let rippedView = stackView.arrangedSubviews[2]
// local var holding the rippedView frame (as set by the stackView)
// get it before moving view from stackView
let r = rippedView.frame
// instantiate views
let insertionView = UIView(frame: scrollView.frame)
let imposeView = UIView(frame: stackView.frame)
// add imposeView to insertionView
insertionView.addSubview(imposeView)
// add insertionView to self.view
self.view.addSubview(insertionView)
// move rippedView from stackView to imposeView
imposeView.addSubview(rippedView)
// just to make it easy to see...
rippedView.backgroundColor = .green
// set to TRUE
rippedView.translatesAutoresizingMaskIntoConstraints = true
// set the frame
rippedView.frame = r
}
Here's a full class example that you can run directly (just assign it to a view controller):
class RipViewViewController: UIViewController {
let aButton: UIButton = {
let v = UIButton()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .red
v.setTitle("Testing", for: .normal)
return v
}()
let scrollView: UIScrollView = {
let v = UIScrollView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .systemBlue
return v
}()
let stackView: UIStackView = {
let v = UIStackView()
v.translatesAutoresizingMaskIntoConstraints = false
v.axis = .vertical
v.spacing = 8
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(aButton)
view.addSubview(scrollView)
scrollView.addSubview(stackView)
let g = view.safeAreaLayoutGuide
let sg = scrollView.contentLayoutGuide
NSLayoutConstraint.activate([
aButton.topAnchor.constraint(equalTo: g.topAnchor, constant: 16.0),
aButton.centerXAnchor.constraint(equalTo: g.centerXAnchor, constant: 0.0),
scrollView.topAnchor.constraint(equalTo: aButton.bottomAnchor, constant: 40.0),
scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -40.0),
stackView.topAnchor.constraint(equalTo: sg.topAnchor, constant: 40.0),
stackView.leadingAnchor.constraint(equalTo: sg.leadingAnchor, constant: 20.0),
stackView.trailingAnchor.constraint(equalTo: sg.trailingAnchor, constant: 20.0),
stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -40.0),
stackView.bottomAnchor.constraint(equalTo: sg.bottomAnchor, constant: 20.0),
])
for i in 1...5 {
let l = UILabel()
l.backgroundColor = .cyan
l.textAlignment = .center
l.text = "Label \(i)"
stackView.addArrangedSubview(l)
}
aButton.addTarget(self, action: #selector(btnTapped(_:)), for: .touchUpInside)
}
#objc func btnTapped(_ sender: Any?) -> Void {
// get a reference to the 3rd arranged subview in the stack view
let rippedView = stackView.arrangedSubviews[2]
// local var holding the rippedView frame (as set by the stackView)
// get it before moving view from stackView
let r = rippedView.frame
// instantiate views
let insertionView = UIView(frame: scrollView.frame)
let imposeView = UIView(frame: stackView.frame)
// add imposeView to insertionView
insertionView.addSubview(imposeView)
// add insertionView to self.view
self.view.addSubview(insertionView)
// move rippedView from stackView to imposeView
imposeView.addSubview(rippedView)
// just to make it easy to see...
rippedView.backgroundColor = .green
// set to TRUE
rippedView.translatesAutoresizingMaskIntoConstraints = true
// set the frame
rippedView.frame = r
}
}

How to increase UIView height which contains UIStackView

I have a custom view which contains a label, label can have multiple line text. So i have added that label inside a UIStackView, now my StackView height is increasing but the custom view height doesn't increases. I haven't added bottom constraint on my StackView. What should I do so that my CustomView height also increases with the StackView.
let myView = Bundle.main.loadNibNamed("TestView", owner: nil, options: nil)![0] as! TestView
myView.lbl.text = "sdvhjvhsdjkvhsjkdvhsjdvhsdjkvhsdjkvhsdjkvhsjdvhsjdvhsjdvhsjdvhsjdvhsjdvhsjdvhsdjvhsdjvhsdjvhsdjvhsdjvhsjdvhsdjvhsdjvhsjdvhsdjvhsjdvhsdjvhsdjvhsdjvhsjdv"
myView.lbl.sizeToFit()
myView.frame = CGRect(x: 10, y: 100, width: UIScreen.main.bounds.width, height: myView.frame.size.height)
myView.setNeedsLayout()
myView.layoutIfNeeded()
self.view.addSubview(myView)
I want to increase my custom view height as per my stackview height.
Please help.
Example of stackView constraints with its superview.
Also superview should not have constraints for its height.
You should set the top and bottom anchors of your custom view to be constrained to the top and bottom anchors of your stackview. As your stackView grows, it will push that bottom margin along. Here's a programmatic example:
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
private lazy var stackView = UIStackView()
private lazy var addLabelButton = UIButton(type: .system)
override func loadView() {
let view = UIView()
view.backgroundColor = .white
let stackViewContainer = UIView(frame: view.bounds)
stackViewContainer.backgroundColor = .yellow
stackViewContainer.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackViewContainer)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
addLabelButton.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(addLabelButton)
stackViewContainer.addSubview(stackView)
NSLayoutConstraint.activate([
// Container constrained to three edges of its superview (fourth edge will grow as the stackview grows
stackViewContainer.leadingAnchor.constraint(equalTo: view.leadingAnchor),
stackViewContainer.trailingAnchor.constraint(equalTo: view.trailingAnchor),
stackViewContainer.topAnchor.constraint(equalTo: view.topAnchor),
// stackView constraints - stackView is constrained to the
// for corners of its contaier, with margins
{
// Stackview has a height of 0 when no arranged subviews have been added.
let heightConstraint = stackView.heightAnchor.constraint(equalToConstant: 0)
heightConstraint.priority = .defaultLow
return heightConstraint
}(),
stackView.topAnchor.constraint(equalTo: stackViewContainer.topAnchor, constant: 8),
stackView.leadingAnchor.constraint(equalTo: stackViewContainer.leadingAnchor, constant: 8),
stackView.trailingAnchor.constraint(equalTo: stackViewContainer.trailingAnchor, constant: -8),
stackView.bottomAnchor.constraint(equalTo: stackViewContainer.bottomAnchor, constant: -8),
// button constraints
addLabelButton.topAnchor.constraint(equalTo: stackViewContainer.bottomAnchor, constant: 8),
addLabelButton.centerXAnchor.constraint(equalTo: stackViewContainer.centerXAnchor)
])
addLabelButton.setTitle("New Label", for: .normal)
addLabelButton.addTarget(self, action: #selector(addLabel(sender:)), for: .touchUpInside)
self.view = view
}
private(set) var labelCount = 0
#objc func addLabel(sender: AnyObject?) {
let label = UILabel()
label.text = "Label #\(labelCount)"
labelCount += 1
stackView.addArrangedSubview(label)
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
Note that when the UIStackView is empty, its height is not well defined. That is why I set its heightAnchor constraint to 0 with a low priority.
First of all you should add bottom constraint on your UIStackView. This will help auto layout in determining the run time size of UIStackView.
Now create instance of your custom UIView but do not set it's frame and add it to UIStackView. Make sure you Custom UiView has all the constraints set for auto layout to determine it's run time frame.
This will increase height of both UIView and UIStackView based on content of UIView elements.
For more details you can follow my detailed answer on this at https://stackoverflow.com/a/57954517/3339966

Resources