How can I animate this tableview header Y position change - swift

I want to slide my table view header into view if the use scrolls up my feed and the header is off screen. If the user then scrolls down I want to hide it again.
I believe I have this working using the following:
final class AccountSettingsController: UITableViewController {
let items: [Int] = Array(0...500)
private lazy var menuView: UIView = {
let view = UIView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .systemPink
view.heightAnchor.constraint(equalToConstant: 120).isActive = true
view.widthAnchor.constraint(equalToConstant: 100).isActive = true
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
edgesForExtendedLayout = []
extendedLayoutIncludesOpaqueBars = false
tableView.tableHeaderViewWithAutolayout = menuView
tableView.tableFooterView = .init()
tableView.refreshControl = .init()
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.textLabel?.text = "This is setting #\(indexPath.row)"
return cell
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
let contentOffset = scrollView.contentOffset.y
let transY = scrollView.panGestureRecognizer.translation(in: scrollView).y
if scrollView.contentOffset.y > 120 {
if transY > 0 {
menuView.transform = .init(translationX: 0, y: contentOffset)
} else if transY < 0 {
menuView.transform = .identity
}
return
}
if scrollView.contentOffset.y <= 120 {
if transY < 0 {
menuView.transform = .identity
} else if transY > 0 {
menuView.transform = .init(translationX: 0, y: contentOffset)
}
}
}
}
I would like to animate the slide in / slide out of the header, so it slides down and up, rather than just appears.
I tried to add UIView.animate blocks such as
if scrollView.contentOffset.y <= 120 {
if transY < 0 {
menuView.transform = .identity
} else if transY > 0 {
UIView.animate(withDuration: 0.25, animations: {
self.menuView.transform = .init(translationX: 0, y: contentOffset)
})
}
}
but this produces a random jumping effect on the header and does not achieve what I would like at all.
I've attached a gif that shows the current effect, as you can see it just appears and disappears. I'd like this to animate down and up for for show / hide.
I also have an extension to set the correct size for my table view header, which you can find here -
extension UITableView {
var tableHeaderViewWithAutolayout: UIView? {
set (view) {
tableHeaderView = view
if let view = view {
lowerPriorities(view)
view.frame.size = view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
tableHeaderView = view
}
}
get {
return tableHeaderView
}
}
fileprivate func lowerPriorities(_ view: UIView) {
for cons in view.constraints {
if cons.priority.rawValue == 1000 {
cons.priority = UILayoutPriority(rawValue: 999)
}
for v in view.subviews {
lowerPriorities(v)
}
}
}
}

I agree with Claudio, using the tableHeaderView is probably complicating things as it is not really intended to be manipulated in this way.
Try the below and see if this gets you what you are looking for.
Instead of manipulating the offset of the header, manipulate the height. You can still give the impression of a scroll.
You could also introduce scrollView.panGestureRecognizer.velocity(in: scrollView) if you wanted to apply a threshold so the menu appears only after a particular scroll speed.
class MyTableViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
private let data: [Int] = Array(0...99)
private let menuView: UIView = {
let view = UIView(frame: .zero)
view.backgroundColor = .purple
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private lazy var tableView: UITableView = {
let view = UITableView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.dataSource = self
view.delegate = self
// view.refreshControl = .init()
view.contentInsetAdjustmentBehavior = .never
view.tableFooterView = .init()
return view
}()
var menuViewHeightAnchor: NSLayoutConstraint!
var tableViewTopAnchor: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
menuViewHeightAnchor = menuView.heightAnchor.constraint(equalToConstant: 120)
tableViewTopAnchor = tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 120)
edgesForExtendedLayout = []
[tableView, menuView].forEach(view.addSubview)
NSLayoutConstraint.activate([
menuViewHeightAnchor,
menuView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
menuView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
menuView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableViewTopAnchor,
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
])
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(frame: .zero)
cell.textLabel?.text = "Cell #\(indexPath.row)"
return cell
}
private var previousOffsetY: CGFloat = 0
private var velocityThreshold: CGFloat = 500
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offsetY = min(120, max(0, scrollView.contentOffset.y))
let translation = scrollView.panGestureRecognizer.translation(in: scrollView)
let velocity = scrollView.panGestureRecognizer.velocity(in: scrollView)
let offsetYDiff = previousOffsetY - offsetY
previousOffsetY = offsetY
let adjustedOffset = menuViewHeightAnchor.constant + offsetYDiff
tableViewTopAnchor.constant = 120 - max(0, offsetY)
menuViewHeightAnchor.constant = min(120, adjustedOffset)
guard offsetY == 120 else { return }
if translation.y > 0 { // DRAGGED DOWN
UIView.animate(withDuration: 0.33, animations: {
self.menuViewHeightAnchor.constant = 120
self.view.layoutIfNeeded()
})
} else if translation.y < 0 { // DRAGGED UP
UIView.animate(withDuration: 0.33, animations: {
self.menuViewHeightAnchor.constant = 0
self.view.layoutIfNeeded()
})
}
}
}

