Unable to detect tap in UIScrollView inside another UIScrollView - ios

I have a vertical scrolling UIScrollView which spans the entire ViewController, which contains this ContentView (UIView), so I am able to detect touch event outside of the scrollViews bounds:
import UIKit
class ContentView: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let reversedSubviews = subviews.reversed()
let hitSubview = reversedSubviews
.first(where: { $0.hitTest(convert(point, to: $0), with: event) != nil })
if hitSubview != nil {
return hitSubview
}
return super.hitTest(point, with: event)
}
}
This is the View hiearchey
UIScrollView (vertical scroll) //Works
|
-- ContentView(UIView)
|
--UIScrollView (horizontal scroll) //Works
|
-- UIView
|
--UIButton //This does not work
|
--UIScrollView (horizontal scroll) //Works
|
-- UIView
|
--UIButton //This does not work
Inside this ContentView I have several other horizontal scrolling UIScrollViews. But I am not able to detect touch events inside these scrollViews, because of the ContentView.
How should I approach this to be able to detect touch event in all scrollViews, even if subviews are out of bounds?
It does not help to put ContentView inside horizontal scrollViews..

Here's a pretty basic example.
Following your hierarchy:
Main View
|
-- UIScrollView (vertical scroll) - Red
|
-- ContentView(UIView) - Medium Blue
|
-- UIScrollView (horizontal scroll) - Green
|
-- UIView - Light Gray
|
-- UIButton - Blue
|
-- UIView - Light Gray
|
-- UIButton - Blue
|
-- UIScrollView (horizontal scroll) - Green
|
-- UIView - Light Gray
|
-- UIButton - Blue
|
-- UIView - Light Gray
|
-- UIButton - Blue
|
This is how it looks - buttons can be tapped:
and, after scrolling down and scrolling each horizontal scroll view to the right:
Example code... no #IBOutlet or #IBAction connections needed, just assign a view controller's class to MultiScrollViewController:
class MultiScrollViewController: UIViewController {
let vertScrollView = UIScrollView()
let contentView = UIView()
let hScrollA = UIScrollView()
let hScrollB = UIScrollView()
override func viewDidLoad() {
super.viewDidLoad()
[vertScrollView, contentView, hScrollA, hScrollB].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
}
contentView.addSubview(hScrollA)
contentView.addSubview(hScrollB)
vertScrollView.addSubview(contentView)
view.addSubview(vertScrollView)
let g = view.safeAreaLayoutGuide
let svContentG = vertScrollView.contentLayoutGuide
let svFrameG = vertScrollView.frameLayoutGuide
NSLayoutConstraint.activate([
// vertical scroll view with 20-pts on each side for "padding"
vertScrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
vertScrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
vertScrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
vertScrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
// content view Top / Leading / Bottom 12-pts to content layout guide
contentView.topAnchor.constraint(equalTo: svContentG.topAnchor, constant: 12.0),
contentView.leadingAnchor.constraint(equalTo: svContentG.leadingAnchor, constant: 12.0),
contentView.bottomAnchor.constraint(equalTo: svContentG.bottomAnchor, constant: -12.0),
// content view Trailing to content layout guide
contentView.trailingAnchor.constraint(equalTo: svContentG.trailingAnchor, constant: 0.0),
// content view Width: scroll frame width minus 24-pts (we don't want horizontal scrolling)
contentView.widthAnchor.constraint(equalTo: svFrameG.widthAnchor, constant: -24.0),
// horizontal scroll view A Top / Leading / Trailing 8-pts to contentView
hScrollA.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8.0),
hScrollA.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8.0),
hScrollA.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -8.0),
// horizontal scroll view B Top 20-pts from Bottom of hScrollA
hScrollB.topAnchor.constraint(equalTo: hScrollA.bottomAnchor, constant: 20.0),
// horizontal scroll view B Leading / Trailing / Bottom 8-pts to contentView
hScrollB.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8.0),
hScrollB.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -8.0),
hScrollB.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8.0),
// so we have some vertical scrolling, make each horizontal scroll view
// 60% the height of the vertical scroll view frame
hScrollA.heightAnchor.constraint(equalTo: svFrameG.heightAnchor, multiplier: 0.6),
hScrollB.heightAnchor.constraint(equalTo: hScrollA.heightAnchor),
])
// each horizontal scroll view will have
// two UIViews, each with a UIButton
let btn1inA = UIButton()
let btn2inA = UIButton()
let btn1inB = UIButton()
let btn2inB = UIButton()
let btn1inAView = UIView()
let btn2inAView = UIView()
let btn1inBView = UIView()
let btn2inBView = UIView()
let btnViews = [btn1inAView, btn2inAView, btn1inBView, btn2inBView]
let btns = [btn1inA, btn2inA, btn1inB, btn2inB]
let ttls = ["Button 1 in A", "Button 2 in A", "Button 1 in B", "Button 2 in B"]
for (btn, str) in zip(btns, ttls) {
btn.setTitle(str, for: [])
btn.setTitleColor(.white, for: .normal)
btn.setTitleColor(.gray, for: .highlighted)
btn.titleLabel?.font = .boldSystemFont(ofSize: 18)
btn.backgroundColor = .blue
btn.contentEdgeInsets = UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16)
btn.translatesAutoresizingMaskIntoConstraints = false
btn.addTarget(self, action: #selector(self.btnTap(_:)), for: .touchUpInside)
}
for (btn, v) in zip(btns, btnViews) {
v.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
v.translatesAutoresizingMaskIntoConstraints = false
v.addSubview(btn)
// center each button in a view with 8-pts padding on all 4 sides
NSLayoutConstraint.activate([
btn.topAnchor.constraint(equalTo: v.topAnchor, constant: 8.0),
btn.leadingAnchor.constraint(equalTo: v.leadingAnchor, constant: 8.0),
btn.trailingAnchor.constraint(equalTo: v.trailingAnchor, constant: -8.0),
btn.bottomAnchor.constraint(equalTo: v.bottomAnchor, constant: -8.0),
])
}
// add button-holding-views to horizontal scroll views
hScrollA.addSubview(btn1inAView)
hScrollA.addSubview(btn2inAView)
hScrollB.addSubview(btn1inBView)
hScrollB.addSubview(btn2inBView)
let svAContentG = hScrollA.contentLayoutGuide
let svAFrameG = hScrollA.frameLayoutGuide
let svBContentG = hScrollB.contentLayoutGuide
let svBFrameG = hScrollB.frameLayoutGuide
NSLayoutConstraint.activate([
// button 1 in hScroll A
// top == content top + 20
btn1inAView.topAnchor.constraint(equalTo: svAContentG.topAnchor, constant: 20.0),
// leading == content leading + 20
btn1inAView.leadingAnchor.constraint(equalTo: svAContentG.leadingAnchor, constant: 20.0),
// button 2 in hScroll A
// leading == button 1 leading + 600 (so we get horizontal scrolling
btn2inAView.leadingAnchor.constraint(equalTo: btn1inAView.trailingAnchor, constant: 600.0),
// trailing == content trailing - 20
btn2inAView.trailingAnchor.constraint(equalTo: svAContentG.trailingAnchor, constant: -20.0),
// bottom to content bottom
btn2inAView.bottomAnchor.constraint(equalTo: svAContentG.bottomAnchor),
// bottom 20-pts from hScroll A frame bottom (so it's at lower-right)
btn2inAView.bottomAnchor.constraint(equalTo: svAFrameG.bottomAnchor, constant: -20.0),
// button 1 in hScroll B
// top == content top + 20
btn1inBView.topAnchor.constraint(equalTo: svBContentG.topAnchor, constant: 20.0),
// leading == content leading + 20
btn1inBView.leadingAnchor.constraint(equalTo: svBContentG.leadingAnchor, constant: 20.0),
// button 2 in hScroll A
// leading == button 1 leading + 600 (so we get horizontal scrolling
btn2inBView.leadingAnchor.constraint(equalTo: btn1inBView.trailingAnchor, constant: 600.0),
// trailing == content trailing - 20
btn2inBView.trailingAnchor.constraint(equalTo: svBContentG.trailingAnchor, constant: -20.0),
// bottom to content bottom
btn2inBView.bottomAnchor.constraint(equalTo: svBContentG.bottomAnchor),
// bottom 20-pts from hScroll B frame bottom (so it's at lower-right)
btn2inBView.bottomAnchor.constraint(equalTo: svBFrameG.bottomAnchor, constant: -20.0),
])
// some background colors so we can see the frames
view.backgroundColor = .yellow
vertScrollView.backgroundColor = .red
// medium blue
contentView.backgroundColor = UIColor(red: 0.0, green: 0.5, blue: 1.0, alpha: 1.0)
hScrollA.backgroundColor = .green
hScrollB.backgroundColor = .green
}
#objc func btnTap(_ sender: UIButton) -> Void {
print(sender.currentTitle ?? "No Button Title")
}
}

