Disable Scrolling of UIScrollView on part of the ScrollView.frame - swift

Hello hope everyone is safe. I have a ViewController(vc) which contains a UIScrollView(scrlView). The scrlView contains two other ViewControllers(vc1 & vc2). On the vc1 I have a button which pressed adds a subview(subViewVc1) to vc1. In order to not be shown on the other scrlView page on delegation begin dragging I remove the subviewVc1. The problem I have is that I can't deactivate scrolling of the scrlView where the subViewVc1 frame is.
I have tried multiple ways as subclassing the scrollview as modifying touchesBegan, but touchesBegan recognises a touch, if the user perform even a small drag the gesture is not recognised anymore. I have tried to add a swipe gesture recogniser but I realised it interfere with the scrollview gesture.
Anybody has any idea on what to do?

Here is one way this can be achieved, instead of disabling a specific frame, you can disable interaction on over a specific view:
SubClass the UIView you add on button tap with no real implementation but just for recognition - you can also use view tags if you prefer
SubClass the UIScrollView and implement touchesShouldCancel to cancel scrolling when interacting with a specific view
Set scrollView.delaysContentTouches = false
Implementation
First I tried to recreate your situation, I created the child view controller class with a button which will go into the scrollview:
class ChildVC: UIViewController
{
let button = UIButton(type: .system)
override func viewDidLoad()
{
super.viewDidLoad()
view.backgroundColor = .yellow
configureButton()
}
private func configureButton()
{
button.setTitle("Add View", for: .normal)
button.translatesAutoresizingMaskIntoConstraints = false
button.addTarget(self,
action: #selector(didTapAddView),
for: .touchUpInside)
view.addSubview(button)
view.addConstraints([
button.leadingAnchor.constraint(equalTo: view.leadingAnchor),
button.bottomAnchor.constraint(equalTo: view.bottomAnchor),
button.trailingAnchor.constraint(equalTo: view.trailingAnchor),
button.heightAnchor.constraint(equalToConstant: 50)
])
}
#objc
private func didTapAddView()
{
let customView = UIView()
customView.backgroundColor = .white
customView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(customView)
view.addConstraints([
customView.leadingAnchor.constraint(equalTo: view.leadingAnchor,
constant: 16),
customView.topAnchor.constraint(equalTo: view.topAnchor,
constant: 16),
customView.trailingAnchor.constraint(equalTo: view.trailingAnchor,
constant: -16),
customView.bottomAnchor.constraint(equalTo: button.topAnchor,
constant: -16)
])
}
}
Then I created your container view controller with the scroll view and embedded the child vc into the view controller:
class ScrollTestViewController: UIViewController
{
private let scrollView = UIScrollView()
private let childVC1 = ChildVC()
private let childVC2 = ChildVC()
private let childVCHeight: CGFloat = 250
private let childVCWidth: CGFloat = UIScreen.main.bounds.width
override func viewDidLoad()
{
super.viewDidLoad()
view.backgroundColor = .white
title = "Scroll Test"
configureScrollView()
configureChildVC1()
configureChildVC2()
}
override func viewDidLayoutSubviews()
{
super.viewDidLayoutSubviews()
scrollView.contentSize = CGSize(width: childVC1.view.bounds.width * 2,
height: scrollView.bounds.height)
}
private func configureScrollView()
{
scrollView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(scrollView)
view.addConstraints([
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
private func configureChildVC1()
{
addChild(childVC1)
childVC1.view.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(childVC1.view)
view.addConstraints([
childVC1.view.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
childVC1.view.centerYAnchor.constraint(equalTo: scrollView.centerYAnchor),
childVC1.view.widthAnchor.constraint(equalToConstant: childVCWidth),
childVC1.view.heightAnchor.constraint(equalToConstant: childVCHeight)
])
}
private func configureChildVC2()
{
addChild(childVC2)
childVC2.view.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(childVC2.view)
view.addConstraints([
childVC2.view.leadingAnchor.constraint(equalTo: childVC1.view.trailingAnchor),
childVC2.view.centerYAnchor.constraint(equalTo: scrollView.centerYAnchor),
childVC2.view.widthAnchor.constraint(equalToConstant: childVCWidth),
childVC2.view.heightAnchor.constraint(equalToConstant: childVCHeight)
])
}
}
After doing this, you will get this result:
No different, user can scroll anywhere even when swiping on top of the newly added view.
So I made these changes:
Create a custom view subclass so I can recognize which view I am dragging over
class CustomView: UIView
{
}
Create a custom scrollview to disable interaction over specific views
fileprivate class CustomScrollView: UIScrollView
{
override func touchesShouldCancel(in view: UIView) -> Bool
{
// Just for demo, I added this
if view is CustomView
{
print("Scroll disabled, tapping custom view")
}
else
{
print("Scroll enabled")
}
// You only need this line
return !(view is CustomView)
}
}
Make the changes in the container view controller with the scroll view
// Replace private let scrollView = UIScrollView() with
private let scrollView = CustomScrollView()
// Add this as well
scrollView.delaysContentTouches = false
Make changes in the child view controllers to use Custom view when tapping the button
#objc
private func didTapAddView()
{
let customView = CustomView()
customView.backgroundColor = .white
customView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(customView)
view.addConstraints([
customView.leadingAnchor.constraint(equalTo: view.leadingAnchor,
constant: 16),
customView.topAnchor.constraint(equalTo: view.topAnchor,
constant: 16),
customView.trailingAnchor.constraint(equalTo: view.trailingAnchor,
constant: -16),
customView.bottomAnchor.constraint(equalTo: button.topAnchor,
constant: -16)
])
}
Now when you swipe over the view, it will prevent you from scrolling
Update based on Mihai's (OP's) comments
Do you have any idea how I could do it for a UIViewController instead
of UIView, Im asking because the view is currently UIViewController
and I lose some specifications about it in transforming it to an
UIView.
So on the button press the view will add a UIViewController instead of
a UIView
A scrollview can add UIViews to it's view hierarchy therefore you can only check if the view you are scrolling on is a specific type of view.
However, I still think the solution can work if you want to add a view controller on a button tap instead of a UIView.
Keep the custom view as it is:
fileprivate class CustomView: UIView
{
}
Create a view controller subclass and change the default view to be the custom view subclass
fileprivate class PurpleVC: UIViewController
{
override func loadView()
{
super.loadView()
// Change the default view from a UIView
// to be of type CustomView
view = CustomView()
view.backgroundColor = .purple
}
}
Add the new view controller on button tap instead of UIView
#objc
private func didTapAddView()
{
let customVC = PurpleVC()
customVC.view.translatesAutoresizingMaskIntoConstraints = false
addChild(customVC)
view.addSubview(customVC.view)
view.addConstraints([
customVC.view.leadingAnchor.constraint(equalTo: view.leadingAnchor,
constant: 16),
customVC.view.topAnchor.constraint(equalTo: view.topAnchor,
constant: 16),
customVC.view.trailingAnchor.constraint(equalTo: view.trailingAnchor,
constant: -16),
customVC.view.bottomAnchor.constraint(equalTo: button.topAnchor,
constant: -16)
])
}
No change to custom scroll view as it should still detect the custom view which is the main view inside your view controller
fileprivate class CustomScrollView: UIScrollView
{
override func touchesShouldCancel(in view: UIView) -> Bool
{
// Just for demo, I added this
if view is CustomView
{
print("Scroll disabled, tapping custom view")
}
else
{
print("Scroll enabled")
}
// You only need this line
return !(view is CustomView)
}
}

Related

Stretchy Header with UIPageViewController

My problem seems obvious and duplicated but I can't manage to make it work.
I'm trying to achieve the famous stretchy header effect (image's top side stuck to top of UIScrollView when scrolling), but with an UIPageViewController instead of simply an image.
My structure is:
UINavigationBar
|-- UIScrollView
|-- UIView (totally optional container)
|-- UIPageViewController (as UIView, embedded with addChild()) <-- TO STICK
|-- UIHostingViewController (SwiftUI view with labels, also embedded)
|-- UITableView (not embedded but could be)
My UIPageViewController contains images to make a carousel, nothing more.
All my views are laid out with NSLayoutConstraints (with visual format for vertical layout in the container).
I trie sticking topAnchor of the page controller's view to the one of self.view (with or without priority) but no luck, and no matter what I do it changes absolutely nothing.
I finally tried to use SnapKit but it doesn't work neither (I don't know much about it but it seems to only be a wrapper for NSLayoutConstaints so I'm not surprised it doesn't work too).
I followed this tutorial, this one and that one but none of them worked.
(How) can I achieve what I want?
EDIT 1:
To clarify, my carousel currently has a forced height of 350. I want to achieve this exact effect (that is shown with a single UIImageView) on my whole carousel:
To clarify as much as possible, I want to replicate this effect to my whole UIPageViewController/carousel so that the displayed page/image can have this effect when scrolled.
NOTE: as mentioned in the structure above, I have a (transparent) navigation bar, and my safe area insets are respected (nothing goes under the status bar). I don't think it would change the solution (as the solution is probably a way to stick the top of the carousel to self.view, no matter the frame of self.view) but I prefer you to know everything.
EDIT 2:
Main VC with #DonMag's answer:
private let info: UITableView = {
let v = UITableView(frame: .zero, style: .insetGrouped)
v.backgroundColor = .systemBackground
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
private lazy var infoHeightConstraint: NSLayoutConstraint = {
// Needed constraint because else standalone UITableView gets an height of 0 even with usual constraints
// I update this constraint in viewWillAppear & viewDidAppear when the table gets a proper contentSize
info.heightAnchor.constraint(equalToConstant: 0.0)
}()
private let scrollView: UIScrollView = {
let v = UIScrollView()
v.contentInsetAdjustmentBehavior = .never
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
...
// MARK: Views declaration
// Container for carousel
let stretchyView = UIView()
stretchyView.translatesAutoresizingMaskIntoConstraints = false
// Carousel
let carouselController = ProfileDetailCarousel(images: [
UIImage(named: "1")!,
UIImage(named: "2")!,
UIImage(named: "3")!,
UIImage(named: "4")!
])
addChild(carouselController)
let carousel: UIView = carouselController.view
carousel.translatesAutoresizingMaskIntoConstraints = false
stretchyView.addSubview(carousel)
carouselController.didMove(toParent: self)
// Container for below-carousel views
let contentView = UIView()
contentView.translatesAutoresizingMaskIntoConstraints = false
// Texts and bio
let bioController = UIHostingController(rootView: ProfileDetailBio())
addChild(bioController)
let bio: UIView = bioController.view
bio.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(bio)
bioController.didMove(toParent: self)
// Info table
info.delegate = tableDelegate
info.dataSource = tableDataSource
tableDelegate.viewController = self
contentView.addSubview(info)
[stretchyView, contentView].forEach { v in
scrollView.addSubview(v)
}
view.addSubview(scrollView)
// MARK: Constraints
let stretchyTop = stretchyView.topAnchor.constraint(equalTo: scrollView.frameLayoutGuide.topAnchor)
stretchyTop.priority = .defaultHigh
NSLayoutConstraint.activate([
// Scroll view
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
scrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
// Stretchy view
stretchyTop,
stretchyView.leadingAnchor.constraint(equalTo: scrollView.frameLayoutGuide.leadingAnchor),
stretchyView.trailingAnchor.constraint(equalTo: scrollView.frameLayoutGuide.trailingAnchor),
stretchyView.heightAnchor.constraint(greaterThanOrEqualToConstant: 350.0),
// Carousel
carousel.topAnchor.constraint(equalTo: stretchyView.topAnchor),
carousel.bottomAnchor.constraint(equalTo: stretchyView.bottomAnchor),
carousel.centerXAnchor.constraint(equalTo: stretchyView.centerXAnchor),
carousel.widthAnchor.constraint(equalTo: stretchyView.widthAnchor),
// Content view
contentView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
contentView.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor),
contentView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor, constant: 350.0),
contentView.topAnchor.constraint(equalTo: stretchyView.bottomAnchor),
// Bio
bio.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10.0),
bio.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
bio.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
bio.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
// Info table
info.topAnchor.constraint(equalTo: bio.bottomAnchor, constant: 12.0),
info.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
info.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
infoHeightConstraint
])
}
Your view hierarchy should be:
UINavigationBar
|-- UIScrollView
|-- UIView ("stretchy" container view)
|-- UIPageViewController (as UIView, embedded with asChild())
|-- UIHostingViewController (SwiftUI view with labels, also embedded)
To get the stretchy view to "stick to the top":
We constrain the stretchy view's Top to the scroll view's .frameLayoutGuide Top, but we give that constraint a less-than-required .priority so we can "push it" up and off the screen.
We also give the stretchy view a Height constraint of greater-than-or-equal-to 350. This will allow it to stretch - but not compress - vertically.
We'll call the view from the UIHostingViewController our "contentView" ... and we'll constrain its Top to the stretchy view's Bottom.
Then, we give the content view another Top constraint -- this time to the scroll view's .contentLayoutGuide, with a constant of 350 (the height of the stretchy view). This, plus the Leading/Trailing/Bottom constraints defines the "scrollable area."
When we scroll (pull) down, the content view will "pull down" the Bottom of the stretchy view.
When we scroll (push) up, the content view will "push up" the entire stretchy view.
Here's how it looks (too big to add as a gif here): https://imgur.com/a/wkThhzN
And here's the sample code to make that. Everything is done via code, so no #IBOutlet or other connections needed. Also note that I used three images for the page views - "ex1", "ex2", "ex3":
View Controller
class StretchyHeaderViewController: UIViewController {
let scrollView: UIScrollView = {
let v = UIScrollView()
v.contentInsetAdjustmentBehavior = .never
return v
}()
let stretchyView: UIView = {
let v = UIView()
return v
}()
let contentView: UIView = {
let v = UIView()
v.backgroundColor = .systemYellow
return v
}()
let stretchyViewHeight: CGFloat = 350.0
override func viewDidLoad() {
super.viewDidLoad()
// set to a greter-than-zero value if you want spacing between the "pages"
let opts = [UIPageViewController.OptionsKey.interPageSpacing: 0.0]
// instantiate the Page View controller
let pgVC = SamplePageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: opts)
// add it as a child controller
self.addChild(pgVC)
// safe unwrap
guard let pgv = pgVC.view else { return }
pgv.translatesAutoresizingMaskIntoConstraints = false
// add the page controller view to stretchyView
stretchyView.addSubview(pgv)
pgVC.didMove(toParent: self)
NSLayoutConstraint.activate([
// constrain page view controller's view on all 4 sides
pgv.topAnchor.constraint(equalTo: stretchyView.topAnchor),
pgv.bottomAnchor.constraint(equalTo: stretchyView.bottomAnchor),
pgv.centerXAnchor.constraint(equalTo: stretchyView.centerXAnchor),
pgv.widthAnchor.constraint(equalTo: stretchyView.widthAnchor),
])
[scrollView, stretchyView, contentView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
}
// add contentView and stretchyView to the scroll view
[stretchyView, contentView].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
// keep stretchyView's Top "pinned" to the Top of the scroll view FRAME
// so its Height will "stretch" when scroll view is pulled down
let stretchyTop = stretchyView.topAnchor.constraint(equalTo: frameG.topAnchor, constant: 0.0)
// priority needs to be less-than-required so we can "push it up" out of view
stretchyTop.priority = .defaultHigh
NSLayoutConstraint.activate([
// scroll view Top to view Top
scrollView.topAnchor.constraint(equalTo: view.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 stretchy view Top to scroll view's FRAME
stretchyTop,
// stretchyView to Leading/Trailing of scroll view FRAME
stretchyView.leadingAnchor.constraint(equalTo: frameG.leadingAnchor, constant: 0.0),
stretchyView.trailingAnchor.constraint(equalTo: frameG.trailingAnchor, constant: 0.0),
// stretchyView Height - greater-than-or-equal-to
// so it can "stretch" vertically
stretchyView.heightAnchor.constraint(greaterThanOrEqualToConstant: stretchyViewHeight),
// content view Leading/Trailing/Bottom to scroll view's CONTENT GUIDE
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),
// content view Top to scroll view's CONTENT GUIDE
// plus Height of stretchyView
contentView.topAnchor.constraint(equalTo: contentG.topAnchor, constant: stretchyViewHeight),
// content view Top to stretchyView Bottom
contentView.topAnchor.constraint(equalTo: stretchyView.bottomAnchor, constant: 0.0),
])
// add some content to the content view so we have something to scroll
addSomeContent()
}
func addSomeContent() {
// vertical stack view with 20 labels
// 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),
])
}
}
Controller for each Page
class OnePageVC: UIViewController {
var image: UIImage = UIImage() {
didSet {
imgView.image = image
}
}
let imgView: UIImageView = {
let v = UIImageView()
v.backgroundColor = .systemBlue
v.contentMode = .scaleAspectFill
v.clipsToBounds = true
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
view.addSubview(imgView)
NSLayoutConstraint.activate([
// constrain image view to all 4 sides
imgView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0.0),
imgView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0.0),
imgView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0.0),
imgView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0.0),
])
}
}
Sample Page View Controller
class SamplePageViewController: UIPageViewController, UIPageViewControllerDelegate, UIPageViewControllerDataSource {
var controllers: [UIViewController] = []
override func viewDidLoad() {
super.viewDidLoad()
let imgNames: [String] = [
"ex1", "ex2", "ex3",
]
for i in 0..<imgNames.count {
let aViewController = OnePageVC()
if let img = UIImage(named: imgNames[i]) {
aViewController.image = img
}
self.controllers.append(aViewController)
}
self.dataSource = self
self.delegate = self
self.setViewControllers([controllers[0]], direction: .forward, animated: false)
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
if let index = controllers.firstIndex(of: viewController), index > 0 {
return controllers[index - 1]
}
return nil
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
if let index = controllers.firstIndex(of: viewController), index < controllers.count - 1 {
return controllers[index + 1]
}
return nil
}
}
Edit
Looking at the code you posted in your question's Edit... it's a little tough, since I don't know what your ProfileDetailBio view is, but here are a couple tips to help debug this type of situation during development:
give your views contrasting background colors... makes it easy to see the frames when you run the app
if a subview fills its superview's width, make it a little narrower so you can see what's "behind / under" it
set .clipsToBounds = true on views you're using as "containers" - such as contentView... if a subview is then "missing" you know it has extended outside the bounds of the container
So, for your code...
// so we can see the contentView frame
contentView.backgroundColor = .systemYellow
// leave some space on the right-side of bio view, so we
// so we can see the contentView behind it
bio.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -100.0),
If you run the app, you will likely see that contentView only extends to the bottom of bio - not to the bottom of info.
If you then do this:
contentView.clipsToBounds = true
info will likely not be visible at all.
Checking your constraints, you have:
bio.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
// Info table
info.topAnchor.constraint(equalTo: bio.bottomAnchor, constant: 12.0),
where it should be:
// no bio bottom anchor
//bio.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
// this is correct
// Info table
info.topAnchor.constraint(equalTo: bio.bottomAnchor, constant: 12.0),
// add this
info.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
Run the app, and you should now again see info, and contentView extends to the bottom of info.
Assuming bio and info height combined are tall enough to require scrolling, you can undo the "debug / dev" changes and you should be good to go.

