Change touch receiver while scrolling a scroll view - ios

Apps like Apple's maps app or Google maps use scrollable bottom sheet overlays to present additional content. While this behavior is not too difficult to rebuild, I struggle to implement one important feature:
When there is a scroll view embedded inside the bottom sheet, then the user can scroll it to the top but then – instead of bouncing off at the top – the bottom sheet starts scrolling down instead of the table view.
Here's an example video of what I mean:
Example Video:
This is a nice user experience as there is no interruption in the scrolling and it's what I expect as a user: It's as if once the content scroll view has reached its top the gesture receiver is automatically handed over the super scroll view.
In order to achieve this behavior, I see three different approaches:
I track the content scroll view's contentOffset in the scroll view's scrollViewDidScroll(_:) delegate method. Then I do
if contentScrollView.contentOffset.y < 0 {
contentScrollView.contentOffset.y = 0
}
to keep the content scroll view from scrolling above the top of its content. Instead, I pass the y distance that it would have scrolled to the super scroll view which scrolls the whole bottom sheet.
I find a way to change the receiver of the scrolling (pan) gesture recognizer from the content scroll view to the super scroll view as soon as the content scroll view has scrolled to its top.
I handle everything inside the super scroll view. It asks its content view controller through a delegate protocol if it wants to handle the touches and only if it doesn't (because its content scroll view has reached the top) the super scroll view scrolls by itself.
While I have managed to implement the first variant (it's what you see in the video), I'd strongly prefer to use approach 2 or 3. It's a much cleaner way to have the view controller that controls the bottom sheet manage all the scrolling logic without exposing its internals.
Unfortunately, I haven't found a way to somehow split the pan gesture into two components (one that controls the receiver scroll view and one that controls another scroll view)
Any ideas on how to achieve this kind of behavior?

I am very interested in this question and I hope by providing how I would implement it, it does not stifle an answer that might show how to truly pass around the responder. The trick I think which I put in the comments is keeping track of the touches. I forgot about how scrollview gobbles those up but you can use a UIPanGesture. See if this is close to what you are looking for. The only case I ran into that might take more thought is using the scroll to dismiss the bottom view. Most of this code is setup to get a working scrollview in the view. I think property animations might be best to make it interruptible or even my personal fav Facebook Pop animations. To keep it simple I just used UIView animations. Let me know if this solves what you are looking for. The code is below and here is the result
. The scrollview remains scrollable and active. I animate the frames but updating constraints could work as well.
import UIKit
class ViewController: UIViewController{
//setup
var items : [Int] = []
lazy var tableView : UITableView = {
let tv = UITableView(frame: CGRect(x: 0, y: topViewHeight, width: self.view.frame.width, height: self.view.frame.height))
tv.autoresizingMask = [.flexibleWidth,.flexibleHeight]
tv.delegate = self
tv.dataSource = self
tv.layer.cornerRadius = 4
return tv
}()
lazy var topView : UIView = {
let v = UIView(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: topViewHeight))
v.backgroundColor = .green
v.autoresizingMask = [.flexibleWidth,.flexibleHeight]
return v
}()
let cellIdentifier = "ourCell"
//for animation
var isAnimating = false
var lastOffset : CGPoint = .zero
var startingTouch : CGPoint?
let topViewHeight : CGFloat = 500
var isShowing : Bool = false
let maxCollapse : CGFloat = 50
override func viewDidLoad() {
super.viewDidLoad()
for x in 0...100{
items.append(x)
}
// Do any additional setup after loading the view, typically from a nib.
self.view.addSubview(topView)
self.view.addSubview(tableView)
self.tableView.reloadData()
let pan = UIPanGestureRecognizer(target: self, action: #selector(moveFunction(pan:)))
pan.delegate = self
self.view.addGestureRecognizer(pan)
}
#objc func moveFunction(pan:UIPanGestureRecognizer) {
let point:CGPoint = pan.location(in: self.view)
switch pan.state {
case .began:
startingTouch = point
break
case .changed:
processMove(touchPoint:point.y)
break
default:
processEnding(currentPointY: point.y)
break
}
}
}
extension ViewController : UITableViewDelegate,UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell : UITableViewCell!
cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier)
if cell == nil {
cell = UITableViewCell(style: .default, reuseIdentifier: cellIdentifier)
}
cell.textLabel?.text = "\(items[indexPath.row])"
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 30
}
}
extension ViewController : UIScrollViewDelegate{
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if isAnimating == true{
scrollView.contentOffset = lastOffset
return
}
lastOffset = scrollView.contentOffset
}
}
extension ViewController : UIGestureRecognizerDelegate{
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}
extension ViewController{
func processMove(touchPoint:CGFloat){
if let start = startingTouch{
if touchPoint <= topViewHeight && start.y > topViewHeight{
isAnimating = true
tableView.frame = CGRect(x: 0, y:touchPoint, width: self.view.frame.width, height: self.view.frame.height)
return
}else if touchPoint >= self.maxCollapse && isShowing == true && start.y < self.maxCollapse{
isAnimating = true
tableView.frame = CGRect(x: 0, y:touchPoint, width: self.view.frame.width, height: self.view.frame.height)
return
}else if isShowing == true && self.tableView.contentOffset.y <= 0{
//this is the only one i am slightly unsure about
isAnimating = true
tableView.frame = CGRect(x: 0, y:touchPoint, width: self.view.frame.width, height: self.view.frame.height)
return
}
}
self.isAnimating = false
}
func processEnding(currentPointY:CGFloat){
startingTouch = nil
if isAnimating{
if currentPointY < topViewHeight/2{
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.0, options: .curveEaseInOut, animations: {
self.tableView.frame = CGRect(x: 0, y:self.maxCollapse, width: self.view.frame.width, height: self.view.frame.height)
}) { (finished) in
self.isShowing = true
}
}else{
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.0, options: .curveEaseInOut, animations: {
self.tableView.frame = CGRect(x: 0, y:self.topViewHeight, width: self.view.frame.width, height: self.view.frame.height)
}) { (finished) in
self.isShowing = false
}
}
}
self.isAnimating = false
}
}