I believe the best approach is by setting the menuView as a subview of the tableViewController, for example on the viewDidLoad method and also adding an inset on the tableView like this:
let menuHeight: CGFloat = 120
var scrollStartingYPoint: CGFloat = 0
private lazy var menuView: UIView = {
let view = UIView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .systemPink
view.heightAnchor.constraint(equalToConstant: menuHeight).isActive = true
view.widthAnchor.constraint(equalToConstant: 100).isActive = true
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
view.addSubview(menuView)
NSLayoutConstraint.activate([
menuView.leftAnchor.constraint(equalTo: view.leftAnchor),
menuView.topAnchor.constraint(equalTo: view.topAnchor)
])
edgesForExtendedLayout = []
extendedLayoutIncludesOpaqueBars = false
tableView.contentInset.top = menuHeight
}
Then you can add the scrolling logic to show/hide the menu, I've done it a little different than you using the scrollViewWillBeginDragging method to store the initial scroll offset, to then determine the scroll direction and offset:
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
scrollStartingYPoint = scrollView.contentOffset.y
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let tableYScrollOffset = scrollView.contentOffset.y
let offset = abs(tableYScrollOffset - scrollStartingYPoint)
if tableYScrollOffset < scrollStartingYPoint { // scrolling down
if menuView.frame.origin.y >= 0 {
return
}
var translationY = -menuHeight + offset
if translationY > 0 {
translationY = 0
}
menuView.transform = CGAffineTransform(translationX: 0, y: translationY)
} else { // scrolling up
if menuView.frame.origin.y <= -menuHeight {
return
}
var translationY = -offset
if translationY < -menuHeight {
translationY = -menuHeight
}
menuView.transform = CGAffineTransform(translationX: 0, y: translationY)
}
}
If you want to animate the presentation of the menu, then you can change the previous code and use your animation code like this:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let tableYScrollOffset = scrollView.contentOffset.y
let offset = abs(tableYScrollOffset - scrollStartingYPoint)
if tableYScrollOffset < scrollStartingYPoint { // scrolling down
UIView.animate(withDuration: 0.25, animations: {
self.menuView.transform = .identity
})
} else { // scrolling up
if menuView.frame.origin.y <= -menuHeight {
return
}
var translationY = -offset
if translationY < -menuHeight {
translationY = -menuHeight
}
menuView.transform = CGAffineTransform(translationX: 0, y: translationY)
}
}

Related

Sizing UIButton depending on length of titleLabel

