UIScrollView Item not moving from the background - swift

I setup my constraints so that I would have a chart that takes up the screen when you load in, but you are able to scroll down to see more information. However, I can see the scroll bar moving, and the chart does not move!
var scrollView: UIScrollView {
let scroll = UIScrollView()
self.view.addSubview(scroll)
scroll.snp.makeConstraints { (make) in
make.edges.equalTo(self.view)
}
return scroll
}
var contentView: UIView {
let content = UIView()
self.scrollView.addSubview(content)
content.snp.makeConstraints { (make) in
make.top.bottom.equalTo(self.scrollView)
make.left.right.equalTo(self.view)
make.width.equalTo(self.scrollView)
}
// self.scrollView.contentSize = content.frame.size
return content
}
override func viewDidLoad() {
super.viewDidLoad()
self.contentView.addSubview(self.chart)
self.chart.snp.makeConstraints { (make) in
// http://snapkit.io/docs/
// https://github.com/SnapKit/SnapKit/issues/448
make.top.equalTo(self.contentView)
make.left.right.equalTo(self.contentView)
make.height.equalTo(self.view).offset(-(self.tabBarController?.tabBar.frame.height)!)
}
self.scrollView.contentSize = CGSize(width: self.view.frame.width, height: 3000) // Testing
}
So the scroll bar moves, but the content is not.

Unfortunately, you're doing a number of things wrong.
First, when you use this construct:
var scrollView: UIScrollView {
let scroll = UIScrollView()
self.view.addSubview(scroll)
scroll.snp.makeConstraints { (make) in
make.edges.equalTo(self.view)
}
return scroll
}
Every time you refer to scrollView you are creating another instance of the scroll view. Running your code, and using Debug View Hierarchy, this is what you get:
The red views are scrollViews and the blue views are contentViews. As you can see, that's definitely not what you want.
Here is an example of how you can do what you're trying to do:
class ViewController: UIViewController {
var scrollView: UIScrollView = {
let scroll = UIScrollView()
scroll.backgroundColor = .red
return scroll
}()
var contentView: UIView = {
let content = UIView()
content.backgroundColor = .blue
return content
}()
var chart: UILabel = {
let v = UILabel()
v.numberOfLines = 0
v.text = "This is the\nChart View"
v.backgroundColor = .cyan
v.textAlignment = .center
return v
}()
var bottomLabel: UILabel = {
let v = UILabel()
v.text = "The Bottom Label"
v.backgroundColor = .yellow
v.textAlignment = .center
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
// add scrollView
self.view.addSubview(scrollView)
// constrain to safe area (so it doesn't extend below the tab bar)
scrollView.snp.makeConstraints { (make) in
make.edges.equalTo(self.view.safeAreaLayoutGuide)
}
// add contentView to scrollView
self.scrollView.addSubview(contentView)
// constrain all 4 edges to scrollView
// this will allow auto-layout to control / define the .contentSize
contentView.snp.makeConstraints { (make) in
make.edges.equalTo(self.scrollView)
}
// constrain contentView's width to scrollView's width
// but we *don't* constrain its height
contentView.snp.makeConstraints { (make) in
make.width.equalTo(self.scrollView)
}
// add the chart to contentView
self.contentView.addSubview(self.chart)
// constrain chart to top, left and right of contentView
// constrain its height to scrollView's height (so it initially fills the visible area
chart.snp.makeConstraints { (make) in
make.top.equalTo(self.contentView)
make.left.right.equalTo(self.contentView)
make.height.equalTo(self.scrollView)
}
// now, we'll add a label to scrollView
self.contentView.addSubview(self.bottomLabel)
// constrain the label left and right with 20-pts, and a height of 40
bottomLabel.snp.makeConstraints { (make) in
make.left.equalTo(self.contentView).offset(20)
make.right.equalTo(self.contentView).offset(-20)
make.height.equalTo(40)
}
// constrain the label's TOP 20-pts below the BOTTOM of chart view
bottomLabel.snp.makeConstraints { (make) in
make.top.equalTo(self.chart.snp.bottom).offset(20)
}
// constrain the BOTTOM of the label 20-pts from the bottom of contentView
bottomLabel.snp.makeConstraints { (make) in
make.bottom.equalTo(self.contentView).offset(-20)
}
// with those constraints, contentView's height is now:
// chart's height + 20 + label's height + 20
// and because contentView's bottom is constrained to scrollView's bottom,
// that becomes scrollView's contentSize
}
}
I added a UILabel as the chart view, and another label below it to show how to use auto-layout to define the scroll content.
Initial view:
Scrolled up:
and, the resulting View Hierarchy:
The comments I've included in the code should clarify how and why it's done this way.

