Using ScrollView Programmatically in Swift 3
It is easy to use constraints to define the scroll content size - so you don't have to do any manual calculations.
Just remember:
- The content elements of your scroll view must have left / top / width / height values. In the case of objects such as labels, they have intrinsic sizes, so you only have to define the left & top.
- The content elements of your scroll view also define the bounds of the scrollable area - the
contentSize
- but they do so with the bottom & right constraints. - Combining those two concepts, you see that you need a "continuous chain" with at least one element defining the top / left / bottom / right extents.
Here is a simple example, that will run directly in a Playground page:
import UIKit
import PlaygroundSupport
class TestViewController : UIViewController {
let labelOne: UILabel = {
let label = UILabel()
label.text = "Scroll Top"
label.backgroundColor = .red
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let labelTwo: UILabel = {
let label = UILabel()
label.text = "Scroll Bottom"
label.backgroundColor = .green
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let scrollView: UIScrollView = {
let v = UIScrollView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .cyan
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
// add the scroll view to self.view
self.view.addSubview(scrollView)
// constrain the scroll view to 8-pts on each side
scrollView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 8.0).isActive = true
scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 8.0).isActive = true
scrollView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -8.0).isActive = true
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -8.0).isActive = true
// add labelOne to the scroll view
scrollView.addSubview(labelOne)
// constrain labelOne to left & top with 16-pts padding
// this also defines the left & top of the scroll content
labelOne.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 16.0).isActive = true
labelOne.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 16.0).isActive = true
// add labelTwo to the scroll view
scrollView.addSubview(labelTwo)
// constrain labelTwo at 400-pts from the left
labelTwo.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 400.0).isActive = true
// constrain labelTwo at 1000-pts from the top
labelTwo.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 1000).isActive = true
// constrain labelTwo to right & bottom with 16-pts padding
labelTwo.rightAnchor.constraint(equalTo: scrollView.rightAnchor, constant: -16.0).isActive = true
labelTwo.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: -16.0).isActive = true
}
}
let vc = TestViewController()
vc.view.backgroundColor = .yellow
PlaygroundPage.current.liveView = vc
Edit - since this answer still gets occasional attention, I've updated the code to use more modern syntax, to respect the safe-area, and to use the scroll view's .contentLayoutGuide
:
class TestViewController : UIViewController {
let labelOne: UILabel = {
let label = UILabel()
label.text = "Scroll Top"
label.backgroundColor = .yellow
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let labelTwo: UILabel = {
let label = UILabel()
label.text = "Scroll Bottom"
label.backgroundColor = .green
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let scrollView: UIScrollView = {
let v = UIScrollView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .cyan
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
// add the scroll view to self.view
self.view.addSubview(scrollView)
// add labelOne to the scroll view
scrollView.addSubview(labelOne)
// add labelTwo to the scroll view
scrollView.addSubview(labelTwo)
// always a good idea to respect safe area
let safeG = view.safeAreaLayoutGuide
// we want to constrain subviews to the scroll view's Content Layout Guide
let contentG = scrollView.contentLayoutGuide
NSLayoutConstraint.activate([
// constrain the scroll view to safe area with 8-pts on each side
scrollView.topAnchor.constraint(equalTo: safeG.topAnchor, constant: 8.0),
scrollView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 8.0),
scrollView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: -8.0),
scrollView.bottomAnchor.constraint(equalTo: safeG.bottomAnchor, constant: -8.0),
// constrain labelOne to leading & top of Content Layout Guide with 16-pts padding
// this also defines the left & top of the scroll content
labelOne.topAnchor.constraint(equalTo: contentG.topAnchor, constant: 16.0),
labelOne.leadingAnchor.constraint(equalTo: contentG.leadingAnchor, constant: 16.0),
// constrain labelTwo leading at 400-pts from labelOne trailing
labelTwo.leadingAnchor.constraint(equalTo: labelOne.trailingAnchor, constant: 400.0),
// constrain labelTwo top at 1000-pts from the labelOne bottom
labelTwo.topAnchor.constraint(equalTo: labelOne.bottomAnchor, constant: 1000),
// constrain labelTwo to trailing & bottom of Content Layout Guide with 16-pts padding
// this also defines the right & bottom of the scroll content
labelTwo.trailingAnchor.constraint(equalTo: contentG.trailingAnchor, constant: -16.0),
labelTwo.bottomAnchor.constraint(equalTo: contentG.bottomAnchor, constant: -16.0),
])
}
}
Create a vertical UIScrollView programmatically in Swift
Strictly I feel problem is at below line.
self.view = scollView
It should be self.view.addSubview(scollView)
Then add all label, buttons, etc in scrollview and then give content size.
Content size is the parameter that will tell scrollview to scroll.
Put self.scrollView.contentSize = CGSize(width:2000, height: 5678)
inside viewDidLayoutSubviews
.
UIScrollView Programmatically in Swift
Your code is working perfectly, but there is a small problem in adding buttonOne on the view.
You have added buttonOne on self.view (i.e subview of self.view), not containerView which is on scrollView.
You should use the following code
containerView.addSubview(buttonOne)
instead of
self.view.addSubview(buttonOne)
Add subviews programmatically to ScrollView using Visual Format Language
each loop round, you add a right side pin constraint to scroll view for every new
added imageView, but there is 20 points between each other.
|-previous-20-newOne-| ** second round loop **
-20-newOne'-| ** third round loop **
this breaks imageView width(240) constraint
one way deal with it:
only add right side pin constraint to the last imageView.
scroll view constraint in your main storyboard also has a break.
+-------------------+
| | |
|-scroll view (240)-|
the bottom one with vertical spacing to super view should not be there. it would
break the scroll view height(240), so delete it will be fine.
maybe you should try:
- set it's constraint priority to 999, or some other value not equal to
1000 - uncheck installed box
- delete it
and now, your scroll view should be OK.
p.s. I found your reference book is based on iOS 6? in the year 2018, starts from iOS 10 or iOS 11 may be a better choice.
happy hacking :)
Add buttons to UIScrollView Swift 5 programmatically
You really should be using Auto-Layout!!!
By far, the easiest way to add buttons (or any view) to a scroll view - in a grid layout like you're asking - and have it automatically determine the scrolling area is with stack views.
Here's a quick example:
class ViewController: UIViewController {
let label = UILabel()
let scrollView = UIScrollView()
override func viewDidLoad() {
super.viewDidLoad()
// start with some text in the label
label.text = "Tap a button"
// center the text in the label
label.textAlignment = .center
// so we can see the frames
label.backgroundColor = .yellow
scrollView.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
// create a vertical stack view to hold the rows of buttons
let verticalStackView = UIStackView()
verticalStackView.axis = .vertical
// we're going to use auto-layout
label.translatesAutoresizingMaskIntoConstraints = false
scrollView.translatesAutoresizingMaskIntoConstraints = false
verticalStackView.translatesAutoresizingMaskIntoConstraints = false
// add label to view
self.view.addSubview(label)
// add Scrollview to view
self.view.addSubview(scrollView)
// add stack view to scrollView
scrollView.addSubview(verticalStackView)
// now let's create the buttons and add them
var idx = 1
for row in 0...20 {
// create a "row" stack view
let rowStack = UIStackView()
// add it to the vertical stack view
verticalStackView.addArrangedSubview(rowStack)
for col in 0...4 {
let button = UIButton()
button.backgroundColor = .gray
button.setTitle("\(idx)", for: .normal)
button.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
// add button to row stack view
rowStack.addArrangedSubview(button)
// buttons should be 50x50
NSLayoutConstraint.activate([
button.widthAnchor.constraint(equalToConstant: 50.0),
button.heightAnchor.constraint(equalToConstant: 50.0),
])
idx += 1
}
}
// finally, let's set our constraints
// respect safe-area
let safeG = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain label
// 50-pts from top
// 80% of the width
// centered horizontally
label.topAnchor.constraint(equalTo: safeG.topAnchor, constant: 50.0),
label.widthAnchor.constraint(equalTo: safeG.widthAnchor, multiplier: 0.8),
label.centerXAnchor.constraint(equalTo: safeG.centerXAnchor),
// constrain scrollView
// 50-pts from bottom of label
// Leading and Trailing to safe-area with 8-pts "padding"
// Bottom to safe-area with 8-pts "padding"
scrollView.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 50.0),
scrollView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 8.0),
scrollView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: -8.0),
scrollView.bottomAnchor.constraint(equalTo: safeG.bottomAnchor, constant: -8.0),
// constrain vertical stack view to scrollView Content Layout Guide
// 8-pts all around (so we have a little "padding")
verticalStackView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor, constant: 8.0),
verticalStackView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor, constant: 8.0),
verticalStackView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor, constant: -8.0),
verticalStackView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor, constant: -8.0),
])
}
@objc func buttonAction(sender: UIButton!) {
label.text = sender.title(for: .normal)
}
}
and this is the result:
then we'll scroll down and tap a different button:
If you want spacing between the buttons, you can add something like this at the end of viewDidLoad()
:
// suppose we want 8-pts spacing between the buttons?
verticalStackView.spacing = 8.0
verticalStackView.arrangedSubviews.forEach { v in
if let stack = v as? UIStackView {
stack.spacing = 8.0
}
}
Now it looks like this:
If you want the grid of buttons centered horizontally, or if you want the spacing (or button sizes) to adjust to fit, there are a few different ways to do that... You didn't describe your ultimate goal in your question, but this should be a good starting point.
ios Swift: ScrollView with dynamic content programmatic layout
A UIScrollView
is designed to automatically allow scrolling when its content is larger than its frame.
By itself, a scroll view has NO intrinsic size. It doesn't matter how many subviews you add to it... if you don't do something to set its frame, its frame size will always be .zero
.
If we want to get the scroll view's frame to grow in height based on its content we need to give it a height constraint when the content size changes.
If we want it to scroll when it has a lot of content, we also need to give it a maximum height.
So, if we want MyView
height to be max of 1/2 the screen (view) height, we constrain its height (in the controller) like this:
myView.heightAnchor.constraint(lessThanOrEqualTo: view.safeAreaLayoutGuide.heightAnchor, multiplier: 0.5)
and then constrain the scroll view height in MyView
like this:
let svh = scrollView.heightAnchor.constraint(equalToConstant: scrollView.contentSize.height)
svh.priority = .required - 1
svh.isActive = true
Here is a modification to your code - lots of comments in the code so you should be able to follow.
First, an example controller:
class MVTestVC: UIViewController {
let myView = MyView()
let sampleStrings: [String] = [
"Short string.",
"This is a longer string which should wrap onto a couple lines.",
"Now let's use a really, really long string. This will make the label taller, but still not enough to require vertical scrolling.",
"We want to see what happens when we DO need scrolling.\n\nSo, let's use a long string, with some embedded newlines.\n\nThis will make the label tall enough that it would exceed one-half the screen height, so we can see that we do, in fact, get vertical scrolling.",
]
var strIndex: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .gray
myView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(myView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// 20-points on each side
myView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
myView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
// centered vertically
myView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
// max 1/2 screen (view) height
myView.heightAnchor.constraint(lessThanOrEqualTo: g.heightAnchor, multiplier: 0.5),
])
myView.backgroundColor = .white
myView.configure(with: sampleStrings[0])
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
strIndex += 1
myView.configure(with: sampleStrings[strIndex % sampleStrings.count])
}
}
and the modified MyView
class:
public final class MyView: UIView {
private let titleLabel = UILabel()
private let scrollView = UIScrollView()
// this will be used to set the scroll view height
private var svh: NSLayoutConstraint!
override public init(frame: CGRect) {
super.init(frame: frame)
let closeButton = UIButton(type: .system)
closeButton.translatesAutoresizingMaskIntoConstraints = false
//(button setup)
closeButton.setTitle("X", for: [])
closeButton.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.showsVerticalScrollIndicator = false
scrollView.alwaysBounceVertical = false
titleLabel.translatesAutoresizingMaskIntoConstraints = false
//(label's font and alignment setup)
titleLabel.font = .systemFont(ofSize: 24.0, weight: .light)
titleLabel.numberOfLines = 0
let successButton = UIButton(type: .system)
successButton.translatesAutoresizingMaskIntoConstraints = false
//(button setup)
successButton.setTitle("Success", for: [])
successButton.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
addSubview(closeButton)
addSubview(scrollView)
addSubview(successButton)
scrollView.addSubview(titleLabel)
let layoutGuide = UILayoutGuide()
addLayoutGuide(layoutGuide)
let contentLayoutGuide = scrollView.contentLayoutGuide
NSLayoutConstraint.activate([
layoutGuide.leadingAnchor.constraint(equalToSystemSpacingAfter: leadingAnchor, multiplier: 2),
trailingAnchor.constraint(equalToSystemSpacingAfter: layoutGuide.trailingAnchor, multiplier: 2),
layoutGuide.topAnchor.constraint(equalToSystemSpacingBelow: topAnchor, multiplier: 2),
bottomAnchor.constraint(equalToSystemSpacingBelow: layoutGuide.bottomAnchor, multiplier: 2),
closeButton.leadingAnchor.constraint(greaterThanOrEqualTo: layoutGuide.leadingAnchor),
layoutGuide.trailingAnchor.constraint(greaterThanOrEqualTo: closeButton.trailingAnchor),
closeButton.centerXAnchor.constraint(equalTo: layoutGuide.centerXAnchor),
closeButton.topAnchor.constraint(equalTo: layoutGuide.topAnchor),
closeButton.heightAnchor.constraint(equalToConstant: 33),
scrollView.topAnchor.constraint(equalTo: closeButton.bottomAnchor),
scrollView.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: layoutGuide.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: successButton.topAnchor),
successButton.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor),
layoutGuide.trailingAnchor.constraint(equalTo: successButton.trailingAnchor),
successButton.heightAnchor.constraint(equalToConstant: 48),
layoutGuide.bottomAnchor.constraint(equalTo: successButton.bottomAnchor),
// constrain the label to the scroll view's Content Layout Guide
titleLabel.topAnchor.constraint(equalTo: contentLayoutGuide.topAnchor, constant: 16),
titleLabel.leadingAnchor.constraint(equalTo: contentLayoutGuide.leadingAnchor, constant: 16),
titleLabel.trailingAnchor.constraint(equalTo: contentLayoutGuide.trailingAnchor, constant: -16),
titleLabel.bottomAnchor.constraint(equalTo: contentLayoutGuide.bottomAnchor, constant: -16),
// label needs a width anchor, otherwise we'll get horizontal scrolling
titleLabel.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor, constant: -32),
])
layer.cornerRadius = 12
// so we can see the framing
scrollView.backgroundColor = .red
titleLabel.backgroundColor = .green
}
public override func layoutSubviews() {
super.layoutSubviews()
// we want to update the scroll view's height constraint when the text changes
if let c = svh {
c.isActive = false
}
// on initial layout, the scroll view's content size will still be zero
// so force another layout pass
if scrollView.contentSize.height == 0 {
scrollView.setNeedsLayout()
scrollView.layoutIfNeeded()
}
// constrain the scroll view's height to the height of its content
// but with a less-than-required priority so we can use a maximum height
svh = scrollView.heightAnchor.constraint(equalToConstant: scrollView.contentSize.height)
svh.priority = .required - 1
svh.isActive = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//public func configure(with viewModel: someViewModel) {
// titleLabel.text = viewModel.title
//}
public func configure(with str: String) {
titleLabel.text = str
// force the scroll view to update its layout
scrollView.setNeedsLayout()
scrollView.layoutIfNeeded()
// force self to update its layout
self.setNeedsLayout()
self.layoutIfNeeded()
}
}
Each tap anywhere on the screen will cycle through a few sample strings to change the text in the label, giving us this:
UIScrollView not scrolling at all when added programmatically in Swift?
view.contentSize = CGSize(width: self.view.bounds.width, height: self.view.bounds.height * 2)
You should try to log the contentSize
in your console after trying to access it. I am not sure if you are setting the correct contentSize
here if the self.view.bounds has been calculated correctly when this gets called at that moment. Since it takes time for self.view frame and bounds to be calculated.
Try setting your contentSize
after you have added the actual content to it based on the actual total content size.
EDIT:
Add a single UIView
inside the scrollView
, with the constraints set to top-bottom-leading-trailing, and add your subviews to it. Also, set the same constraints on the scrollView to the superView top-bottom-leading-trailing.
Programmatically display UIScrollView in Swift
you are setting the bounds of the scrollView to the size of the whole view with this code: scrollView.frame = view.bounds
.
The scrollView only needs to scroll the content in the bottom view. Scroll Views have their own content, that is normally larger than the viewable area of the screen/view. The scroll view just allows you to pan the viewport of that view.
So add the bottom view and setup your constraints on that. add the scrollView to the bottom view and then add your content into the scrollView.
Make sure that your bottom view has clipToBounds
set to true and then you should be able to keep the headers in place and just scroll the content.
I'll try and put an example together for you shortly.
EDIT:
I've just created this simple example which shows the scroll behaviour you need. This works in a playground or just as a simple view controller. I've intentionally not used auto layout or setup constraints due to time, but you will see what you need to solve your issue
class ViewController: UIViewController {
var topView: UIView!
var bottomView: UIView!
var scrollView: UIScrollView!
var contentView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
let screenSize = UIScreen.main.bounds
self.topView = UIView(frame: CGRect(x: 0, y: 0, width: screenSize.width, height: 100))
self.bottomView = UIView(frame: CGRect(x: 0, y: 100, width: screenSize.width, height: screenSize.height - 100))
self.scrollView = UIScrollView(frame: CGRect(x: 0, y: 100, width: screenSize.width, height: screenSize.height - 100))
self.contentView = UIView(frame: CGRect(x: 0, y: 0, width: screenSize.width, height: screenSize.height * 3))
self.view.backgroundColor = .white
self.view.addSubview(self.topView)
self.view.addSubview(self.bottomView)
self.bottomView.addSubview(self.scrollView)
self.scrollView.addSubview(self.contentView)
self.bottomView.clipsToBounds = true
self.scrollView.contentSize = CGSize(width: screenSize.width, height: screenSize.height * 3)
self.contentView.backgroundColor = .gray
}
}
Related Topics
Calayer with Transparent Hole in It
In iOS, How to Create a Button That Is Always on Top of All Other View Controllers
Add Entry to iOS .Plist File via Cordova Config.Xml
Date to Milliseconds and Back to Date in Swift
Swift - Clipping Image with Uibezierpath
Alamofire Invalid Value Around Character 0
Autolayout + Rtl + Uilabel Text Alignment
Using Updatechildvalues to Delete from Firebase
Rotation Methods Deprecated, Equivalent of 'Didrotatefrominterfaceorientation'
How to List Out All the Subviews in a Uiviewcontroller in iOS
How to Style Uitextview to Like Rounded Rect Text Field
Steps to Upload an iPhone Application to the Appstore
How to Turn a Cvpixelbuffer into a Uiimage
Command /Usr/Bin/Codesign Failed with Exit Code 1
Trying to Set Only Time in Uidatepicker in Swift 2.0