UIScrollView autolayout loop if content frame is less than scrool view frame - swift

Trying to put UIViewController view to scrollview for all possible screen sizes.
class ScrollableContentViewController: UIViewController {
private lazy var scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.showsVerticalScrollIndicator = false
scrollView.showsHorizontalScrollIndicator = false
return scrollView
}()
private var contentView: UIView?
override func viewDidLoad() {
super.viewDidLoad()
guard let view = self.view else {
return
}
view.removeFromSuperview()
self.view = scrollView
scrollView.addSubview(view)
self.contentView = view.superview
if let contentView = view.superview {
view.translatesAutoresizingMaskIntoConstraints = false
view.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true
view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true
view.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
}
let wc = view.widthAnchor.constraint(equalTo: scrollView.widthAnchor)
wc.priority = UILayoutPriority(rawValue: 1000)
wc.isActive = true
let hc = view.heightAnchor.constraint(equalTo: scrollView.heightAnchor)
hc.priority = UILayoutPriority(rawValue: 700)
hc.isActive = true
}
}
And here is my test ViewController in storyboard
enter image description here
The problem
if I put hc.priority = UILayoutPriority(rawValue: 100), everything works on small phones with scrolling, but on large screens autolayout loop happens
when I do hc.priority = UILayoutPriority(rawValue: 1000) everything works on large phones (no scrolling) and no scrolling on small screens - that is works as designed.
The question
Why hc.priority = UILayoutPriority(rawValue: 100) causes layout loop? But at the same time working good on small screens.
Tried to debug it, but got no sufficient results

Related

stack programmatically, giving space for bottom

i am trying to place the image below the text i add
class SolicitudViewController: BaseViewController {
lazy var imagePrincipal : UIImageView = {
let imageView = UIImageView()
imageView.image = UIImage(named: "diseƱo")
imageView.contentMode = .scaleAspectFit
imageView.translatesAutoresizingMaskIntoConstraints = false
//imageView.heightAnchor.constraint(equalToConstant: 50).isActive = true
//imageView.bottomAnchor.constraint(equalToConstant: 100).isActive = true
return imageView
}()
lazy var stackView : UIStackView = {
let stack = UIStackView()
stack.axis = .vertical
stack.distribution = .fill
stack.translatesAutoresizingMaskIntoConstraints = false
stack.addArrangedSubview(imagePrincipal)
//stack.addArrangedSubview(lblsubTitulo)
//stack.addArrangedSubview(lineView)
stack.addArrangedSubview(imageEvaluando2)
stack.addArrangedSubview(imageEvaluando3)
return stack
}()
lazy var scrollView : UIScrollView = {
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(stackView)
return scrollView
}()
override func viewDidLoad() {
super.viewDidLoad()
setSubtitle(subtitle: "test View")
}
I have tried to use this: stack.setCustomSpacing(30, after: imagePrincipal) but it positions the image on top, I want the image to be below the text

ScrollView scrollToPosition programmaticaly

i have a ViewController that containts View and One more View inside of it
Scroll works fine when i do it by my mouse, but if i use method scrollView.setContentOffset nothing happens.
I tried to check if scroll available using scrollView.delegate = works fine
UiViewController
class WishListViewController: UIViewController {
private lazy var wishListHeaderView: WishListHeaderView = {
let view = WishListHeaderView()
view.delegate = self
return view
}()
}
UiView
class WishListHeaderView: UICollectionReusableView {
private lazy var wishListNavigationView: WishListNavigationView = {
let view = WishListNavigationView()
view.delegate = self
return view
}()
}
Current view with scroll, that is not working
private lazy var scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.showsHorizontalScrollIndicator = false
return scrollView
}()
private lazy var tabsStackView: UIStackView = {
let stackView = UIStackView()
stackView.distribution = .fill
stackView.axis = .horizontal
stackView.spacing = 8
stackView.backgroundColor = PaletteApp.grayBackgroundButton
return stackView
}()
private func commonInit() {
addSubview(scrollView)
scrollView.snp.makeConstraints { make in
make.left.right.top.bottom.equalToSuperview().inset(16)
make.height.equalTo(38)
}
scrollView.addSubview(tabsStackView)
tabsStackView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
.........
Here is method in this view, debagger shows that i am in this method. And after this if i use print i see scrollviewOffset (100, 0)
func scrollToFirstTab() {
self.scrollView.setContentOffset(CGPoint(x: 100, y: 0), animated: true)
}
Where is a problem? Thank you
Content offset is not the correct method, offset is the gap between content and scroll view itself. For more about content offset, check this answer.
You need to use func scrollRectToVisible(CGRect, animated: Bool).
CGRect.zero to scroll to top.

How to use automatically/dynamically set scrollview to fit the content view

Surprisingly, this is harder than I thought. I followed this tutorial which seems rather straightforward but I am programmatically creating my view instead of using storyboard. Just to be clear, the content I add to the content view is static i.e. it's not growing or increasing.
Here are the definitions of scroll view and content view:
lazy var contentView : UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
lazy var scrollView : UIScrollView = {
let scrollView = UIScrollView(frame: .zero)
scrollView.backgroundColor = .white
scrollView.frame = self.view.bounds
scrollView.bounces = true
scrollView.autoresizingMask = .flexibleHeight
scrollView.contentSize = CGSize(width: self.view.frame.width, height: contentView.frame.height)
scrollView.translatesAutoresizingMaskIntoConstraints = false
return scrollView
}()
In view did load add the scroll view and set its constraints in the view controller:
view.addSubview(scrollView)
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
scrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
scrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
Then I add content view and the constraints:
scrollView.addSubview(contentView)
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor).isActive = true
contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor).isActive = true
contentView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
let constraint = contentView.heightAnchor.constraint(equalTo: view.heightAnchor)
constraint.priority = UILayoutPriority(250)
constraint.isActive = true
The problem is that I cannot scroll all the way to the bottom.
Initial points to note
Delete
scrollView.frame = self.view.bounds
It is pointless to give scrollView a frame, as you are going to give it a frame later through the use of constraints.
Delete
scrollView.autoresizingMask = .flexibleHeight
You are using constraints, not the autoresizing mask, to give the scroll view its frame and later resizing behavior.
Delete
scrollView.contentSize = CGSize(width: self.view.frame.width, height: contentView.frame.height)
Once the scroll view is under the influence of constraints, you must use constraints, not contentSize, to give it a content size that determines scroll behavior.
Adding the content view
With those preliminaries out of the way, let's talk about how you add the content view to the scroll view:
scrollView.addSubview(contentView)
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor).isActive = true
contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor).isActive = true
That is not exactly wrong, but it is very outmoded. You should pin the content view to the scroll view's content layout guide; that is what it is for. So, wherever you have equalTo: scrollView, change it to equalTo: scrollView.contentLayoutGuide.
Scrolling
Okay! Now everything is assembled and we are ready to talk about scrolling. What makes the scroll view scrollable, in this configuration, is that the content view is bigger than the scroll view itself. Well, so far, that's not true; in fact, the content view has no size at all. So we must proceed to give it some size.
Your attempt to do that is rather feeble. Let's extract the key lines where you give the content view height and width constraints:
contentView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
contentView.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true
(Delete the other two lines, as they are not really doing anything useful now.) So now is the content view any bigger than the scroll view? Well, maybe, but if so, only by a tiny bit, because the content view is merely the size of the main view, and the scroll view is either that same size or a tiny bit smaller.
Since we are just demonstrating, it would be better to make the content view a lot bigger than the scroll view, so we can do some major scrolling. Change the second line to this:
contentView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier:2 ).isActive = true
Yeah, baby! Now we can really scroll.
Making the content more visible
Still, it's a little hard to see what's happening (everything is white on white), so I suggest you fill the content view with some color that will permit us to see what's going on. Declare a self-drawing view as follows:
class MyView : UIView {
override class var layerClass : AnyClass { CAGradientLayer.self }
override func willMove(toSuperview newSuperview: UIView?) {
let lay = self.layer as! CAGradientLayer
lay.colors = [UIColor.red.cgColor, UIColor.green.cgColor]
}
}
Now change
let view = UIView()
to
let view = MyView()
Now it is very obvious when you scroll to the bottom; the real green is visible at the bottom.
Summary
Here is the complete code of the corrected example:
class MyView : UIView {
override class var layerClass : AnyClass { CAGradientLayer.self }
override func willMove(toSuperview newSuperview: UIView?) {
let lay = self.layer as! CAGradientLayer
lay.colors = [UIColor.red.cgColor, UIColor.green.cgColor]
}
}
class ViewController: UIViewController {
lazy var contentView : UIView = {
let view = MyView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
lazy var scrollView : UIScrollView = {
let scrollView = UIScrollView(frame: .zero)
scrollView.backgroundColor = .white
scrollView.bounces = true
scrollView.translatesAutoresizingMaskIntoConstraints = false
return scrollView
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(scrollView)
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
scrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
scrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
scrollView.addSubview(contentView)
contentView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor).isActive = true
contentView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor).isActive = true
contentView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor).isActive = true
contentView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor).isActive = true
contentView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
contentView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier:2 ).isActive = true
}
}