Related

Prevent cell content from "jumping" when applying constraint

I have a subclassed UICollectionViewCell and I want it to expand when tapped.
To achieve this, I put the title into a view ("titleStack") and the body into a separate view ("bodyStack"), and then put both of them into a container UIStackView ("mainStack"). I then constrain the contentView of the cell to the leading, trailing, and top edges of mainStack.
When the cell is selected, a constraint is applied that sets the bottom of the contentView's constraint to be the bottom of bodyStack. When it's unselected, I remove that constraint and instead apply one that sets the contentView's bottom constraint equal to titleStack's bottom constraint.
For the most part this works well, but when deselecting, there's this little jump, as you can see in this video:
What I would like is for titleStack to stay pinned to the top while the cell animates the shrinking portion, but it appears to jump to the bottom, giving it a sort of glitchy look. I'm wondering how I can change this.
I've pasted the relevant code below:
private func setUp() {
backgroundColor = .systemGray6
clipsToBounds = true
layer.cornerRadius = cornerRadius
setUpMainStack()
setUpConstraints()
updateAppearance()
}
private func setUpMainStack() {
contentView.constrain(mainStack, using: .edges, padding: 5, except: [.bottom])
mainStack.add([titleStack, bodyStack])
bodyStack.add([countryLabel, foundedLabel, codeLabel, nationalLabel])
}
private func setUpConstraints() {
titleStack.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
closedConstraint =
titleStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
closedConstraint?.priority = .defaultLow // use low priority so stack stays pinned to top of cell
openConstraint =
bodyStack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
openConstraint?.priority = .defaultLow
}
/// Updates the views to reflect changes in selection
private func updateAppearance() {
UIView.animate(withDuration: 0.3) {
self.closedConstraint?.isActive = !self.isSelected
self.openConstraint?.isActive = self.isSelected
}
}
Thanks so much!
I was able to solve this by simply showing and hiding my "bodyStack" as well as using "layoutIfNeeded." I removed closedConstraint and openConstraint and just gave it a normal bottom constraint.
The relevant code:
func updateAppearance() {
UIView.animate(withDuration: 0.3) {
self.bodyStack.isHidden = !self.isSelected
self.layoutIfNeeded()
}
}

How can I centre a view when using a large nav title?