Auto layout with textview inside views [Swift]

I'm trying to create two views inside a main view and these view are autoresized by a textView, I want to add first one view and then by clicking a button add the seconds but I have some errors.
But I don't want to put equal height constraint to the main view, it has to resize according to textviews inside its 2 subviews
Here's a snippet of my code and an image of what i mean:
#objc func buttonAction() {
print("action")
mainView.addSubview(secondView)
firstView.topAnchor.constraint(equalTo: mainView.topAnchor).isActive = false
// I've tried to disable it to change the topAnchor of firstView
NSLayoutConstraint.activate([
secondView.bottomAnchor.constraint(equalTo: firstView.topAnchor),
secondView.leadingAnchor.constraint(equalTo: mainView.leadingAnchor),
secondView.trailingAnchor.constraint(equalTo: mainView.trailingAnchor),
secondView.topAnchor.constraint(equalTo: mainView.topAnchor)
])
}
func addConstraints() {
NSLayoutConstraint.activate([
firstView.topAnchor.constraint(equalTo: mainView.topAnchor),
firstView.bottomAnchor.constraint(equalTo: mainView.bottomAnchor),
firstView.leadingAnchor.constraint(equalTo: mainView.leadingAnchor),
firstView.trailingAnchor.constraint(equalTo: mainView.trailingAnchor),
])
NSLayoutConstraint.activate([
mainView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
mainView.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
}
class View1: UIView { // the same as View2 class
lazy var textView: UITextView = {
let tv = UITextView()
tv.backgroundColor = .red
tv.isScrollEnabled = false
tv.font = UIFont(name: "Avenir", size: 16)
tv.text = "First"
tv.translatesAutoresizingMaskIntoConstraints = false
return tv
}()
init() {
super.init(frame: .zero)
setup()
}
required init?(coder: NSCoder) {
fatalError()
}
func setup() {
addSubview(textView)
textView.fillSuperview(padding: .init(top: 4, left: 4, bottom: 4, right: 4))
// fillSuperview() put 4 in top,left,right,bottom of superview
}
}
Here's an image of what I mean:
You need to save a reference to the firstView top constraint and deactivate that reference. You were trying to deactivate a new reference to the firstView top constraint.
class ViewController: UIViewController {
var firstViewTop: NSLayoutConstraint!
func addConstraints() {
firstViewTop = firstView.topAnchor.constraint(equalTo: mainView.topAnchor)
NSLayoutConstraint.activate([
firstViewTop,
firstView.bottomAnchor.constraint(equalTo: mainView.bottomAnchor),
firstView.leadingAnchor.constraint(equalTo: mainView.leadingAnchor),
firstView.trailingAnchor.constraint(equalTo: mainView.trailingAnchor),
])
NSLayoutConstraint.activate([
mainView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
mainView.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
}
#objc func buttonAction() {
mainView.addSubview(secondView)
firstViewTop.isActive = false
NSLayoutConstraint.activate([
secondView.bottomAnchor.constraint(equalTo: firstView.topAnchor),
secondView.leadingAnchor.constraint(equalTo: mainView.leadingAnchor),
secondView.trailingAnchor.constraint(equalTo: mainView.trailingAnchor),
secondView.topAnchor.constraint(equalTo: mainView.topAnchor)
])
}
}

UITableView on UIScrollView - didSelectRowAt not called on tap (gesture recognizer traps tap)

I have a UIScrollView on which I place some text fields and a tableView.
This is not a table delegate issue. I can get didSelectRowAt if I click and drag on a row. Just the simple row tap does not get through.
I do not show the table implementation below because it generally works and I think it is the gesture bit that is causing the problem.
I have added a gesture recognizer to the UIScrollView so that if you tap on the view background you will end the editing of the current field. This works to dismiss editing and keyboard, but for a UITableView I do not get the didSelectRowAt when I tap on a row. I have to press the row and drag horizontally in order to get the didSelectRowAt method call.
Is there a way to forward the tap gesture to the UITableView? Or is it just a bad idea to use the gesture recognizer on the UIScrollView if I have a table on that view? A button on that same view will get the tap and process the button event. Not sure why the table will not get the tap. The table will scroll also properly, but does not see the row tap.
The main UIView Class Methods
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(white: 1, alpha: 1.0) // was .97
// Container
setupScrollView()
registerKeyboardNotifications()
addGestureRecognizer()
}
func setupScrollView() {
view.addSubview(scrollView)
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.contentSize = CGSize(width: view.frame.width, height: 1000)
scrollView.autoresizingMask = UIViewAutoresizing.flexibleBottomMargin
scrollView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0).isActive = true
scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0).isActive = true
scrollView.widthAnchor.constraint(equalTo: view.widthAnchor, constant: 0).isActive = true
scrollView.heightAnchor.constraint(equalTo: view.heightAnchor, constant: 0).isActive = true
}
func addGestureRecognizer() {
// When tap on the container -- end editing on current field
let tapRecognizer = UITapGestureRecognizer(target: self,action: #selector(tapDidTouch(sender: )))
self.scrollView.addGestureRecognizer(tapRecognizer)
}
#objc func tapDidTouch(sender: Any) { // this method eats UITableView clicks
self.view.endEditing(true)
}
The extension for handling keyboard
extension TestView {
func registerKeyboardNotifications() {
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self,selector: #selector(keyboardWillShow(notif:)),name: .UIKeyboardWillShow,object: nil)
notificationCenter.addObserver( self, selector: #selector(keyboardWillHide(notif:)),name: .UIKeyboardWillHide,object: nil)
notificationCenter.addObserver(self, selector: #selector(keyboardWillShow(notif:)), name: .UIKeyboardWillChangeFrame, object: nil)
}
#objc func keyboardWillShow(notif: Notification) {
guard let frame = notif.userInfo?[UIKeyboardFrameEndUserInfoKey] as? CGRect else { return}
scrollView.contentInset = UIEdgeInsets(top: 0.0,
left: 0.0,
bottom: frame.height,
right: 0.0)
}
#objc func keyboardWillHide(notif: Notification) {
scrollView.contentInset = UIEdgeInsets()
}
}