StackView: Overriding custom UIview intrinsicContent size produces very unexpected outcomes

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

I can't add subViews in UIScrollView

I'm trying add a subview to UIScrollView. I add scrollView, subView and set constraints bellow :
class ViewController: UIViewController {
let scrollView : UIScrollView = {
let scrollView = UIScrollView()
scrollView.backgroundColor = .yellow
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.alwaysBounceVertical = true
return scrollView
}()
let catImageView : UIImageView = {
let img = UIImageView()
img.translatesAutoresizingMaskIntoConstraints = false
img.backgroundColor = .white
return img
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
view.addSubview(scrollView)
scrollView.frame = self.view.bounds
scrollView.contentSize = CGSize(width: self.view.frame.width, height: 1000)
scrollView.addSubview(catImageView)
catImageView.centerXAnchor.constraint(equalTo: self.scrollView.centerXAnchor).isActive = true
catImageView.centerYAnchor.constraint(equalTo: self.scrollView.centerYAnchor).isActive = true
catImageView.widthAnchor.constraint(equalToConstant: 200).isActive = true
catImageView.heightAnchor.constraint(equalToConstant: 100).isActive = true
}
I builded and scrollView and subiew are disappear. I don't know why...
Then I trying add subview another way like this :
view.addSubview(catImageView)
catImageView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
catImageView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
catImageView.widthAnchor.constraint(equalToConstant: 200).isActive = true
catImageView.heightAnchor.constraint(equalToConstant: 100).isActive = true
It's still the same. Please explained to me why. Thank a lot
You should only set the translatesAutoresizingMaskIntoConstraints to false in case you are adding the constraints to the view. You've set the property on the scrollView to false, but used the frame, and not constraints. Remove the line:
scrollView.translatesAutoresizingMaskIntoConstraints = false // <- remove
and it should work.
Hope this helps! Good luck!