Related

Swift: Changing (translate) a UIView position through a pan gesture in its superview window

Introduction
Context:
In my main ViewController I have a scrollView with a few objects inside (which are UIViews). When one of the UIViews are tapped/selected I animate forward a UITextView in a UIView to go with the selected object. (only one UIView can appear at a time)
This UIView that appears on object selection is separated into a separate class called AdjunctiveTextView.
Issue/goal:
(the example code provided below will clear make this clear, I've also commented where the issue lies in the code)
When an object has been tapped and has an adjacent UIView with a text I want to have that adjacent UIView to follow with the scrollView.
I'm using a UIPanGestureRecognizer to attempt to do this. But I can't figure out how to make it work when the user drags in the scrollview. It only work if the user drags on the actual adjunctiveTextView.
Everything works as expected except that the adjunctiveTextView does not change its position during the panGesture.
I would like (if possible) to have the AdjunctiveTextView as a separate class. My ViewController file is getting rather big.
Question:
Why doesn't the UIPanGestureRecognizer work as expected? What is needed in order for it to translate the backView correctly?
Code
My attempt: (as shown below)
My attempt simply makes the backView itself "dragable" around through the panGesture. Nothing happens to it when I scroll the scrollView.
(I have only included relevant portions of my code)
class ViewController: UIViewController {
let adjunctiveTextView = AdjunctiveTextView()
// this is a delegate method which gets called when an object is tapped in the scrollView
func scrollViewObjectIsTapped(_ objectScrollView: ObjectScrollView, object: AvailableObject) {
** adjunctiveTextView.scrollView = scrollView // **Edited! (scrollView is the name of the scrollView in this class too)
adjunctiveTextView.showView(passInObject: AvailableObject)
}
}
class AdjunctiveTextView: NSObject {
lazy var backView: UIView = {
//backView setup
}
lazy var textView: UITextView = {
//textView setup
}
//additional init and setup
** weak var scrollView : UIScrollView! // **Edited!
func showView(passInObject: AvailableObject) {
if let window = UIApplication.shared.keyWindow {
// the issue must either be here in the PanGesture setup
let panG = UIPanGestureRecognizer(target: self, action: #selector(translateView(sender:)))
panG.cancelsTouchesInView = false
// window.addGestureRecognizer(panG)
** scrollView.addGestureRecognizer(panG) // **Edited!
window.addSubview(backView)
textView.text = passInObject.information
backView.frame = CGRect(x: passInObject.frame.minX, y: passInObject.minY, width: window.frame.width - passInObject.maxX - 6, height: textView.bounds.height + 5)
backView.alpha = 0
//it animates a change of the backViews x position and alpha.
UIView.animate(withDuration: 0.42, delay: 0, options: .curveEaseInOut, animations: {
self.backView.alpha = 1
self.backView.frame = CGRect(x: passInObject.frame.minX + passInObject.frame.width, y: passInObject.minY, width: window.frame.width - passInObject.maxX - 6, height: textView.bounds.height + 5)
}, completion: nil)
}
}
// or the issue is here in the handle function for the PanGesture.
#objc private func translateView(sender: UIPanGestureRecognizer) {
if let window = UIApplication.shared.keyWindow {
let translation = sender.translation(in: window) //Have tried setting this to scrollView also
switch sender.state {
case .began, .changed:
backView.center = CGPoint(x: backView.center.x, y: backView.center.y + translation.y)
sender.setTranslation(CGPoint.zero, in: window) //Have tried setting this to sccrollView also
break
case .ended:
break
default:
break
}
}
}
}
Thanks for reading my question.
I just add a weak reference to your scrollView and then add the pan gesture to scrollView. It works as you want. You may consider add another pan gesture to the back view if you want your original behavior.
class AdjunctiveTextView: NSObject {
lazy var backView: UIView = {
//backView setup
return UIView.init()
}()
lazy var textView: UITextView = {
//textView setup
return UITextView.init(frame: CGRect.init(x: 0, y: 0, width: 300, height: 100))
}()
weak var scrollView: UIScrollView!
//additional init and setup
func showView(passInObject: AvailableObject) {
if let window = UIApplication.shared.keyWindow {
// the issue must either be here in the PanGesture setup
let panG = UIPanGestureRecognizer(target: self, action: #selector(translateView(sender:)))
panG.cancelsTouchesInView = false
// passInObject.addGestureRecognizer(panG)
scrollView.addGestureRecognizer(panG)
window.addSubview(backView)
textView.text = passInObject.information
textView.backgroundColor = UIColor.blue
backView.addSubview(textView)
backView.frame = CGRect(x: passInObject.frame.minX, y: passInObject.frame.minY, width: window.frame.width - passInObject.frame.maxX - 6, height: textView.bounds.height + 5)
backView.alpha = 0
//it animates a change of the backViews x position and alpha.
UIView.animate(withDuration: 0.42, delay: 0, options: .curveEaseInOut, animations: {
self.backView.alpha = 1
self.backView.frame = CGRect(x: passInObject.frame.minX + passInObject.frame.width , y: passInObject.frame.minY , width: window.frame.width - passInObject.frame.maxX - 6, height: self.textView.bounds.height + 5)
self.backView.backgroundColor = UIColor.red
}, completion: nil)
}
}
// or the issue is here in the handle function for the PanGesture.
#objc private func translateView(sender: UIPanGestureRecognizer) {
if let window = UIApplication.shared.keyWindow {
let translation = sender.translation(in: window)
switch sender.state {
case .began, .changed:
backView.center = CGPoint(x: backView.center.x, y: backView.center.y + translation.y)
sender.setTranslation(CGPoint.zero, in: window)
break
case .ended:
break
default:
break
}
}
}
}
class ObjectScrollView: UIScrollView{
}
class AvailableObject: UIView{
var information: String!
}
class MySCNViewController: UIViewController {
#IBOutlet weak var oScrollView: ObjectScrollView!
// this is a delegate method which gets called when an object is tapped in the scrollView
func scrollViewObjectIsTapped(_ objectScrollView: ObjectScrollView, object: AvailableObject) {
adjunctiveTextView.showView(passInObject: object)
}
let adjunctiveTextView = AdjunctiveTextView()
let ao = AvailableObject.init(frame: CGRect.init(x: 0, y: 0, width: 200, height: 200))
override func viewDidLoad() {
super.viewDidLoad()
ao.information = "test"
adjunctiveTextView.scrollView = oScrollView
ao.backgroundColor = UIColor.yellow
}
#IBAction func tap(_ sender: Any?){
scrollViewObjectIsTapped(oScrollView, object: ao)}
}