I also faced this issue when I did not had much understanding about UITableView & UICollectionView and how to avoid nesting of two scroll view where scrolling are in same direction (horizontal & horizontal OR vertical & vertical).
This problem happens generally when your internal UI elements are drawn outside of the available view port(the frame which is rendered on screen) of any screen view. Sometimes it happens due to issues with constraints set up.
How to debug?
To quickly debug, identify the parent UIViews of the buttons which are not responding. Check Clips to Bounds from Identity Inspector which is false by default. Which Clips to Bounds true, you are only seeing the views which you can interact with.
You can check the issue in the below YouTube Video:
https://youtu.be/KMxFu85sXnc
Solution:
Revisit the constraints or frame calculations for the parent views.
UICollectionView (horizontal scroll) inside another UICollectionView or UITableView(vertical scroll) might be a good approach if you are not following that.

Related

iOS: How to scroll navigation bar as view controller scrolls

I want to scroll the navigation bar as the user scrolls on the view controller. This should be similar to how the YouTube app's home page is working. When the user scrolls down, the navigation bar should be made visible. The navigation bar should move as much as the scroll amount.
I'm aware of hidesBarOnSwipe and setNavigationBarHidden, but these do not give precise control of the y-axis. I'm also reading that Apple does not support directly modifying the navigation bar frame.
So, how does YouTube do this? I'm looking for an MVP demonstrating navigation bar position change along with a UIScrollView offset change.
Without additional detail about what you want to do, I'll make some guesses.
First, the top of the YouTube app's home page is almost certainly not a UINavigationBar -- it doesn't behave like one, there is no pushing/popping of controllers going on, it's in a tab bar controller setup, etc.
So, let's assume it's a view with subviews - we'll call it a "sliding header view" - and your goal is:
don't let the header view's top scroll down
"push it up" when scrolling up
"pull it down" when scrolling down
We can accomplish this by constraining the Top of that header view to the Top of the scroll view's Frame Layout Guide.
when we start to scroll, we'll save the current .contentOffset.y
when we scroll, we'll get the relative scroll y distance
if we're scrolling Up, we'll change the Top Constraint .constant value to move the header view up
if we're scrolling Down, we'll change the Top Constraint .constant value to move the header view down
Here's how it will look at the start:
as we scroll up just a little:
after we've scrolled up farther:
as we scroll down just a little:
after we've scrolled down farther:
Here's the example code for that:
Simple two-label "header" view
class SlidingHeaderView: UIView {
// simple view with two labels
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
backgroundColor = .systemBlue
let v1 = UILabel()
v1.translatesAutoresizingMaskIntoConstraints = false
v1.text = "Label 1"
v1.backgroundColor = .yellow
addSubview(v1)
let v2 = UILabel()
v2.translatesAutoresizingMaskIntoConstraints = false
v2.text = "Label 2"
v2.backgroundColor = .yellow
addSubview(v2)
NSLayoutConstraint.activate([
v1.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12.0),
v1.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12.0),
v2.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12.0),
v2.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12.0),
v1.topAnchor.constraint(equalTo: topAnchor, constant: 8.0),
v2.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8.0),
v2.topAnchor.constraint(equalTo: v1.bottomAnchor, constant: 4.0),
v2.heightAnchor.constraint(equalTo: v1.heightAnchor),
])
}
}
example view controller
class SlidingHeaderViewController: UIViewController {
let scrollView: UIScrollView = {
let v = UIScrollView()
v.contentInsetAdjustmentBehavior = .never
return v
}()
let slidingHeaderView: SlidingHeaderView = {
let v = SlidingHeaderView()
return v
}()
let contentView: UIView = {
let v = UIView()
v.backgroundColor = .systemYellow
return v
}()
// Top constraint for the slidingHeaderView
var slidingViewTopC: NSLayoutConstraint!
// to track the scroll activity
var curScrollY: CGFloat = 0
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
[scrollView, slidingHeaderView, contentView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
}
// add contentView and slidingHeaderView to the scroll view
[contentView, slidingHeaderView].forEach { v in
scrollView.addSubview(v)
}
// add scroll view to self.view
view.addSubview(scrollView)
let safeG = view.safeAreaLayoutGuide
let contentG = scrollView.contentLayoutGuide
let frameG = scrollView.frameLayoutGuide
// we're going to change slidingHeaderView's Top constraint relative to the Top of the scroll view FRAME
slidingViewTopC = slidingHeaderView.topAnchor.constraint(equalTo: frameG.topAnchor, constant: 0.0)
NSLayoutConstraint.activate([
// scroll view Top to view Top
scrollView.topAnchor.constraint(equalTo: safeG.topAnchor, constant: 0.0),
// scroll view Leading/Trailing/Bottom to safe area
scrollView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 0.0),
scrollView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: 0.0),
scrollView.bottomAnchor.constraint(equalTo: safeG.bottomAnchor, constant: 0.0),
// constrain slidingHeaderView Top to scroll view's FRAME
slidingViewTopC,
// slidingHeaderView to Leading/Trailing of scroll view FRAME
slidingHeaderView.leadingAnchor.constraint(equalTo: frameG.leadingAnchor, constant: 0.0),
slidingHeaderView.trailingAnchor.constraint(equalTo: frameG.trailingAnchor, constant: 0.0),
// no Height or Bottom constraint for slidingHeaderView
// content view Top/Leading/Trailing/Bottom to scroll view's CONTENT GUIDE
contentView.topAnchor.constraint(equalTo: contentG.topAnchor, constant: 0.0),
contentView.leadingAnchor.constraint(equalTo: contentG.leadingAnchor, constant: 0.0),
contentView.trailingAnchor.constraint(equalTo: contentG.trailingAnchor, constant: 0.0),
contentView.bottomAnchor.constraint(equalTo: contentG.bottomAnchor, constant: 0.0),
// content view Width to scroll view's FRAME
contentView.widthAnchor.constraint(equalTo: frameG.widthAnchor, constant: 0.0),
])
// add some content to the content view so we have something to scroll
addSomeContent()
// because we're going to track the scroll offset
scrollView.delegate = self
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if slidingHeaderView.frame.height == 0 {
// get the size of the slidingHeaderView
let sz = slidingHeaderView.systemLayoutSizeFitting(CGSize(width: scrollView.frame.width, height: .greatestFiniteMagnitude), withHorizontalFittingPriority: .required, verticalFittingPriority: .defaultLow)
// use its Height for the scroll view's Top contentInset
scrollView.contentInset = UIEdgeInsets(top: sz.height, left: 0, bottom: 0, right: 0)
}
}
func addSomeContent() {
// create a vertical stack view with a bunch of labels
// and add it to our content view so we have something to scroll
let stack = UIStackView()
stack.axis = .vertical
stack.spacing = 32
stack.backgroundColor = .gray
stack.translatesAutoresizingMaskIntoConstraints = false
for i in 1...20 {
let v = UILabel()
v.text = "Label \(i)"
v.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
v.heightAnchor.constraint(equalToConstant: 48.0).isActive = true
stack.addArrangedSubview(v)
}
contentView.addSubview(stack)
NSLayoutConstraint.activate([
stack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16.0),
stack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16.0),
stack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16.0),
stack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -16.0),
])
}
}
extension SlidingHeaderViewController: UIScrollViewDelegate {
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
curScrollY = scrollView.contentOffset.y
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let diffY = scrollView.contentOffset.y - curScrollY
var newY: CGFloat = slidingViewTopC.constant - diffY
if diffY < 0 {
// we're scrolling DOWN
newY = min(newY, 0.0)
} else {
// we're scrolling UP
if scrollView.contentOffset.y <= -slidingHeaderView.frame.height {
newY = 0.0
} else {
newY = max(-slidingHeaderView.frame.height, newY)
}
}
// update slidingHeaderView Top constraint constant
slidingViewTopC.constant = newY
curScrollY = scrollView.contentOffset.y
}
}
Everything is done via code - no #IBOutlet or #IBAction connections needed.

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

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