I have a search view controller, I'd like to show a loading indicator in the centre of the screen, but as I'm using large title navigation, it appears to be offset but the height of the large nav?
How can I offset this so it is in the true centre of the screen?
I am setting it currently using
tableView.addSubview(searchingView)
searchingView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
searchingView.centerXAnchor.constraint(equalTo: tableView.centerXAnchor),
searchingView.centerYAnchor.constraint(equalTo: tableView.centerYAnchor)
])
My view controller is below
final class SearchViewController: UITableViewController {
private var searchLoadingController: SearchLoadingController?
private var searchController: UISearchController?
convenience init(searchLoadingController: SearchLoadingController, searchController: UISearchController) {
self.init(nibName: nil, bundle: nil)
self.searchLoadingController = searchLoadingController
self.searchController = searchController
}
override func viewDidLoad() {
super.viewDidLoad()
configureUI()
}
}
extension SearchViewController: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
guard let text = searchController.searchBar.text, !text.isEmpty else { return }
searchLoadingController?.search(query: text)
}
}
extension SearchViewController: SearchErrorView {
func display(_ viewModel: SearchErrorViewModel) { }
}
private extension SearchViewController {
func configureUI() {
navigationController?.navigationBar.prefersLargeTitles = true
navigationItem.searchController = searchController
tableView.tableFooterView = UIView(frame: .zero)
searchController?.searchBar.searchBarStyle = .minimal
searchController?.searchResultsUpdater = self
searchController?.obscuresBackgroundDuringPresentation = false
if let searchingView = searchLoadingController?.view {
tableView.addSubview(searchingView)
searchingView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
searchingView.centerXAnchor.constraint(equalTo: tableView.centerXAnchor),
searchingView.centerYAnchor.constraint(equalTo: tableView.centerYAnchor)
])
}
}
}
If you really want the activity indicator in the center of the screen you need to do some calculations:
func addActivityIndicatorToCenterOfScreen() {
let screenHeight = UIScreen.main.bounds.size.height
let activityIndicatorHeight = searchingView.bounds.size.height
let safeAreaBottomInset = tableView.safeAreaInsets.bottom
let yOffset = (screenHeight - activityIndicatorHeight) / 2 - safeAreaBottomInset
tableView.addSubview(searchingView)
searchingView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
searchingView.centerXAnchor.constraint(equalTo: tableView.centerXAnchor),
searchingView.bottomAnchor.constraint(equalTo: tableView.safeAreaLayoutGuide.bottomAnchor, constant: -yOffset),
])
}
Also ensure that you configure the constraints after your views are laid out. viewDidAppear is a good place for that. This is necessary because we need to know the size of the tableView and the safe area.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
addActivityIndicatorToCenterOfScreen()
}
Now for some explanation. tableView.safeAreaLayoutGuide includes the portion of the tableView between the navigation bar and the notch at the bottom of the screen. tableView.safeAreaInsets.bottom gives us the height of the notch at the bottom of the screen. The yOffset is the center of the screen minus half the height of the activity indicator minus the height of notch of the bottom of the screen. We will offset the bottom of the activity indicator from the bottom of the safe area. That will place the center of the activity indicator at the center of the screen.
If I were you, I would center the activity indicator between the navigation bar and the top of the notch. It's way cleaner and you can do this in viewDidLoad:
func addActivityIndicatorCenteredBetweenNavBarAndSafeAreaBottom() {
tableView.addSubview(searchingView)
searchingView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
searchingView.centerXAnchor.constraint(equalTo: tableView.safeAreaLayoutGuide.centerXAnchor),
searchingView.centerYAnchor.constraint(equalTo: tableView.safeAreaLayoutGuide.centerYAnchor),
])
}

Cocoa Swift: Subview not resizing with superview

I'm adding a subview(NSView), here is my code:
override func viewDidAppear() {
self.view.needsDisplay = true
let newView = NSView()
newView.autoresizesSubviews = true
newView.frame = view.bounds
newView.wantsLayer = true
newView.layer?.backgroundColor = NSColor.green.cgColor
view.addSubview(newView)
}
And it works fine
But when I resize the window the subview is not resizing.
Any of you knows why or how can make the subview resize with superview?
I'll really appreciate your help
You set view.autoresizesSubviews to true, which tells view to resize each of its subviews. But you also have to specify how you want each subview to be resized. You do that by setting the subview's autoresizingMask. Since you want the subview's frame to continue to match the superview's bounds, you want the subview's width and height to be flexible, and you want its X and Y margins to be fixed (at zero). Thus:
override func viewDidAppear() {
self.view.needsDisplay = true
let newView = NSView()
// The following line had no effect on the layout of newView in view,
// so I have commented it out.
// newView.autoresizesSubviews = true
newView.frame = view.bounds
// The following line tells view to resize newView so that newView.frame
// stays equal to view.bounds.
newView.autoresizingMask = [.width, .height]
newView.wantsLayer = true
newView.layer?.backgroundColor = NSColor.green.cgColor
view.addSubview(newView)
}
I found a fix for this issue:
override func viewWillLayout() {
super.viewWillLayout()
newView.frame = view.bounds
}

