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.
Related
I'm trying to zoom an image that is inside a UIScrollView, and that UIScrollView it's inside of a root UIScrollView that allow paging, similar to the Photos app.
In the viewDidLoad() inside the view that contains the root scrollView I call prepareScrollView()
func prepareScrollView(){
let tap = UITapGestureRecognizer(target: self, action: #selector(hideNavigationItem))
scrollView.addGestureRecognizer(tap)
scrollView.delegate = self
scrollView.minimumZoomScale = 1.0
scrollView.maximumZoomScale = 4.0
scrollView.zoomScale = 1.0
let width = self.view.bounds.width
let height = self.view.bounds.height
totalWidth = width * CGFloat((imageArray!.count)!) //The width must be the width of the screen by the number of the images in the array
scrollView.contentSize = CGSize(width: totalWidth, height: height)
scrollView.isPagingEnabled = true
loadPages()
}
The loadPages() function load every scrollView with their image and add it to the root scrollview.
func loadPages(){
for i in 0...((imageArray!.count)! - 1){
let pageScroll = Page(frame: CGRect(x: self.view.bounds.size.width * CGFloat(i), y: self.view.frame.origin.y, width: self.view.bounds.size.width, height: self.view.bounds.size.height))
pageScroll.minimumZoomScale = 1.0
pageScroll.maximumZoomScale = 4.0
pageScroll.zoomScale = 1.0
let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: self.view.bounds.size.width, height: self.view.bounds.size.height))
imageView.image = UIImage(data: (imageArray![i])!)
imageView.contentMode = .scaleToFill
pageScroll.addSubview(imageView)
scrollView.addSubview(pageScroll)
}
}
The Page object it's just a UIScrollView object.
class Page: UIScrollView {
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return self.subviews[0]//Return the image
}
}
And inside the root scrollView viewForZooming function it calls the viewForZooming function of the correspond child scrollview.
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
if(self.scrollView.subviews.count > 0){
let childScrollView = self.scrollView.subviews[currentPage] as! Page
let image = childScrollView.viewForZooming(in: scrollView)
return image
}
return nil
}
With this approach I can navigate horizontally but I can't zoom in the current image. The viewForZooming function executes in both objects. What I'm doing wrong?
Finally I solve my issue. I changed the Page object to a simple UIScrollView and then set the delegate of the scrollView object to self.
func loadPages(){
for i in 0...((comic?.comicsPages!.count)! - 1){
let pageScroll = UIScrollView(frame: CGRect(x: self.view.bounds.size.width * CGFloat(i), y: self.view.frame.origin.y, width: self.view.bounds.size.width, height: self.view.bounds.size.height))
pageScroll.minimumZoomScale = 1.0
pageScroll.maximumZoomScale = 4.0
pageScroll.zoomScale = 1.0
pageScroll.isUserInteractionEnabled = true
pageScroll.delegate = self //Here
let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: self.view.bounds.size.width, height: self.view.bounds.size.height))
imageView.image = UIImage(data: (imageArray![i])!)
imageView.contentMode = .scaleToFill
pageScroll.addSubview(imageView)
scrollView.addSubview(pageScroll)
}
}
And then in the viewForZooming function of the root scrollView I change the code for this:
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
if(scrollView.subviews.count > 0){
return scrollView.subviews[0]
}
return nil
}
The scrollView parameter is the child scrollview so it works as expected.
I am trying to create an infinite scrolling paging UIScrollView I have been following the Advanced UIScrollView Techniques video from WWDC 2010 however I am unsure as to how to create tiling for a paging UIScrollView. I have been using this tutorial for guidance Infinite Paging along with this Stack Exchange answer. Is it possible to create such an effect in a paging UIScrollView or is tiling primarily used only in a continuous scrolling environment. Thank you for your help.
If you need infinite scrolling you can change element position after scroll.
very simple example and dirty code but I home I helped you:
import UIKit
class MainVC: UIViewController, UIScrollViewDelegate {
#IBOutlet var scrollView: UIScrollView!
var currentView: UIView?
var nextView: UIView?
var previousView: UIView?
override func viewDidLoad() {
super.viewDidLoad()
scrollView.isPagingEnabled = true
scrollView.delegate = self
generateViews()
}
func generateViews() {
let screenSize = UIScreen.main.bounds
currentView = UIView()
nextView = UIView()
previousView = UIView()
addPosition(cView: currentView!, nView: nextView!, pView: previousView!)
currentView?.backgroundColor = .red
nextView?.backgroundColor = .green
previousView?.backgroundColor = .blue
scrollView.addSubview(currentView!)
scrollView.addSubview(previousView!)
scrollView.addSubview(nextView!)
scrollView.contentSize = CGSize(width: screenSize.width * 3, height: screenSize.height)
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if scrollView.contentOffset.x >= (UIScreen.main.bounds.width * 2) {
addPosition(cView: nextView!, nView: previousView!, pView: currentView!)
} else if scrollView.contentOffset.x == 0 {
addPosition(cView: previousView!, nView: currentView!, pView: nextView!)
}
scrollView.contentOffset = CGPoint(x: UIScreen.main.bounds.width, y: 0)
}
func addPosition(cView: UIView, nView: UIView, pView: UIView) {
let screenSize = UIScreen.main.bounds
cView.frame = CGRect(origin: CGPoint(x: screenSize.width, y: 0), size: screenSize.size)
nView.frame = CGRect(origin: CGPoint(x: screenSize.width * 2, y: 0), size: screenSize.size)
pView.frame = CGRect(origin: CGPoint(x: 0, y: 0), size: screenSize.size)
currentView = cView
nextView = nView
previousView = pView
}
}
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)}
}
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
}
}
The following is the current implementation. The grey button is floating on the scroll view. Is there a way to make the button appear once the yellow view (end of scroll view) is reached. Then keep it floating on the screen at the very bottom.
I'm using the following code:
override func scrollViewDidScroll(scrollView: UIScrollView) {
if (scrollView.contentOffset.y >= (scrollView.contentSize.height - scrollView.frame.size.height)) {
//reached bottom - how to show button below yellow
// and keep it floating as shown above
}
}
Adding additional code of what I've tried so far:
#IBOutlet weak var btnScroll: UIButton!
var startingFrame : CGRect!
var endingFrame : CGRect!
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if (scrollView.contentOffset.y >= (scrollView.contentSize.height - scrollView.frame.size.height)) && self.btnScroll.isHidden {
self.btnScroll.isHidden = false
self.btnScroll.frame = startingFrame // outside of screen somewhere in bottom
UIView.animate(withDuration: 1.0) {
self.btnScroll.frame = self.endingFrame // where it should be placed
}
}
}
func configureSizes() {
let screenSize = UIScreen.main.bounds
let screenWidth = screenSize.width
let screenHeight = screenSize.height
startingFrame = CGRect(x: 0, y: screenHeight+100, width: screenWidth, height: 100)
endingFrame = CGRect(x: 0, y: screenHeight-100, width: screenWidth, height: 100)
}
override func viewDidLoad() {
super.viewDidLoad()
configureSizes()
}
If I understand you right you want to put button on the position which shown on the gif
Try this code:
override func scrollViewDidScroll(scrollView: UIScrollView) {
if (scrollView.contentOffset.y >= (scrollView.contentSize.height - scrollView.frame.size.height)) && self.button.isHidden {
self.button.isHidden = false
self.button.frame = startingFrame // outside of screen somewhere in bottom
UIView.animate(withDuration: 1.0) {
self.button.frame = yourFrame // where it should be placed
}
}
}
UPDATE
add this code to hide your button before animation will show it
override func viewDidLoad() {
super.viewDidLoad()
self.button.isHidden = true
...
}