How to make UIView which is inside scrollview adapt to screen orientation when user changes screen from portrait to landscape in swift

How to make UIView which is inside scrollview adapt to screen orientation when user changes screen from portrait to landscape in swift?
var scrollView: UIScrollView = {
var scroll = UIScrollView()
scroll.translatesAutoresizingMaskIntoConstraints = false
return scroll
}()
view.addSubview(scrollView)
scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 100).isActive = true
scrollView.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0).isActive = true
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0).isActive = true
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0).isActive = true
scrollView.heightAnchor.constraint(equalToConstant: 500).isActive = true
for i in 0..<arr.count {
var contentView = UIView()
contentView.frame = CGRect(x: i * Int(view.bounds.size.width) + 10, y: 0, width: Int(view.bounds.size.width) - 20 , height: Int(view.frame.height))
scrollView.contentSize = CGSize(width: (view.frame.size.width * CGFloat((Double(i)+1))) ,height: scrollView.frame.size.height)
}
Image
You really want to be using auto-layout instead of trying to calculate frame sizes. Let it do all the work for you.
Based on your code, it looks like you want each "contentView" to be the width of the scrollView's frame, minus 20 (so you have 10-pts of space on each side).
You can quite easily do this by embedding your contentViews in a UIStackView.
Here's a simple example:
class ViewController: UIViewController {
var scrollView: UIScrollView = {
var scroll = UIScrollView()
scroll.translatesAutoresizingMaskIntoConstraints = false
return scroll
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(scrollView)
// use a stack view to hold and arrange the scrollView's subviews
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.spacing = 20
// add the stackView to the scrollView
scrollView.addSubview(stackView)
// respect safe area
let safeG = view.safeAreaLayoutGuide
// use scrollView's Content Layout Guide to define scrollable content
let layoutG = scrollView.contentLayoutGuide
// use scrollView's Frame Layout Guide to define content height (since you want horizontal scrolling)
let frameG = scrollView.frameLayoutGuide
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: safeG.topAnchor, constant: 100),
// you're setting leading and trailing, so no need for centerX
//scrollView.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0),
scrollView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 0),
scrollView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: 0),
// let's constrain the scrollView bottom to the view (safe area) bottom
//scrollView.heightAnchor.constraint(equalToConstant: 500),
scrollView.bottomAnchor.constraint(equalTo: safeG.bottomAnchor, constant: -10.0),
// constrain Top and Bottom of the stackView to scrollView's Content Layout Guide
stackView.topAnchor.constraint(equalTo: layoutG.topAnchor),
stackView.bottomAnchor.constraint(equalTo: layoutG.bottomAnchor),
// 10-pts space on leading and trailing
stackView.leadingAnchor.constraint(equalTo: layoutG.leadingAnchor, constant: 10.0),
stackView.trailingAnchor.constraint(equalTo: layoutG.trailingAnchor, constant: -10.0),
// constrain stackView's height to scrollView's Frame Layout Guide height
stackView.heightAnchor.constraint(equalTo: frameG.heightAnchor),
])
// add some views to the stack view
let arr: [UIColor] = [
.red, .green, .blue, .yellow, .purple,
]
for i in 0..<arr.count {
let contentView = UIView()
contentView.backgroundColor = arr[i]
stackView.addArrangedSubview(contentView)
// constrain each "contentView" width to scrollView's Frame Layout Guide width minus 20
contentView.widthAnchor.constraint(equalTo: frameG.widthAnchor, constant: -20).isActive = true
// don't do this
//contentView.frame = CGRect(x: i * Int(view.bounds.size.width) + 10, y: 0, width: Int(view.bounds.size.width) - 20 , height: Int(view.frame.height))
// don't do this
//scrollView.contentSize = CGSize(width: (view.frame.size.width * CGFloat((Double(i)+1))) ,height: scrollView.frame.size.height)
}
}
}
Run that and see if that's what you're going for.
You need to add your subviews to the scroll view and setup their constraints - using of an auto-layout. Don't use contentView.frame = CGRect(...) and scrollView.contentSize = CGSize(...).
For example you can change your for-in to this:
Note: this is only example, change your for-in loop to your needs.
for i in 0..<arr.count {
// we need to distinguish the first and last subviews (because different constraints)
let topAnchor = i == 0 ? scrollView.topAnchor : scrollView.subviews.last!
let isLast = i == arr.count - 1
// here we will use a specific height for all subviews except the last one
let subviewHeight = 60
var contentView = UIView()
contentView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(contentView)
if isLast {
NSLayoutConstraint.activate([
contentView.topAnchor.constraint(equalTo: topAnchor),
contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
contentView.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor)
]
} else {
NSLayoutConstraint.activate([
contentView.topAnchor.constraint(equalTo: topAnchor),
contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
contentView.heightAnchor.constraint(equalToConstant: subviewHeight),
contentView.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor)
]
}
}