So I have a UIButton and I'm setting the title in it to a string that is dynamic in length. I want the width of the titleLabel to be half of the screen width. I've tried using .sizeToFit() but this causes the button to use the CGSize before the constraint was applied to the titleLabel. I tried using .sizeThatFits(button.titleLabel?.intrinsicContentSize) but this also didn't work. I think the important functions below are the init() & presentCallout(), but I'm showing the entire class just for a more complete understanding. The class I'm playing with looks like:
class CustomCalloutView: UIView, MGLCalloutView {
var representedObject: MGLAnnotation
// Allow the callout to remain open during panning.
let dismissesAutomatically: Bool = false
let isAnchoredToAnnotation: Bool = true
// https://github.com/mapbox/mapbox-gl-native/issues/9228
override var center: CGPoint {
set {
var newCenter = newValue
newCenter.y -= bounds.midY
super.center = newCenter
}
get {
return super.center
}
}
lazy var leftAccessoryView = UIView() /* unused */
lazy var rightAccessoryView = UIView() /* unused */
weak var delegate: MGLCalloutViewDelegate?
let tipHeight: CGFloat = 10.0
let tipWidth: CGFloat = 20.0
let mainBody: UIButton
required init(representedObject: MGLAnnotation) {
self.representedObject = representedObject
self.mainBody = UIButton(type: .system)
super.init(frame: .zero)
backgroundColor = .clear
mainBody.backgroundColor = .white
mainBody.tintColor = .black
mainBody.contentEdgeInsets = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0)
mainBody.layer.cornerRadius = 4.0
addSubview(mainBody)
// I thought this would work, but it doesn't.
// mainBody.translatesAutoresizingMaskIntoConstraints = false
// mainBody.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
// mainBody.leftAnchor.constraint(equalTo: self.rightAnchor).isActive = true
// mainBody.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true
// mainBody.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
}
required init?(coder decoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - MGLCalloutView API
func presentCallout(from rect: CGRect, in view: UIView, constrainedTo constrainedRect: CGRect, animated: Bool) {
delegate?.calloutViewWillAppear?(self)
view.addSubview(self)
// Prepare title label.
mainBody.setTitle(representedObject.title!, for: .normal)
mainBody.titleLabel?.lineBreakMode = .byWordWrapping
mainBody.titleLabel?.numberOfLines = 0
mainBody.sizeToFit()
if isCalloutTappable() {
// Handle taps and eventually try to send them to the delegate (usually the map view).
mainBody.addTarget(self, action: #selector(CustomCalloutView.calloutTapped), for: .touchUpInside)
} else {
// Disable tapping and highlighting.
mainBody.isUserInteractionEnabled = false
}
// Prepare our frame, adding extra space at the bottom for the tip.
let frameWidth = mainBody.bounds.size.width
let frameHeight = mainBody.bounds.size.height + tipHeight
let frameOriginX = rect.origin.x + (rect.size.width/2.0) - (frameWidth/2.0)
let frameOriginY = rect.origin.y - frameHeight
frame = CGRect(x: frameOriginX, y: frameOriginY, width: frameWidth, height: frameHeight)
if animated {
alpha = 0
UIView.animate(withDuration: 0.2) { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.alpha = 1
strongSelf.delegate?.calloutViewDidAppear?(strongSelf)
}
} else {
delegate?.calloutViewDidAppear?(self)
}
}
func dismissCallout(animated: Bool) {
if (superview != nil) {
if animated {
UIView.animate(withDuration: 0.2, animations: { [weak self] in
self?.alpha = 0
}, completion: { [weak self] _ in
self?.removeFromSuperview()
})
} else {
removeFromSuperview()
}
}
}
// MARK: - Callout interaction handlers
func isCalloutTappable() -> Bool {
if let delegate = delegate {
if delegate.responds(to: #selector(MGLCalloutViewDelegate.calloutViewShouldHighlight)) {
return delegate.calloutViewShouldHighlight!(self)
}
}
return false
}
#objc func calloutTapped() {
if isCalloutTappable() && delegate!.responds(to: #selector(MGLCalloutViewDelegate.calloutViewTapped)) {
delegate!.calloutViewTapped!(self)
}
}
// MARK: - Custom view styling
override func draw(_ rect: CGRect) {
// Draw the pointed tip at the bottom.
let fillColor: UIColor = .white
let tipLeft = rect.origin.x + (rect.size.width / 2.0) - (tipWidth / 2.0)
let tipBottom = CGPoint(x: rect.origin.x + (rect.size.width / 2.0), y: rect.origin.y + rect.size.height)
let heightWithoutTip = rect.size.height - tipHeight - 1
let currentContext = UIGraphicsGetCurrentContext()!
let tipPath = CGMutablePath()
tipPath.move(to: CGPoint(x: tipLeft, y: heightWithoutTip))
tipPath.addLine(to: CGPoint(x: tipBottom.x, y: tipBottom.y))
tipPath.addLine(to: CGPoint(x: tipLeft + tipWidth, y: heightWithoutTip))
tipPath.closeSubpath()
fillColor.setFill()
currentContext.addPath(tipPath)
currentContext.fillPath()
}
}
This is what it looks like for a short title and a long title. When the title gets too long, I want the text to wrap and the bubble to get a taller height. As you can see in the image set below, the first 'Short Name' works fine as a map annotation bubble. When the name gets super long though, it just widens the bubble to the point it goes off the screen.
https://imgur.com/a/I5z0zUd
Any help on how to fix is much appreciated. Thanks!
To enable word-wrapping to multiple lines in a UIButton, you need to create your own button subclass.
For example:
class MultilineTitleButton: UIButton {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
func commonInit() -> Void {
self.titleLabel?.numberOfLines = 0
self.titleLabel?.textAlignment = .center
self.setContentHuggingPriority(UILayoutPriority.defaultLow + 1, for: .vertical)
self.setContentHuggingPriority(UILayoutPriority.defaultLow + 1, for: .horizontal)
}
override var intrinsicContentSize: CGSize {
let size = self.titleLabel!.intrinsicContentSize
return CGSize(width: size.width + contentEdgeInsets.left + contentEdgeInsets.right, height: size.height + contentEdgeInsets.top + contentEdgeInsets.bottom)
}
override func layoutSubviews() {
super.layoutSubviews()
titleLabel?.preferredMaxLayoutWidth = self.titleLabel!.frame.size.width
}
}
That button will wrap the title onto multiple lines, cooperating with auto-layout / constraints.
I don't have any projects with MapBox, but here is an example using a modified version of your CustomCalloutView. I commented out any MapBox specific code. You may be able to un-comment those lines and use this as-is:
class CustomCalloutView: UIView { //}, MGLCalloutView {
//var representedObject: MGLAnnotation
var repTitle: String = ""
// Allow the callout to remain open during panning.
let dismissesAutomatically: Bool = false
let isAnchoredToAnnotation: Bool = true
// https://github.com/mapbox/mapbox-gl-native/issues/9228
// NOTE: this causes a vertical shift when NOT using MapBox
// override var center: CGPoint {
// set {
// var newCenter = newValue
// newCenter.y -= bounds.midY
// super.center = newCenter
// }
// get {
// return super.center
// }
// }
lazy var leftAccessoryView = UIView() /* unused */
lazy var rightAccessoryView = UIView() /* unused */
//weak var delegate: MGLCalloutViewDelegate?
let tipHeight: CGFloat = 10.0
let tipWidth: CGFloat = 20.0
let mainBody: UIButton
var anchorView: UIView!
override func willMove(toSuperview newSuperview: UIView?) {
if newSuperview == nil {
anchorView.removeFromSuperview()
}
}
//required init(representedObject: MGLAnnotation) {
required init(title: String) {
self.repTitle = title
self.mainBody = MultilineTitleButton()
super.init(frame: .zero)
backgroundColor = .clear
mainBody.backgroundColor = .white
mainBody.setTitleColor(.black, for: [])
mainBody.tintColor = .black
mainBody.contentEdgeInsets = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0)
mainBody.layer.cornerRadius = 4.0
addSubview(mainBody)
mainBody.translatesAutoresizingMaskIntoConstraints = false
let padding: CGFloat = 8.0
NSLayoutConstraint.activate([
mainBody.topAnchor.constraint(equalTo: self.topAnchor, constant: padding),
mainBody.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: padding),
mainBody.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -padding),
mainBody.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -padding),
])
}
required init?(coder decoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - MGLCalloutView API
func presentCallout(from rect: CGRect, in view: UIView, constrainedTo constrainedRect: CGRect, animated: Bool) {
//delegate?.calloutViewWillAppear?(self)
// since we'll be using auto-layout for the mutli-line button
// we'll add an "anchor view" to the superview
// it will be removed when self is removed
anchorView = UIView(frame: rect)
anchorView.isUserInteractionEnabled = false
anchorView.backgroundColor = .clear
view.addSubview(anchorView)
view.addSubview(self)
// Prepare title label.
//mainBody.setTitle(representedObject.title!, for: .normal)
mainBody.setTitle(self.repTitle, for: .normal)
// if isCalloutTappable() {
// // Handle taps and eventually try to send them to the delegate (usually the map view).
// mainBody.addTarget(self, action: #selector(CustomCalloutView.calloutTapped), for: .touchUpInside)
// } else {
// // Disable tapping and highlighting.
// mainBody.isUserInteractionEnabled = false
// }
self.translatesAutoresizingMaskIntoConstraints = false
anchorView.autoresizingMask = [.flexibleTopMargin, .flexibleLeftMargin, .flexibleRightMargin, .flexibleBottomMargin]
NSLayoutConstraint.activate([
self.centerXAnchor.constraint(equalTo: anchorView.centerXAnchor),
self.bottomAnchor.constraint(equalTo: anchorView.topAnchor),
self.widthAnchor.constraint(lessThanOrEqualToConstant: constrainedRect.width),
])
if animated {
alpha = 0
UIView.animate(withDuration: 0.2) { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.alpha = 1
//strongSelf.delegate?.calloutViewDidAppear?(strongSelf)
}
} else {
//delegate?.calloutViewDidAppear?(self)
}
}
func dismissCallout(animated: Bool) {
if (superview != nil) {
if animated {
UIView.animate(withDuration: 0.2, animations: { [weak self] in
self?.alpha = 0
}, completion: { [weak self] _ in
self?.removeFromSuperview()
})
} else {
removeFromSuperview()
}
}
}
// MARK: - Callout interaction handlers
// func isCalloutTappable() -> Bool {
// if let delegate = delegate {
// if delegate.responds(to: #selector(MGLCalloutViewDelegate.calloutViewShouldHighlight)) {
// return delegate.calloutViewShouldHighlight!(self)
// }
// }
// return false
// }
//
// #objc func calloutTapped() {
// if isCalloutTappable() && delegate!.responds(to: #selector(MGLCalloutViewDelegate.calloutViewTapped)) {
// delegate!.calloutViewTapped!(self)
// }
// }
// MARK: - Custom view styling
override func draw(_ rect: CGRect) {
print(#function)
// Draw the pointed tip at the bottom.
let fillColor: UIColor = .red
let tipLeft = rect.origin.x + (rect.size.width / 2.0) - (tipWidth / 2.0)
let tipBottom = CGPoint(x: rect.origin.x + (rect.size.width / 2.0), y: rect.origin.y + rect.size.height)
let heightWithoutTip = rect.size.height - tipHeight - 1
let currentContext = UIGraphicsGetCurrentContext()!
let tipPath = CGMutablePath()
tipPath.move(to: CGPoint(x: tipLeft, y: heightWithoutTip))
tipPath.addLine(to: CGPoint(x: tipBottom.x, y: tipBottom.y))
tipPath.addLine(to: CGPoint(x: tipLeft + tipWidth, y: heightWithoutTip))
tipPath.closeSubpath()
fillColor.setFill()
currentContext.addPath(tipPath)
currentContext.fillPath()
}
}
Here is a sample view controller showing that "Callout View" with various length titles, restricted to 70% of the width of the view:
class CalloutTestVC: UIViewController {
let sampleTitles: [String] = [
"Short Title",
"Slightly Longer Title",
"A ridiculously long title that will need to wrap!",
]
var idx: Int = -1
let tapView = UIView()
var ccv: CustomCalloutView!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(red: 0.8939146399, green: 0.8417750597, blue: 0.7458069921, alpha: 1)
tapView.backgroundColor = .systemBlue
tapView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tapView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
tapView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
tapView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
tapView.widthAnchor.constraint(equalToConstant: 60),
tapView.heightAnchor.constraint(equalTo: tapView.widthAnchor),
])
// tap the Blue View to cycle through Sample Titles for the Callout View
// using the Blue view as the "anchor rect"
let t = UITapGestureRecognizer(target: self, action: #selector(gotTap))
tapView.addGestureRecognizer(t)
}
#objc func gotTap() -> Void {
if ccv != nil {
ccv.removeFromSuperview()
}
// increment sampleTitles array index
// to cycle through the strings
idx += 1
let validIdx = idx % sampleTitles.count
let str = sampleTitles[validIdx]
// create a new Callout view
ccv = CustomCalloutView(title: str)
// to restrict the "callout view" width to less-than 1/2 the screen width
// use view.width * 0.5 for the constrainedTo width
// may look better restricting it to 70%
ccv.presentCallout(from: tapView.frame, in: self.view, constrainedTo: CGRect(x: 0, y: 0, width: view.frame.size.width * 0.7, height: 100), animated: false)
}
}
It looks like this:
The UIButton class owns the titleLabel and is going to position and set the constraints on that label itself. More likely than not you are going to have to create a subclass of UIButton and override its "updateConstraints" method to position the titleLabel where you want it to go.
Your code should probably not be basing the size of the button off the size of the screen. It might set the size of off some other view in your hierarchy that happens to be the size of the screen but grabbing the screen bounds in the middle of setting a view's size is unusual.