UIStackView setCustomSpacing at runtime

I have a horizontal UIStackView that, by default, looks as follows:
The view with the heart is initially hidden and then shown at runtime. I would like to reduce the spacing between the heart view and the account name view.
The following code does the job, but only, when executed in viewDidLoad:
stackView.setCustomSpacing(8, after: heartView)
When changing the custom spacing later on, say on a button press, it doesn't have any effect. Now, the issue here is, that the custom spacing is lost, once the subviews inside the stack view change: when un-/hiding views from the stack view, the custom spacing is reset and cannot be modified.
Things, I've tried:
verified the spacing is set by printing stackView.customSpacing(after: heartView) (which properly returns 8)
unsuccessfully ran several reload functions:
stackView.layoutIfNeeded()
stackView.layoutSubviews()
view.layoutIfNeeded()
view.layoutSubviews()
viewDidLayoutSubviews()
How can I update the custom spacing of my stack view at runtime?
You need to make sure the UIStackView's distribution property is set to .fill or .fillProportionally.
I created the following swift playground and it looks like I am able to use setCustomSpacing at runtime with random values and see the effect of that.
import UIKit
import PlaygroundSupport
public class VC: UIViewController {
let view1 = UIView()
let view2 = UIView()
let view3 = UIView()
var stackView: UIStackView!
public init() {
super.init(nibName: nil, bundle: nil)
}
public required init?(coder aDecoder: NSCoder) {
fatalError()
}
public override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view1.backgroundColor = .red
view2.backgroundColor = .green
view3.backgroundColor = .blue
view2.isHidden = true
stackView = UIStackView(arrangedSubviews: [view1, view2, view3])
stackView.spacing = 10
stackView.axis = .horizontal
stackView.distribution = .fillProportionally
let uiSwitch = UISwitch()
uiSwitch.addTarget(self, action: #selector(onSwitch), for: .valueChanged)
view1.addSubview(uiSwitch)
uiSwitch.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
uiSwitch.centerXAnchor.constraint(equalTo: view1.centerXAnchor),
uiSwitch.centerYAnchor.constraint(equalTo: view1.centerYAnchor)
])
view.addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
stackView.heightAnchor.constraint(equalToConstant: 50),
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 50),
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -50)
])
}
#objc public func onSwitch(sender: Any) {
view2.isHidden = !view2.isHidden
if !view2.isHidden {
stackView.setCustomSpacing(CGFloat(arc4random_uniform(40)), after: view2)
}
}
}
PlaygroundPage.current.liveView = VC()
PlaygroundPage.current.needsIndefiniteExecution = true
Another reason setCustomSpacing can fail is if you call it before adding the arranged subview after which you want to apply the spacing.
Won't work:
headerStackView.setCustomSpacing(50, after: myLabel)
headerStackView.addArrangedSubview(myLabel)
Will work:
headerStackView.addArrangedSubview(myLabel)
headerStackView.setCustomSpacing(50, after: myLabel)
I also noticed that custom spacing values get reset after hiding/unhiding children. I was able to override updateConstraints() for my parent view and set the custom spacing as needed. The views then kept their intended spacing.
override func updateConstraints() {
super.updateConstraints()
stackView.setCustomSpacing(10, after: childView)
}