IOS swift scrollview programmatically

I am implementing a scrollview with multiple views in it programmatically. When I add subviews to it, they are not been displayed.
Here is a full example of adding 3 UIView subviews to a UIScrollView, using constraints to define the scroll view's .contentSize.
You can run this directly in a Playground page - including the ability to scroll:
import UIKit
import PlaygroundSupport
class TestViewController : UIViewController {
let redView: UIView = {
let v = UIView()
v.backgroundColor = .red
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let greenView: UIView = {
let v = UIView()
v.backgroundColor = .green
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let blueView: UIView = {
let v = UIView()
v.backgroundColor = .blue
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
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 three views to the scroll view
scrollView.addSubview(redView)
scrollView.addSubview(greenView)
scrollView.addSubview(blueView)
// give each view a height of 300
NSLayoutConstraint.activate([
redView.heightAnchor.constraint(equalToConstant: 300),
greenView.heightAnchor.constraint(equalToConstant: 300),
blueView.heightAnchor.constraint(equalToConstant: 300),
])
// give each view a width constraint equal to scrollView's width
NSLayoutConstraint.activate([
redView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
greenView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
blueView.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
])
// constrain each view's leading and trailing to the scrollView
// this also defines the width of the scrollView's .contentSize
NSLayoutConstraint.activate([
redView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
greenView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
blueView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
redView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
greenView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
blueView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
])
// constrain redView's Top to scrollView's Top + 8-pts padding
// this also defines the Top of the scrollView's .contentSize
NSLayoutConstraint.activate([
redView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 8.0),
])
// constrain greenView's Top to redView's Bottom + 20-pts spacing
NSLayoutConstraint.activate([
greenView.topAnchor.constraint(equalTo: redView.bottomAnchor, constant: 20.0),
])
// constrain blueView's Top to greenView's Bottom + 20-pts spacing
NSLayoutConstraint.activate([
blueView.topAnchor.constraint(equalTo: greenView.bottomAnchor, constant: 20.0),
])
// constrain blueView's Bottom to scrollView's Bottom + 8-pts padding
// this also defines the Bottom / Height of the scrollView's .contentSize
// Note: it must be negative
NSLayoutConstraint.activate([
blueView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: -8.0),
])
// result:
// scrollView's .contentSize.width is now
// scrollView's width (defined by subviews' leading and trailing anchors
//
// and scrollView's .contentSize.height is now
// redView-Height + 20-pts-spacing +
// greenView-Height + 20-pts-spacing +
// blueView-Height +
// 8-pts top-padding + 8-pts bottom-padding
// or 956
}
}
let vc = TestViewController()
vc.view.backgroundColor = .yellow
PlaygroundPage.current.liveView = vc