Animate UIView position on tableview scroll and again tableview drag

I have a list of items which a header menu at the top.
When the user scrolls up the menu should scroll off screen also and when they return to the top of the list the menu should be visible.
In much the same way as a tableViewHeader behaves.
However, should the header be off screen and the user ends a drag down on the list, this header view should animate down from the top. If the user then ends a drag up on the list, the header should animate back off screen.
I have achieved the first part of this below, however am struggling to achieve the animation effect.
I had considered using 2 views for the menu, one that scrolls and one that animates it's position, however this feels off, I'm sure there must be a better way without duplicating views.
class TableViewScene: UIViewController {
let data = Array(0...99)
var headerViewOneTopConstraint: NSLayoutConstraint!
var tableViewTopConstraint: NSLayoutConstraint!
lazy var headerViewOne: UIView = {
let view = UIView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .purple
return view
}()
lazy var tableView: UITableView = {
let view = UITableView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
view.delegate = self
view.dataSource = self
view.tableFooterView = .init()
view.refreshControl = .init()
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.navigationBar.isTranslucent = false
headerViewOneTopConstraint = headerViewOne.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor)
tableViewTopConstraint = tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 184)
[headerViewOne, tableView].forEach(view.addSubview)
NSLayoutConstraint.activate([
headerViewOneTopConstraint,
headerViewOne.leadingAnchor.constraint(equalTo: view.leadingAnchor),
headerViewOne.trailingAnchor.constraint(equalTo: view.trailingAnchor),
headerViewOne.heightAnchor.constraint(equalToConstant: 184),
tableViewTopConstraint,
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
}
}
extension TableViewScene: UITableViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offsetY = scrollView.contentOffset.y
headerViewOneTopConstraint.constant = max(-184, min(0, -offsetY))
tableViewTopConstraint.constant = 184 - max(0, offsetY)
}
}
I had also tried adding this to the view
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
let translation = scrollView.panGestureRecognizer.translation(in: scrollView.superview)
if translation.y > 0 {
headerViewOne.transform = .init(translationX: 0, y: 184)
} else if translation.y < 0 {
headerViewOne.transform = .identity
}
}
This does not work and instead just renders a black space the view used to fill on scroll to the top.
It's hard to say exactly what you are looking for but this might work for you. In this implementation when the user drags up the header will hide and when the user drags down the header will show. This is true regardless of the scroll position.
I made a change to one of your constraints so that the top of the tableView is pinned to the bottom of the header:
tableViewTopConstraint = tableView.topAnchor.constraint(equalTo: headerViewOne.bottomAnchor)
I'm also tracking the state of the header like so:
enum HeaderState {
case hidden
case revealed
case hiding
case revealing
}
var headerState: HeaderState = .revealed
Now for the scrolling logic. If the scroll position is near the top then we want to manually show/hide the header. However, if the scroll position is near the bottom or center then we want to animate the show/hide functionality.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let translation = scrollView.panGestureRecognizer.translation(in: scrollView.superview)
if scrollView.contentOffset.y < 184 {
if translation.y > 0 && headerState != .revealed && headerState != .revealing {
headerViewOneTopConstraint.constant = max(-184, min(0, -scrollView.contentOffset.y))
} else if translation.y < 0 {
headerViewOneTopConstraint.constant = max(-184, min(0, -scrollView.contentOffset.y))
}
return
}
if translation.y > 0 && headerState == .hidden {
self.headerState = .revealing
UIView.animate(withDuration: 0.4, animations: {
self.headerViewOneTopConstraint.constant = 0
self.view.layoutIfNeeded()
}, completion: { _ in
self.headerState = .revealed
})
} else if translation.y < 0 && headerState == .revealed {
self.headerState = .hiding
UIView.animate(withDuration: 0.4, animations: {
self.headerViewOneTopConstraint.constant = -184
self.view.layoutIfNeeded()
}, completion: { _ in
self.headerState = .hidden
})
}
}
Give this a shot and see if it meets your needs.