Snapkit centerY constraint centers item above the center Y axis

I'm trying to make a custom UICollectionView cell class. The cell consists of a content view and a label. I want the label to be in the center of the view, horizontally and vertically, but instead the label is placed above the content view's center y axis.
I've made sure that the constraints are set, no other constraints are being set, and that the issue affects all views in the content view (I added another view and set its center Y axis as a test, and that also didn't work). I also set the content view and the label's background colors to be contrasting, and have confirmed that the label is not lying on the content view's center y anchor.
Here is how I set the consraints:
label.snp.makeConstraints{make in
make.centerX.centerY.equalToSuperview()
}
Here is what I get instead. Clearly the label is not centered vertically. You can see the blue UIView, which I added as a test, is also not centered vertically.
I used to add my constraints programmatically in this way
self.view.addSubview(image)
image.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
image.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
image.heightAnchor.constraint(equalToConstant: 30).isActive = true
image.widthAnchor.constraint(equalToConstant: 30).isActive = true
and my image is declarated in this way
let image: UIImageView = {
let theImageView = UIImageView()
theImageView.image = UIImage(named: "ico_return")
theImageView.translatesAutoresizingMaskIntoConstraints = false
return theImageView
}()
Hope it helps
Can you try Following Code.
class FilterCollectionViewCell: UICollectionViewCell {
let labelTemp = UILabel()
override func awakeFromNib() {
labelTemp.backgroundColor = .white
labelTemp.textColor = .black
labelTemp.text = "testing"
self.contentView.addSubview(labelTemp)
labelTemp.snp.makeConstraints { (make) in
make.centerX.centerY.equalTo(self.contentView)
}
}
}
Fast and easy:
myLabel.snp.makeConstraints { (make) in
make.center.equalTo(self.topView.snp.center)
}

UIStackView Animation Issue