Increase the Height of the Tableview when the Keyboard Disappears?

I would like to increase the height of a tableview whenever the keyboard disappears. I have a tableview that is populated from the bottom upwards. The tableview is not filled entirely with data, meaning there is some empty space at the top of the tableview. There will be some cells in the tableview. Since the table view is populated from the bottom upwards, these cells should be towards the bottom of the tableview. When the keyboard is shown, the total height of the tableview is about half of the screen. When the keyboard disappears, the total height of the tableview should be almost the whole screen. This means that when the keyboard disappears, the cells should start from further down on the screen.
This is what it looks like when there are two cells in the tableview:
This is what it looks like when two more cells have been added to the tableview:
This is what it looks like when the keyboard disappears:
When the keyboard disappears, the four cells should start from the bottom of the screen, not from halfway up the screen. The four cells should move downwards so that they start from where the gray line at the bottom of the screen is. There should not be a big gap between where the first cell (which says "Apple") is and where the gray line at the bottom of the screen is. At the same time, the tableView itself should be bigger. This means that while the top of the tableview remains in the same place, the bottom of the tableview is now lower than it was before, because the keyboard is now not taking up approximately half of the screen. How can I do this? Thanks.
Here is my code right now:
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var textField: UITextField!
#IBOutlet weak var lowerGrayLine: UIView!
#IBOutlet weak var tableView: UITableView!
var fruits = [FruitModel]()
override func viewDidLoad() {
super.viewDidLoad()
//Adjust height of the lower Gray Line
lowerGrayLine.frame.origin.y = 411
//No dividing lines in the tableview initially
self.tableView.separatorStyle = .none
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 140
if (self.tableView(self.tableView, numberOfRowsInSection: 0) > 0)
{
self.updateTableContentInset()
}
// Allow for keyboard dismissal
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(ViewController.dismissKeyboard))
view.addGestureRecognizer(tap)
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.keyboardWillShow), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.keyboardWillHide), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
// Show keyboard
textField.becomeFirstResponder()
}
func keyboardWillShow(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
//Adjust text box, and dividing line placement
textField.frame.origin.y = 416
lowerGrayLine.frame.origin.y = 411
}
}
func keyboardWillHide(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue {
textField.frame.origin.y = 597
lowerGrayLine.frame.origin.y = 557
}
}
//Calls this function when the tap is recognized.
func dismissKeyboard() {
//Causes the view (or one of its embedded text fields) to resign the first responder status.
view.endEditing(true)
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.fruits.count;
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "fruitCell"
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? FruitTableViewCell else {
fatalError("The dequeued cell is not an instance of FruitTableViewCell.")
}
var fruitName = fruits[indexPath.row]
var fruitNamename = fruitName.name
var colon = ":"
cell.nameLabel.text = fruitNamename + colon
cell.fruitLabel.text = fruitName.fruit
//Make some adjustments to make a line appear when a fruitCell is actually being shown
cell.preservesSuperviewLayoutMargins = false
cell.separatorInset = UIEdgeInsets.zero
cell.layoutMargins = UIEdgeInsets.zero
// This causes the bottom-most cell to not have a cell separator
if (indexPath.row == fruits.count-1) {
cell.separatorInset = UIEdgeInsetsMake(0, cell.bounds.size.width, 0, 0);
}
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
// Adjust the tableView
func updateTableContentInset() {
let numRows = tableView(tableView, numberOfRowsInSection: 0)
var contentInsetTop = tableView.bounds.size.height
for i in 0..<numRows {
contentInsetTop -= tableView(tableView, heightForRowAt: IndexPath(item: i, section: 0))
if contentInsetTop <= 0 {
contentInsetTop = 0
}
}
self.tableView.contentInset = UIEdgeInsetsMake(contentInsetTop, 0, 0, 0)
self.tableView.scrollToRow(at: IndexPath(item: numRows-1, section: 0), at: .bottom, animated: false)
//Prevent scrolling
self.tableView.bounces = false;
//When we have actual fruit Cells to show, we want the line divisions
self.tableView.separatorStyle = .singleLine
}
}
I have tried adding the following code to my keyboardWillShow function:
var tableViewFrame: CGRect!
tableViewFrame = self.tableView.frame
tableViewFrame.size.height += 146.0
tableView.frame = tableViewFrame
tableView.contentInset = UIEdgeInsetsMake(146.0, 0, 0, 0);
However, when the keyboard disappears, there are horizontal gray lines that span the screen, as shown below:
EDIT:
These horizontal gray lines (six in total) should not be there. How do I prevent them from appearing when the keyboard has disappeared?
The simplest solution is IQKeyboardManager. This will manage the space below keyboard for you.
Otherwise, add a bottom constraint tableViewBottomConstraint to the table view. Set its initial value tableViewBottomConstraint.constant = 0
Now on set it equal to the keyboard height + padding in keyboardWillShow keyboardWillHide
func keyboardWillShow(notification: NSNotification) {
tableViewBottomConstraint.constant = keyboardHeight + padding
layoutTableView()
}
func keyboardWillHide(notification: NSNotification) {
tableViewBottomConstraint.constant = 0
layoutTableView()
}
private func layoutTableView() {
UIView.animate(withDuration: 0.3, animations: {
tableView.superview?.layoutIfNeeded()
}, completion: nil)
}
To do this you need to write your code in keyboardWillHide:
Resize the tableView increasing its frame's height (or the relative constraint's constant value) of the keyboard's height;
Key point: set tableView's top content inset equal to the keyboard's height.
So for instance if the keyboard's height is 216pt, you will do:
CGRect tableViewFrame = tableView.frame;
tableViewFrame.size.height += 216.0;
tableView.frame = tableViewFrame;
tableView.contentInset = UIEdgeInsetsMake(216.0, 0, 0, 0);
If you are using constraints instead, the only difference is that you'll change the height of the constraint's constant relative to the height of the tableView.
P.s. if you want a deeper explanation of what UIEdgeInsets is, take a look here: https://developer.apple.com/documentation/uikit/uiedgeinsets
They have been made exactly to create empty space with a specific offset.
func keyboardWillShow(notification:NSNotification) {
if let keyboardSize = notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? CGRect {
let contentInsets = UIEdgeInsets(top: 0, left: 0, bottom:
keyboardSize.height, right: 0)
print(contentInsets)
btnBottomConstrain.constant = contentInsets.bottom
UIView.animate(withDuration: 0.2, delay: 0, options: .transitionCurlDown, animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
}
func keyboardWillHide(notification:NSNotification) {
if let keyboardSize = notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? CGRect {
let contentInsets = UIEdgeInsets(top: 0, left: 0, bottom: keyboardSize.height, right: 0)
print(contentInsets)
self.btnBottomConstrain.constant = 0
UIView.animate(withDuration: 0.2, delay: 0, options: .transitionCurlDown, animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
}
replace these two methods with your keyboardshow and keybordhide methods accordingly. here btnBottomConstrain will be the outlet of your tableview's bottom constraint. I hope this will work for you.

loop reseting in scrollview (swift3)

My code is a uiscrolview with two pages and a button. The problem is that if I am on the view controller info (middle image). Then hit the button to go back to scrollview it goes back to page 1. So no matter what going from the info slide to the scrollview it always goes back to view controller 1 and the loop resets. If I am on vc 2 hit the info button go to the info slide.If I hit the button on the info slide I want to return to vc 2. My code is below. Essentially the loop always resets I want to fix that if possible.
import UIKit
class ViewController: UIViewController, UIScrollViewDelegate {
#IBOutlet var pageControl: UIPageControl!
#IBOutlet var slideScrollView: UIScrollView!
override func viewDidLoad() {
super.viewDidLoad()
slideScrollView.delegate = self
let slides:[slide] = createSlides()
setupSlideScrollView(slides: slides)
pageControl.numberOfPages = slides.count
pageControl.currentPage = 0
view.bringSubview(toFront: pageControl)
}
override var prefersStatusBarHidden: Bool {
return true
}
func createSlides() -> [slide]{
let slide1:slide = Bundle.main.loadNibNamed("slide", owner: self, options: nil)?.first as! slide
slide1.label.text = "1"
let slide2:slide = Bundle.main.loadNibNamed("slide", owner: self, options: nil)?.first as! slide
slide2.label.text = "2"
return [slide1,slide2 ]
}
func setupSlideScrollView(slides:[slide]) {
slideScrollView.frame = CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height)
slideScrollView.contentSize = CGSize(width: view.frame.width * CGFloat(slides.count), height: view.frame.height)
for i in 0..<slides.count{
slides[i].frame = CGRect(x: view.frame.width * CGFloat(i), y: 0, width: view.frame.width, height: view.frame.height)
slideScrollView.addSubview(slides[i])
slideScrollView.isPagingEnabled = true
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let pageIndex = round(scrollView.contentOffset.x/view.frame.width)
pageControl.currentPage = Int(pageIndex)
}}
This sounds like a ViewWillAppear or ViewWillAppear problem , check if you are setting anything in those functions.

UIScrollView doesn't scroll upwards sometimes

I am using 2 scroll Views When the inner Scroll View is scrolled upwards, outer scrollView also moves upwards(positive contentoffset.y) and when inner SV's contentoffset.y goes less than y, both scroll Views come back to their original positions.
Here is the code:
//secondView is a UIView inside outerScrollView and above inner SV(scrollView).
var scollView: UIScrollView!
var yOffset: CGFloat!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.automaticallyAdjustsScrollViewInsets = true
self.outerScrollView.delegate = self
self.scrollView = UIScrollView(frame: CGRect(x: 0, y: self.secondView.frame.origin.y + self.secondView.frame.height, width: self.view.frame.width, height: self.view.frame.height - (264)))
self.scrollView.delegate = self
self.outerView.addSubview(self.scrollView)
self.scrollView.setContentOffset(CGPoint.zero, animated: true)
}
func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
if (scrollView == self.scrollView) {
if (scrollView.contentOffset.y > 0) {
if (self.outerScrollView.contentOffset.y <= 0 ) {
let contentnOffset:CGPoint = CGPoint(x: 0.0, y: self.yOffset)
DispatchQueue.main.async {
self.outerScrollView.setContentOffset(contentnOffset, animated: true)
self.scrollView.frame.size = CGSize(width: self.view.frame.width, height: self.view.frame.height - self.qLabel.frame.height - 9)
if (scrollView.contentSize.height < self.view.frame.height - self.qLabel.frame.height - 9) {
let downPanGesture: UISwipeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(self.handlePan))
downPanGesture.direction = .down
self.scrollView.addGestureRecognizer(downPanGesture)
}
}
}
} else {
DispatchQueue.main.async {
self.outerScrollView.setContentOffset(CGPoint.zero, animated: true)
self.scrollView.frame.size = CGSize(width: self.view.frame.width, height: self.view.frame.height - (self.scrollView.frame.origin.y))
}
}
}
}
Although everything works fine, there is just 1 small problem. After the inner scrollView is scrolled upwards and contentoffset has changed and I want to keep on scrolling this view, it sometimes just doesn't scroll(upwards). So, I have to first scrooll it downwards and then it will again start scrolling fine. It only happens rarely but it's quite annoying.
Can anybody tell me what could be the possible problem?
Thanks!
P.S. I have tried all other scrollView's delegate methods like didenddecelerating() or didenddragging() etc.

Recreate iOS Flipkart App product detail top view

I am trying to recreate iOS Flipkart app product detail top view as in image below
First image when image is displayed full and detail is at bottom (http://i.stack.imgur.com/1cutn.png)
Second image as user scroll up the detail content (http://i.stack.imgur.com/DXcHf.png)
Feature in above screens that I want to use:
User can scroll through the product images horizontally. User can also scroll up the detail content by dragging vertically over images.
As per my thought table view covers the whole screen with a transparent header view and a collection view stays behind the table view.
My Approach:
Add UIViewController subclass
let viewController = ProductDetailViewController()
self.navigationController?.pushViewController(viewController, animated: true)
Add a UICollectionView below navigation bar
var collectionView: UICollectionView = {
let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
layout.sectionInset = UIEdgeInsetsMake(0, 0, 0, 0)
layout.minimumLineSpacing = 5
layout.scrollDirection = UICollectionViewScrollDirection.Horizontal
let collectionView = UICollectionView(frame: CGRectZero, collectionViewLayout: layout)
collectionView.backgroundColor = UIColor.lightGrayColor()
collectionView.showsHorizontalScrollIndicator = false
return collectionView
}()
self.collectionView.registerClass(PDetailImageCell.self, forCellWithReuseIdentifier: PDetailImageCellIdentifier)
func addCollectionViewForProductImages() {
let screenSize = UIScreen.mainScreen().bounds.size
self.collectionView.frame = CGRect(x: 0, y: 0, width: screenSize.width, height: self.tableHeaderHeight)
self.collectionView.pagingEnabled = true
self.collectionView.dataSource = self
self.collectionView.delegate = self
self.tableView.tableHeaderView?.addSubview(self.collectionView)
}
Add UITableView
let screenSize = UIScreen.mainScreen().bounds.size
self.tableView.frame = CGRect(x: 0, y: 0, width: screenSize.width, height: screenSize.height)
self.view.addSubview(self.tableView)
self.tableView.backgroundColor = UIColor.clearColor()
self.tableView.dataSource = self
self.tableView.delegate = self
self.tableView.canCancelContentTouches = false
Add a transparent UITableHeaderView
let headerView = TableTransparentHeaderView(frame: CGRect(x: 0, y: 0, width: screenSize.width, height: self.tableHeaderHeight))
headerView.backgroundColor = UIColor.clearColor()
self.tableView.tableHeaderView = headerView
Skip touches when user drags on table header. For this, I override the hitTest method in TouchHandledTableView class.
class TouchHandledTableView: UITableView {
var isScrollingHorizontal: Bool?
override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
let hitView = super.hitTest(point, withEvent: event)
let liesInHeaderView = self.tableHeaderView?.pointInside(point, withEvent: event)
if liesInHeaderView == true && self.isScrollingHorizontal != nil {
if self.isScrollingHorizontal == false {
debugPrint("hitTest returned hitView")
return hitView
} else {
debugPrint("hitTest returned nil")
return nil
}
} else {
debugPrint("hitTest returned hitView")
return hitView
}
}
}
But this stopped table scrolling vertically. So I tried to catch weather the tableview is being dragged vertical or horizontal. To catch the scrolling direction, I added swipe gesture, touch event handler,scrollViewDidScroll but all of these are being called after hitTest method called. Once direction detected hitTest is not called in the same phase.

Resources