How can I position these UIView elements from the right using CGRect to position

I have a UIView sub class that allows me to create a group of 'tags' for the footer of some content. At the moment however they are position aligned to the left edge, I would like them to be positioned from the right.
I have included a playground below that should run the screen shot you can see.
The position is set within the layoutSubviews method of CloudTagView.
I tried to play around with their position but have not been able to start them from the right however.
import UIKit
import PlaygroundSupport
// CLOUD VIEW WRAPPER - THIS IS THE CONTAINER FOR THE TAGS AND SETS UP THEIR FRAME
class CloudTagView: UIView {
weak var delegate: TagViewDelegate?
override var intrinsicContentSize: CGSize {
return frame.size
}
var removeOnDismiss = true
var resizeToFit = true
var tags = [TagView]() {
didSet {
layoutSubviews()
}
}
var padding = 5 {
didSet {
layoutSubviews()
}
}
var maxLengthPerTag = 0 {
didSet {
layoutSubviews()
}
}
public override init(frame: CGRect) {
super.init(frame: frame)
isUserInteractionEnabled = true
clipsToBounds = true
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
isUserInteractionEnabled = true
clipsToBounds = true
}
override func layoutSubviews() {
for tag in subviews {
tag.removeFromSuperview()
}
var xAxis = padding
var yAxis = padding
var maxHeight = 0
for (index, tag) in tags.enumerated() {
setMaxLengthIfNeededIn(tag)
tag.delegate = self
if index == 0 {
maxHeight = Int(tag.frame.height)
}else{
let expectedWidth = xAxis + Int(tag.frame.width) + padding
if expectedWidth > Int(frame.width) {
yAxis += maxHeight + padding
xAxis = padding
maxHeight = Int(tag.frame.height)
}
if Int(tag.frame.height) > maxHeight {
maxHeight = Int(tag.frame.height)
}
}
tag.frame = CGRect(x: xAxis, y: yAxis, width: Int(tag.frame.size.width), height: Int(tag.frame.size.height))
addSubview(tag)
tag.layoutIfNeeded()
xAxis += Int(tag.frame.width) + padding
}
if resizeToFit {
frame = CGRect(x: frame.origin.x, y: frame.origin.y, width: frame.size.width, height: CGFloat(yAxis + maxHeight + padding))
}
}
// MARK: Methods
fileprivate func setMaxLengthIfNeededIn(_ tag: TagView) {
if maxLengthPerTag > 0 && tag.maxLength != maxLengthPerTag {
tag.maxLength = maxLengthPerTag
}
}
}
// EVERYTHING BELOW HERE IS JUST SETUP / REQUIRED TO RUN IN PLAYGROUND
class ViewController:UIViewController{
let cloudView: CloudTagView = {
let view = CloudTagView(frame: .zero)
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
let tags = ["these", "are", "my", "tags"]
tags.forEach { tag in
let t = TagView(text: tag)
t.backgroundColor = .darkGray
t.tintColor = .white
cloudView.tags.append(t)
}
view.backgroundColor = .white
view.addSubview(cloudView)
NSLayoutConstraint.activate([
cloudView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
cloudView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
cloudView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
cloudView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor)
])
}
}
// Tag View
class TagView: UIView {
weak var delegate: TagViewDelegate?
var text = "" {
didSet {
layoutSubviews()
}
}
var marginTop = 5 {
didSet {
layoutSubviews()
}
}
var marginLeft = 10 {
didSet {
layoutSubviews()
}
}
var iconImage = UIImage(named: "close_tag_2", in: Bundle(for: CloudTagView.self), compatibleWith: nil) {
didSet {
layoutSubviews()
}
}
var maxLength = 0 {
didSet {
layoutSubviews()
}
}
override var backgroundColor: UIColor? {
didSet {
layoutSubviews()
}
}
override var tintColor: UIColor? {
didSet {
layoutSubviews()
}
}
var font: UIFont = UIFont.systemFont(ofSize: 12) {
didSet {
layoutSubviews()
}
}
fileprivate let dismissView: UIView
fileprivate let icon: UIImageView
fileprivate let textLabel: UILabel
public override init(frame: CGRect) {
dismissView = UIView()
icon = UIImageView()
textLabel = UILabel()
super.init(frame: frame)
isUserInteractionEnabled = true
addSubview(textLabel)
addSubview(icon)
addSubview(dismissView)
dismissView.isUserInteractionEnabled = true
textLabel.isUserInteractionEnabled = true
dismissView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(TagView.iconTapped)))
textLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(TagView.labelTapped)))
backgroundColor = UIColor(white: 0.0, alpha: 0.6)
tintColor = UIColor.white
}
public required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public init(text: String) {
dismissView = UIView()
icon = UIImageView()
textLabel = UILabel()
super.init(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
isUserInteractionEnabled = true
addSubview(textLabel)
addSubview(icon)
addSubview(dismissView)
dismissView.isUserInteractionEnabled = true
textLabel.isUserInteractionEnabled = true
dismissView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(TagView.iconTapped)))
textLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(TagView.labelTapped)))
self.text = text
backgroundColor = UIColor(white: 0.0, alpha: 0.6)
tintColor = UIColor.white
}
override func layoutSubviews() {
icon.frame = CGRect(x: marginLeft, y: marginTop + 4, width: 8, height: 8)
icon.image = iconImage?.withRenderingMode(.alwaysTemplate)
icon.tintColor = tintColor
let textLeft: Int
if icon.image != nil {
dismissView.isUserInteractionEnabled = true
textLeft = marginLeft + Int(icon.frame.width ) + marginLeft / 2
} else {
dismissView.isUserInteractionEnabled = false
textLeft = marginLeft
}
textLabel.frame = CGRect(x: textLeft, y: marginTop, width: 100, height: 20)
textLabel.backgroundColor = UIColor(white: 0, alpha: 0.0)
if maxLength > 0 && text.count > maxLength {
textLabel.text = text.prefix(maxLength)+"..."
}else{
textLabel.text = text
}
textLabel.textAlignment = .center
textLabel.font = font
textLabel.textColor = tintColor
textLabel.sizeToFit()
let tagHeight = Int(max(textLabel.frame.height,14)) + marginTop * 2
let tagWidth = textLeft + Int(max(textLabel.frame.width,14)) + marginLeft
let dismissLeft = Int(icon.frame.origin.x) + Int(icon.frame.width) + marginLeft / 2
dismissView.frame = CGRect(x: 0, y: 0, width: dismissLeft, height: tagHeight)
frame = CGRect(x: Int(frame.origin.x), y: Int(frame.origin.y), width: tagWidth, height: tagHeight)
layer.cornerRadius = bounds.height / 2
}
// MARK: Actions
#objc func iconTapped(){
delegate?.tagDismissed?(self)
}
#objc func labelTapped(){
delegate?.tagTouched?(self)
}
}
// MARK: TagViewDelegate
#objc protocol TagViewDelegate {
#objc optional func tagTouched(_ tag: TagView)
#objc optional func tagDismissed(_ tag: TagView)
}
extension CloudTagView: TagViewDelegate {
public func tagDismissed(_ tag: TagView) {
delegate?.tagDismissed?(tag)
if removeOnDismiss {
if let index = tags.firstIndex(of: tag) {
tags.remove(at: index)
}
}
}
public func tagTouched(_ tag: TagView) {
delegate?.tagTouched?(tag)
}
}
let viewController = ViewController()
PlaygroundPage.current.liveView = viewController
PlaygroundPage.current.needsIndefiniteExecution = true
UIStackView can line subviews up in a row for you, including with trailing alignment. Here is a playground example:
import SwiftUI
import PlaygroundSupport
class V: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let tags = ["test", "testing", "test more"].map { word -> UIView in
let label = UILabel()
label.text = word
label.translatesAutoresizingMaskIntoConstraints = false
let background = UIView()
background.backgroundColor = .cyan
background.layer.cornerRadius = 8
background.clipsToBounds = true
background.addSubview(label)
NSLayoutConstraint.activate([
background.centerXAnchor.constraint(equalTo: label.centerXAnchor),
background.centerYAnchor.constraint(equalTo: label.centerYAnchor),
background.widthAnchor.constraint(equalTo: label.widthAnchor, constant: 16),
background.heightAnchor.constraint(equalTo: label.heightAnchor, constant: 16),
])
return background
}
let stack = UIStackView.init(arrangedSubviews: [UIView()] + tags)
stack.translatesAutoresizingMaskIntoConstraints = false
stack.axis = .horizontal
stack.alignment = .trailing
stack.spacing = 12
view.addSubview(stack)
NSLayoutConstraint.activate([
stack.topAnchor.constraint(equalTo: view.topAnchor),
stack.widthAnchor.constraint(equalTo: view.widthAnchor),
])
view.backgroundColor = .white
}
}
PlaygroundPage.current.liveView = V()