StackView not filling Scrollview

I have seen several questions like this but none of the answers have managed to fix it for me.
I have a view at the bottom of the screen that contains a scrollView that contains a stackView that will be populated with buttons.
My view is built programmatically like so:
import UIKit
class BottomBar: UIView {
typealias BindTap = ((String) -> Void)?
private let scrollView = UIScrollView()
private let buttonsStackView = UIStackView()
var onTap: BindTap
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setUpViews()
setUpLayout()
}
private func setUpViews() {
backgroundColor = .cyan
scrollView.backgroundColor = .red
buttonsStackView.backgroundColor = .green
buttonsStackView.alignment = .fill
buttonsStackView.distribution = .equalSpacing
buttonsStackView.axis = .horizontal
buttonsStackView.spacing = 5
scrollView.addSubview(buttonsStackView)
addSubview(scrollView)
}
private func setUpLayout() {
buttonsStackView.pinToSuperview(edges: [.top, .bottom, .left, .right],
constant: 5,
priority: .defaultHigh)
scrollView.pinToSuperview(edges: [.top, .bottom, .left, .right],
constant: 0,
priority: .defaultHigh)
}
func addModelButtons(models: [Model]) {
models.forEach { model in
let modelButton = UIButton()
modelButton.backgroundColor = .lightGray
modelButton.setTitle(model.fileName, for: .normal)
modelButton.addTarget(self, action: #selector(modelButtonTapped), for: .touchUpInside)
buttonsStackView.addArrangedSubview(modelButton)
if let first = models.first,
first.fileName == model.fileName {
updateSelectedButtonColor(modelButton)
}
}
}
#objc private func modelButtonTapped(button: UIButton) {
guard let modelName = button.titleLabel?.text else { return }
onTap?(modelName)
resetButtonColors()
updateSelectedButtonColor(button)
}
private func resetButtonColors() {
for case let button as UIButton in buttonsStackView.subviews {
button.backgroundColor = .lightGray
}
}
private func updateSelectedButtonColor(_ button: UIButton) {
button.backgroundColor = .darkGray
}
}
I cant see what is missing. I've added a picture so you can see that the stackView is wrapping around the buttons and not filling the scrollview.
Any help would be great. Im sure its a simple fix i just cant see it!
Here's the thing to understand about scrollViews. Unless you give the content area of a scrollView an explicit size, it will determine its size from its subviews.
In your case, you've told it that your stackView is 5 points away from the edges of the scrollView. That ties the size of the stackView to the size of the content area of the scrollView. At this point, the stackView is controlling the size of the scrollable area of the scrollView. Since your stackView only has 2 buttons, the stackView shrinks to the size of those two buttons and the scrollable area of the scrollView is 10 wider than that. Since the buttons are small, this does not fill the screen.
What you want is that the buttons stretch to fill the apparent size of the scrollView. In order for that to happen, you need to tell Auto Layout that the stackView's width must be greater than or equal to the width of the scrollView - 10.
Add this constraint after scrollView.addSubview(buttonsStackView):
buttonsStackView.widthAnchor.constraint(greaterThanOrEqualTo: scrollView.widthAnchor, multiplier: 1, constant: -10).isActive = true