I have a subStackView inside a stackView and when I hide/show the contents of ONE subStackView, the animation goes all the way up over the other stack views: https://www.youtube.com/watch?v=vKXwX7OpkxU
This is how I create the subStackView. I tried with and without clipToBounds and with an without translatedAutoresizingMaskIntoConstraints. Also tried layoutIfNeeded in the animation part.
let subStackView = UIStackView(arrangedSubviews: [self.innerView[0], self.innerView[1])
subStackView.translatesAutoresizingMaskIntoConstraints = false
subStackView.axis = .vertical
subStackView.distribution = .fillEqually
subStackView.alignment = .fill
subStackView.spacing = 0
subStackView.clipsToBounds = true
This subStackView is then loaded into a mainStackView which results in the issue.
One way to fix your problem is to control more directly how the purple view is shown and hidden. What you're doing now (I assume) is setting isHidden property to true and then letting the stack view do whatever it wants. Instead, let's put the purple view inside a container view, and animate the container view's height down to zero. Then it can look like this:
The reason to use a container view instead of just animating the purple view's height directly is that you might (in general) have other constraints controlling the purple view's height, so also constraining its height to zero would fill up your console with unsatisfiable constraint errors.
So here's what I did for the demo. I made a “Hello, world!” label with a purple background. I constrained its height to 80. I put the label inside a container view (just a plain UIView). I constrained the top, leading, and trailing edges of the label to the container view, as normal. I also constrained the bottom edge of the label to the container view, but at priority 999* (which is less than the default, “required” priority of 1000). This means that the container view will try very hard to be the same size as the label, but if the container view is forced to change height, it will do so without affecting the label's height.
The container also has clipsToBounds set, so if the container becomes shorter than the label, the bottom part of the label is hidden.
To toggle the visibility of the label, I activate or deactivate a required-priority height constraint on the container view that sets its height to zero. Then I ask the window to lay out its children, inside an animation block.
In my demo, I also have the stack view's spacing set to 12. If I just leave the container view “visible” (not isHidden) with a height of zero, the stack view will put 12 points of space after the button, which can look incorrect. On iOS 11 and later, I fix this by setting a custom spacing of 0 after the button when I “hide” the container, and restore the default spacing when I “show” it.
On iOS version before iOS 11, I just go ahead and really hide the container (setting its isHidden to true) after the hiding animation completes. And I show the container (setting its isHidden to false) before running the showing animation. This results in a little bump as the spacing instantly disappears or reappears, but it's not too bad.
Handling the stack view spacing makes the code substantially bigger, so if you're not using spacing in your stack view, you can use simpler code.
Anyway, here's my code:
class TaskletViewController: UIViewController {
#IBAction func buttonWasTapped() {
if detailContainerHideConstraint == nil {
detailContainerHideConstraint = detailContainer.heightAnchor.constraint(equalToConstant: 0)
}
let wantHidden = !(detailContainerHideConstraint?.isActive ?? false)
if wantHidden {
UIView.animate(withDuration: 0.25, animations: {
if #available(iOS 11.0, *) {
self.stackView.setCustomSpacing(0, after: self.button)
}
self.detailContainerHideConstraint?.isActive = true
self.view.window?.layoutIfNeeded()
}, completion: { _ in
if #available(iOS 11.0, *) { } else {
self.detailContainer.isHidden = true
}
})
} else {
if #available(iOS 11.0, *) { } else {
detailContainer.isHidden = false
}
UIView.animate(withDuration: 0.25, animations: {
if #available(iOS 11.0, *) {
self.stackView.setCustomSpacing(self.stackView.spacing, after: self.button)
}
self.detailContainerHideConstraint?.isActive = false
self.view.window?.layoutIfNeeded()
})
}
}
override func loadView() {
stackView.axis = .vertical
stackView.spacing = 12
stackView.translatesAutoresizingMaskIntoConstraints = false
button.translatesAutoresizingMaskIntoConstraints = false
button.backgroundColor = UIColor.green.withAlphaComponent(0.2)
button.setTitle("Tap to toggle", for: .normal)
button.addTarget(self, action: #selector(buttonWasTapped), for: .touchUpInside)
button.setContentHuggingPriority(.required, for: .vertical)
button.setContentCompressionResistancePriority(.required, for: .vertical)
stackView.addArrangedSubview(button)
detailLabel.translatesAutoresizingMaskIntoConstraints = false
detailLabel.text = "Hello, world!"
detailLabel.textAlignment = .center
detailLabel.backgroundColor = UIColor.purple.withAlphaComponent(0.2)
detailLabel.heightAnchor.constraint(equalToConstant: 80).isActive = true
detailContainer.translatesAutoresizingMaskIntoConstraints = false
detailContainer.clipsToBounds = true
detailContainer.addSubview(detailLabel)
let bottomConstraint = detailLabel.bottomAnchor.constraint(equalTo: detailContainer.bottomAnchor)
bottomConstraint.priority = .init(999)
NSLayoutConstraint.activate([
detailLabel.topAnchor.constraint(equalTo: detailContainer.topAnchor),
detailLabel.leadingAnchor.constraint(equalTo: detailContainer.leadingAnchor),
detailLabel.trailingAnchor.constraint(equalTo: detailContainer.trailingAnchor),
bottomConstraint
])
stackView.addArrangedSubview(detailContainer)
self.view = stackView
}
private let stackView = UIStackView()
private let button = UIButton(type: .roundedRect)
private let detailLabel = UILabel()
private let detailContainer = UIView()
private var detailContainerHideConstraint: NSLayoutConstraint?
}