How to adjust height of child view controller to match container view's height

I have a container View with adjustable height. Now I want to set the same frame of the view of child view controller according to container view's height. Sometimes it does work in the simulation but often it fails. The frame of child view controller's view often stays the same as defined at the beginning. Do you know a workaround to solve this problem ?
Here is my main ViewController with containerView named bottomView
class ViewController: UIViewController {
var startPosition: CGPoint!
var originalHeight: CGFloat = 0
var bottomView = UIView()
var gestureRecognizer = UIPanGestureRecognizer()
let scrollView = UIScrollView()
let controller = EditorViewController()
override func viewDidLoad() {
self.view.backgroundColor = .white
view.addSubview(bottomView)
bottomView.translatesAutoresizingMaskIntoConstraints = false
bottomView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
bottomView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
bottomView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
bottomView.heightAnchor.constraint(equalToConstant: 100).isActive = true
bottomView.backgroundColor = .white
gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(viewDidDragged(_:)))
bottomView.isUserInteractionEnabled = true
bottomView.addGestureRecognizer(gestureRecognizer)
// add childviewController
self.addChild(controller)
controller.view.translatesAutoresizingMaskIntoConstraints = false
controller.view.frame = bottomView.bounds
bottomView.addSubview(controller.view)
controller.view.rightAnchor.constraint(equalTo: bottomView.rightAnchor).isActive = true
controller.view.leftAnchor.constraint(equalTo: bottomView.leftAnchor).isActive = true
controller.view.bottomAnchor.constraint(equalTo: bottomView.bottomAnchor).isActive = true
controller.view.topAnchor.constraint(equalTo: bottomView.topAnchor).isActive = true
controller.didMove(toParent: self)
}
}
The childView Controller looks like this:
class EditorViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .red
}
And in this way I change the height of container view and tried to adjust the height of child viewController, too. But it does not work.
#objc func viewDidDragged(_ sender: UIPanGestureRecognizer) {
if sender.state == .began {
startPosition = gestureRecognizer.location(in: self.view) // the postion at which PanGestue Started
originalHeight = bottomView.frame.size.height
}
if sender.state == .began || sender.state == .changed {
let endPosition = sender.location(in: self.view)
let difference = endPosition.y - startPosition.y
let newHeight = originalHeight - difference
if self.view.frame.size.height - endPosition.y < 100 {
bottomView.frame = CGRect(x: 0, y: self.view.frame.size.height - 100, width: self.view.frame.size.width, height: 100)
controller.view.frame = bottomView.bounds
} else {
bottomView.frame = CGRect(x: 0, y: self.view.frame.size.height - newHeight, width: self.view.frame.size.width, height: newHeight)
controller.view.frame = bottomView.bounds
}
}
if sender.state == .ended || sender.state == .cancelled {
//Do Something
}
}
Try using a constraint to change the height. Something like:
class ViewController: UIViewController {
...
var heightConstraint: NSLayoutConstraint?
override func viewDidLoad() {
...
heightConstraint = bottomView.heightAnchor.constraint(equalToConstant: 100)
heightConstraint?.isActive = true
....
}
#objc func viewDidDragged(_ sender: UIPanGestureRecognizer) {
...
if sender.state == .began || sender.state == .changed {
let endPosition = sender.location(in: self.view)
let difference = endPosition.y - startPosition.y
let newHeight = originalHeight - difference
if self.view.frame.size.height - endPosition.y < 100 {
heightConstraint?.constant = 100
} else {
heightConstraint?.constant = newHeight
}
}
...
}
}