Adding Stackview to UIScrollView

I have a UIScrollView. It has a stack view. And this stack view contains 12 buttons. (Horizontal scroll view)
Stackview constraints :- top,leading,trailing,bottom to the scroll view and equal widths to the scroll view.
My problem is every time when I run, stack view width limits to the scroll view width and buttons are too small acording to the width of the stack view and my scroll view is not scrollable.
How to make this scrollable
Step-by-Step for setting this up in IB / Storyboards...
Add a view - height 50 leading/top/trailing - blue background
add a scrollview to that view - pin leading/top/trailing/bottom to 0 - set scrollview background to yellow so we can see where it is
add a button to the scroll view
duplicate it so you have 12 buttons
group them into a stack view, and set the stack view's constraints to 0 leading/top/trailing/bottom
and set the stack view's distribution to "equal spacing"
result running in simulator (with no code at all):
and the buttons scroll left and right... no code setting of .contentSize...
So you want this:
Here's how I did it in Xcode 8.3.3.
New Project > iOS > Single View Application.
Open Main.storyboard.
Drag a scroll view into the scene.
Pin top, leading, and trailing of the scroll view to 0. Set height to 30.
Drag a horizontal stack view into the scroll view.
Pin all four edges of the stack view to 0.
Set stack view spacing to 4.
Drag twelve buttons into the stack view.
Set target device to iPhone SE.
Build & run.
Resulting document outline:
If you make your Stackview width equal to the scrollview width, then that's all you'll get, and of course it won't scroll.
Don't give your Stackview a width constraint... let the buttons "fill it out".
Edit: Here is a simple example that you can run directly in a Playground page:
import UIKit
import PlaygroundSupport
class TestViewController : UIViewController {
let scrollView: UIScrollView = {
let v = UIScrollView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .cyan
return v
}()
let stackView : UIStackView = {
let v = UIStackView()
v.translatesAutoresizingMaskIntoConstraints = false
v.axis = .horizontal
v.distribution = .equalSpacing
v.spacing = 10.0
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 the stack view to the scroll view
scrollView.addSubview(stackView)
// constrain the stackview view to 8-pts on each side
// this *also* controls the .contentSize of the scrollview
stackView.leftAnchor.constraint(equalTo: scrollView.leftAnchor, constant: 8.0).isActive = true
stackView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 8.0).isActive = true
stackView.rightAnchor.constraint(equalTo: scrollView.rightAnchor, constant: -8.0).isActive = true
stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: -8.0).isActive = true
// add ten buttons to the stack view
for i in 1...10 {
let b = UIButton()
b.translatesAutoresizingMaskIntoConstraints = false
b.setTitle("Button \(i)", for: .normal)
b.backgroundColor = .blue
stackView.addArrangedSubview(b)
}
}
}
let vc = TestViewController()
vc.view.backgroundColor = .yellow
PlaygroundPage.current.liveView = vc

Resources