Swift imageview frame is equal to image original size

when creating an UIImageView with an Image, the view's frame is set to the image's original size. I would like to know it's actual size after all anchor constraints have been applied
I've tried different images and they all do the same thing.
The class with the issues is: SuggestionCloud;
I will explain the issue in three parts:
FIRST: the superclass that sets up all UI elements and invokes the "faulty' custom UIImage class (SuggestionCloud).
SECOND: The suggesionCloud Class
UIScaleControllerVew
Class UIScaleControllerView: UIViewController: {
let suggestionCloud : SuggenstionCloud = {
let cloud = SuggenstionCloud(image: UIImage(named: "suggestionCloud.png"))
cloud.translatesAutoresizingMaskIntoConstraints = false;
return cloud;
}();
override func viewDidLoad() {
super.viewDidLoad()
print("UIScaleController_DidLoad")
view.backgroundColor = UIColor(hexString: "8ED7F5")
view.addSubview(weigtImageView)
view.addSubview(textView)
view.addSubview(bottomMenu);
view.addSubview(suggestionCloud)
setUpLayout()
suggestionCloud.setLabels(weightedTags: stuff, selectedTags: selected)
}
extension UIScaleControllerVew {
func setUpLayout() {
// SuggestionCloud
suggestionCloud.setConstraints(
topAnchor: textView.bottomAnchor, topConstant: 0,
bottomAnchor: bottomMenu.topAnchor, bottomConstant: 0,
trailingAnchor: view.trailingAnchor, trailingConstant: 10,
leadingAnchor: view.leadingAnchor, leadingConstant: 10
//all UI elements are setup underneath..took those out for th
The suggestionCloud Class:
import UIKit
class SuggenstionCloud: UIImageView {
override init(image: UIImage?) {
super.init(image: image)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func setConstraints(
topAnchor : NSLayoutAnchor<NSLayoutYAxisAnchor>, topConstant: CGFloat,
bottomAnchor: NSLayoutAnchor<NSLayoutYAxisAnchor>, bottomConstant: CGFloat,
trailingAnchor: NSLayoutAnchor<NSLayoutXAxisAnchor>, trailingConstant: CGFloat,
leadingAnchor: NSLayoutAnchor<NSLayoutXAxisAnchor>, leadingConstant: CGFloat)
{
self.contentMode = .scaleToFill
self.topAnchor.constraint(equalTo: topAnchor, constant: topConstant).isActive = true;
self.bottomAnchor.constraint(equalTo: bottomAnchor, constant: bottomConstant).isActive = true;
self.trailingAnchor.constraint(equalTo: trailingAnchor, constant: trailingConstant).isActive = true;
self.leadingAnchor.constraint(equalTo: leadingAnchor, constant: leadingConstant).isActive = true;
}
public func setLabels(weightedTags: [String: Int], selectedTags: [String]) {
let buttons : [UIButton] = createButtons(weightedTags: weightedTags);
createLayout(buttons: buttons)
}
private func createButton(buttonText: String) -> UIButton {
let button = UIButton()
button.setTitle(buttonText, for: .normal)
button.titleLabel?.font = UIFont(name: "Avenir-Light", size: 20.0)
button.translatesAutoresizingMaskIntoConstraints = false;
button.backgroundColor = .blue
self.addSubview(button)
return button;
}
private func createButtons(weightedTags: [String: Int]) -> [UIButton] {
var buttons : [UIButton] = [];
for tag in weightedTags {
buttons.append(createButton(buttonText: tag.key))
}
return buttons;
}
private func createLayout(buttons : [UIButton]) {
if buttons.count == 0 { return }
let leftEdgePadding : CGFloat = 30;
let rightEdgePadding : CGFloat = 30;
let topPadding : CGFloat = 30;
let padding : CGFloat = 10;
let availableHeight : CGFloat = self.frame.height + (-2 * topPadding)
let availableWidth : CGFloat = self.frame.width + (-2 * padding)
var i = 0;
var totalHeight : CGFloat = 0;
var rowLength : CGFloat = 0;
var rowCount : Int = 0;
var lastButton : UIButton!
for button in buttons {
if totalHeight > availableHeight {print("Cloud out of space"); return}
if rowLength == 0 && rowCount == 0
{
setFirstConstraints(button: button, padding: topPadding)
totalHeight = button.intrinsicContentSize.height + topPadding;
rowLength += button.intrinsicContentSize.width + padding
lastButton = button;
}
else if rowLength + button.intrinsicContentSize.width < availableWidth
{
setConstraints(button, padding, topPadding, lastButton, rowCount)
rowLength += button.intrinsicContentSize.width + padding;
lastButton = button;
}
else
{
totalHeight += button.intrinsicContentSize.height + padding
setNewRowConstraint(button: button, padding: padding, totalHeight: totalHeight)
rowLength = 0;
rowCount += 1
lastButton = button
}
i += 1;
}
}
private func setNewRowConstraint(
button: UIButton,
padding: CGFloat,
totalHeight: CGFloat)
{
let totalPadding = button.intrinsicContentSize.height + padding + totalHeight
button.topAnchor.constraint(equalTo: self.topAnchor, constant: totalHeight).isActive = true
button.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: padding).isActive = true
}
private func setConstraints (
_ button : UIButton,
_ leftPadding: CGFloat,
_ topPadding: CGFloat ,
_ lastButton: UIButton,
_ rows: Int)
{
button.leadingAnchor.constraint(equalTo: lastButton.trailingAnchor, constant: leftPadding).isActive = true
button.topAnchor.constraint(equalTo: self.topAnchor, constant: topPadding).isActive = true
}
private func setFirstConstraints(button: UIButton, padding: CGFloat)
{
button.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: padding).isActive = true
button.topAnchor.constraint(equalTo: self.topAnchor, constant: padding ).isActive = true
}
}
THIRD: finally the issue:
I'm creating buttons dynamically to fit inside the view.
I have to set each buttion's anchors programmatically to fit their supeclass's . dimensions dynamically.
However: Inside my algorithm self.frame.size is : 800,800 : the original image size upon init().
let availableHeight : CGFloat = self.frame.height // = 800
let availableWidth : CGFloat = self.frame.width // 800 no bueno
The weird this is that the actual size of the UIView is correct in the Simulator. So the contraints work, but the Image view is not aware of it's actual dimensions
Could anyone help me figure this one out? What am i doing wrong?
Set labels is being called too soon. The frame isn't set by the time viewDidLoad is called. You need to wait until after viewDidLayoutSubviews/layoutSubviews. Using a flag and doing the set up in didLayoutSubviews should allow you to read the frame properly.
var didSetUpSuggestionCloud = false
override func viewDidLoad() {
super.viewDidLoad()
print("UIScaleController_DidLoad")
view.backgroundColor = UIColor(hexString: "8ED7F5")
view.addSubview(weigtImageView)
view.addSubview(textView)
view.addSubview(bottomMenu);
view.addSubview(suggestionCloud)
setUpLayout()
//make sure suggestionClouds setConstraints is called here
}
override func viewDidLayoutSubviews() {
guard !self.didSetUpSuggestionCloud else {
return
}
suggestionCloud.setLabels(weightedTags: stuff, selectedTags: selected)
self.didSetUpSuggestionCloud = true
}
A flag is necessary because viewDidLayoutSubviews can be called multiple times throughout a view controllers lifecycle.