I am trying to achieve pull to refresh in a WKWebView like in this Material pull to refresh GIF. Because we want to include websites that already have a html navigation bar, we need to keep the web view fix when dragging down. I found pull to refresh (UIRefreshControl) to table views and web views but the views go down as the user drags down.
I set the delegate to scrollView property of the web view and receive notifications. When I drag near to top and get to 0 (vertical scroll view offset) I can disable the scroll view, enable the pan gesture. But to actually move the custom spinner view I need a second touch.
The method func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) doesn't work because I have a scroll view over.
private func gestures() {
self.panGesture = UIPanGestureRecognizer(
target: self,
action: "panGestureCaptured:"
)
self.panGesture!.enabled = false
self.webView.addGestureRecognizer(self.panGesture!)
}
func scrollViewDidScroll(scrollView: UIScrollView) {
if scrollView.contentOffset.y <= 0 {
self.view.layoutIfNeeded()
self.spinnerTopLayoutConstraint?.constant = -scrollView.contentOffset.y
}
}
func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if scrollView.panGestureRecognizer.translationInView(scrollView.superview).y > 0 { // dragging down
if scrollView.contentOffset.y == 0 {
self.webView!.scrollView.scrollEnabled = false
// self.webView!.scrollView.canCancelContentTouches = true
self.panGesture!.enabled = true
// self.refreshWebView()
}
self.showNavigationItems()
} else { // dragging up
self.hideNavigationItems()
}
}
Thanks to #Darko: the idea was to use the panGesture property of the scroll view (also property of the web view).
private func gestures() {
self.webView.scrollView.panGestureRecognizer.addTarget(
self,
action: "panGestureCaptured:"
)
}
func panGestureCaptured(gesture: UIGestureRecognizer) {
let touchPoint = gesture.locationInView(self.webView)
print("touchPoint: \(touchPoint)")
print("panGestureCaptured scrollView offset \(self.webView!.scrollView.contentOffset.y)")
if self.webView!.scrollView.contentOffset.y == 0 {
if self.webView!.scrollView.panGestureRecognizer.state == UIGestureRecognizerState.Changed {
if touchPoint.y < self.webView!.frame.height * 0.3 {
self.spinnerTopLayoutConstraint?.constant = touchPoint.y
} else {
self.spinnerTopLayoutConstraint?.constant = self.webView!.frame.height * 0.3
}
} else if self.webView!.scrollView.panGestureRecognizer.state == UIGestureRecognizerState.Ended {
self.spinnerTopLayoutConstraint?.constant = UIApplication.sharedApplication().statusBarFrame.height + 20
}
}
}
WebView's scroll view already has a pan recognizer (webView.scrollView.panGestureRecognizer) so you could listen to this one (with addTarget) and disable bouncing on the scroll view. In this way you don't need to disable/enable the pan recognizer.
Related
I have a project where I’m adding three UILabels to the view controller’s view. When the user begins moving their finger around the screen, I want to be able to determine when they their finger is moving over any of these UILabels.
I’m assuming a UIPanGestureRecognizer is what I need (for when the user is moving their finger around the screen) but I’m not sure where to add the gesture. (I can add a tap gesture to a UILabel, but this isn’t what I need)
Assuming I add the UIPanGestureRecognizer to the main view, how would I go about accomplishing this?
if gesture.state == .changed {
// if finger moving over UILabelA…
// …do this
// else if finger moving over UILabelB…
// …do something else
}
You can do this with either a UIPanGestureRecognizer or by implementing touchesMoved(...) - which to use depends on what else you might be doing.
For pan gesture, add the recognizer to the view (NOT to the labels):
#objc func handlePan(_ g: UIPanGestureRecognizer) {
if g.state == .changed {
// get the location of the gesture
let loc = g.location(in: view)
// loop through each label to see if its frame contains the gesture point
theLabels.forEach { v in
if v.frame.contains(loc) {
print("Pan Gesture - we're panning over label:", v.text)
}
}
}
}
For using touches, no need to add a gesture recognizer:
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let t = touches.first {
// get the location of the touch
let loc = t.location(in: view)
// loop through each label to see if its frame contains the touch point
theLabels.forEach { v in
if v.frame.contains(loc) {
print("Touch - we're dragging the touch over label:", v.text)
}
}
}
}
I just faced a really difficult point while making a complicated view that has pageViewController, tableview, and scrollview.
This is what I want to program
case 1) scroll up : button stactview and pageviewcontroller locate headerview.snp.botttom and hide red view and tableview will scroll
case 2) scroll down : show red view, button stactview and pageviewcontroller locate red view.snp.bottom (like photo)
I made it like these cases, but the problem is tableview in pageviewcontroller didn't move.
What should I do to make it smooth scrolling?
I found example what I want to make
https://raw.githubusercontent.com/tsucres/HeaderedTabScrollView/master/Screenshots/customAnim.gif
http://cocoadocs.org/docsets/MXSegmentedPager/3.2.2/
I build view programmatically using snapkit
self.tabTopValue = 440 // redview height
self.view.addSubview(scrollView)
scrollView.snp.makeConstraints { make in
make.leading.trailing.bottom.equalToSuperview()
make.top.equalTo(headerView.snp.bottom)
}
scrollView.addSubview(contentView)
contentView.snp.makeConstraints { make in
make.edges.equalToSuperview()
make.width.equalTo(self.scrollView)
}
redView.snp.makeConstraints { make in
make.leading.equalTo(contentView.snp.leading)
make.trailing.equalTo(contentView.snp.trailing)
make.top.equalTo(header.snp.bottom).offset(0)
make.height.equalTo(440)
}
// tabbar
tabButtonStackView.addArrangedSubview(historyInfoButton)
tabButtonStackView.addArrangedSubview(graphButton)
tabButtonStackView.addArrangedSubview(lineageInfoButton)
contentView.addSubview(tabButtonStackView)
tabButtonStackView.snp.makeConstraints { make in
make.leading.trailing.equalTo(contentView)
tabTopConstraint = make.top.equalTo(headerView.snp.bottom).offset(self.tabTopValue).constraint
make.height.equalTo(40)
}
addChild(pageViewController)
contentView.addSubview(pageViewController.view)
pageViewController.didMove(toParent: self)
pageViewController.view.snp.makeConstraints { make in
make.top.equalTo(tabButtonStackView.snp.bottom)
make.leading.trailing.equalTo(contentView)
make.height.equalTo(scrollView)
make.bottom.equalTo(contentView.snp.bottom).offset(0)
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
print("detail scroll = \(scrollView.contentOffset.y)")
if scrollView.contentOffset.y < 0 {
self.tabButtonStackView.snp.updateConstraints { make in
self.tabTopConstraint = make.top.equalTo(headerView.snp.bottom).offset(scrollView.contentOffset.y + tabTopValue).constraint
}
} else if scrollView.contentOffset.y == 0 {
UIView.animate(withDuration: 0.3, delay: 0.0, animations: {
self.tabButtonStackView.snp.updateConstraints { make in
self.tabTopConstraint = make.top.equalTo(self.headerView.snp.bottom).offset(self.tabTopValue).constraint
}
}, completion: nil)
}
if scrollView.contentOffset.y < tabTopValue {
self.tabButtonStackView.snp.updateConstraints { make in
self.tabTopConstraint = make.top.equalTo(headerView.snp.bottom).offset(tabTopValue - scrollView.contentOffset.y).constraint
}
} else if scrollView.contentOffset.y > tabTopValue {
self.tabButtonStackView.snp.updateConstraints { make in
self.tabTopConstraint = make.top.equalTo(headerView.snp.bottom).offset(0).constraint
}
}
}
I have been experimenting with custom interactive view controller presentation and dismissal (using a combination of UIPresentationController, UIPercentDrivenInteractiveTransition, UIViewControllerAnimatedTransitioning, and UIViewControllerTransitioningDelegate) and have mostly gotten things working well for my needs.
However, there is one common scenario that I've yet to find addressed in any of the tutorials or documentation that I've read, leading me to the following question:
...
What is the proper way of handling custom interactive view controller dismissal, via a pan gesture, when the dismissed view contains a UIScrollView (ie. UITableView, UICollectionView, WKWebView, etc)?
...
Basically, what I'd like is for the following:
View controllers are interactively dismissible by panning them down. This is common UX in many apps.
If the dismissed view controller contains a (vertically-scrolling) scroll view, panning down scrolls that view as expected until the user reaches the top, after which the scrolling ceases and the pan-to-dismiss occurs.
Scroll views should otherwise behave as normal.
I know that this is technically possible - I've seen it in other apps, such as Overcast and Apple's own Music app - but I've not been able to find the key to coordinating the behavior of my pan gesture with that of the scroll view(s).
Most of my own attempts center on trying to conditionally enable/disable the scrollview (or its associated pan gesture recognizer) based on its contentOffset.y while scrolling and having the view controller dismissal's pan gesture recognizer take over from there, but this has been fraught with problems and I fear that I am overthinking it.
I feel like the secret mostly lies in the following pan gesture recognizer delegate method:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
// ...
}
I have created a reduced sample project which should demonstrate the scenario more clearly. Any code suggestions are highly welcome!
https://github.com/Darchmare/SlidePanel-iOS
Solution
Make scrollView stop scrolling after it reached top by using UIScrollView's bounces property and scrollViewDidScroll(_:) method.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
scrollView.bounces = (scrollView.contentOffset.y > 10);
}
Don't forget to set scrollView.delegate = self
Only handle panGestureRecognizer when scrollView reached top - It means when scrollView.contentOffset.y == 0 by using a protocol.
protocol PanelAnimationControllerDelegate {
func shouldHandlePanelInteractionGesture() -> Bool
}
ViewController
func shouldHandlePanelInteractionGesture() -> Bool {
return (scrollView.contentOffset.y == 0);
}
PanelInteractionController
class PanelInteractionController: ... {
var startY:CGFloat = 0
private weak var viewController: (UIViewController & PanelAnimationControllerDelegate)?
#objc func handlePanGestureRecognizer(_ gestureRecognizer: UIPanGestureRecognizer) {
switch gestureRecognizer.state {
case .began:
break
case .changed:
let translation = gestureRecognizer.translation(in: gestureRecognizer.view!.superview!)
let velocity = gestureRecognizer.velocity(in: gestureRecognizer.view!.superview)
let state = gestureRecognizer.state
// Don't do anything when |scrollView| is scrolling
if !(viewController?.shouldHandlePanelInteractionGesture())! && percentComplete == 0 {
return;
}
var rawProgress = CGFloat(0.0)
rawProgress = ((translation.y - startTransitionY) / gestureRecognizer.view!.bounds.size.height)
let progress = CGFloat(fminf(fmaxf(Float(rawProgress), 0.0), 1.0))
if abs(velocity.x) > abs(velocity.y) && state == .began {
// If the user attempts a pan and it looks like it's going to be mostly horizontal, bail - we don't want it... - JAC
return
}
if !self.interactionInProgress {
// Start to pan |viewController| down
self.interactionInProgress = true
startTransitionY = translation.y;
self.viewController?.dismiss(animated: true, completion: nil)
} else {
// If the user gets to a certain point within the dismissal and releases the panel, allow the dismissal to complete... - JAC
self.shouldCompleteTransition = progress > 0.2
update(progress)
}
case .cancelled:
self.interactionInProgress = false
startTransitionY = 0
cancel()
case .ended:
self.interactionInProgress = false
startTransitionY = 0
if self.shouldCompleteTransition == false {
cancel()
} else {
finish()
}
case .failed:
self.interactionInProgress = false
startTransitionY = 0
cancel()
default:
break;
}
}
}
Result
For more detail, you can take a look at my sample project
For me, this little bit of code answered a lot of my issues and greatly helped my custom transitions in scrollviews, it will hold a negative scrollview offset from moving while trying to start a transition or showing an activity indicator on the top. My guess is that this will solve at least some of your transition/animation hiccups:
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if scrollView.contentOffset.y < -75 {
scrollView.contentInset.top = -scrollView.contentOffset.y
}
// Do animation or transition
}
I believe you don't need an additional pan gesture recognizer to implement this. You can simply hook onto the different delegate methods of the scroll view to achieve the "pan to dismiss" effect. Here is how I went about it
// Set the dragging property to true
func scrollViewWillBeginDragging(_: UIScrollView) {
isDragging = true
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// If not dragging, we could make an early exit
guard isDragging else {
return
}
let topOffset = scrollView.contentOffset.y + statusBarHeight
// If The dismissal has not already started and the user has scrolled to the top and they are currently scrolling, then initiate the interactive dismissal
if !isDismissing && topOffset <= 0 && scrollView.isTracking {
startInteractiveTransition()
return
}
// If its already being dismissed, then calculate the progress and update the interactive dismissal animator
if isDismissing {
updateInteractiveTransitionProgress()
}
}
// Once the scroll ends, check for a few things
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
// Early return
if !isDismissing {
return
}
// Optional check to dismiss the controller, if swiped from the top
checkForFastSwipes()
// If dragged enough, dismiss the controller, otherwise cancel the
transition
if interactor?.shouldFinish ?? false {
interactor?.finish()
} else {
interactor?.cancel()
}
// Finally reset the transition properties
resetTransitionProperties()
}
private func checkForFastSwipes() {
let velocity = scrollView.panGestureRecognizer.velocity(in: view)
let velDiff = velocity.y - velocity.x
if velDiff > 0 && velDiff >= 75 {
interactor?.hasStarted = false
self.dismiss()
interactor?.shouldFinish = true
}
}
private func startInteractiveTransition() {
isDismissing = true
interactor?.hasStarted = true
dismiss()
}
private func updateInteractiveTransitionProgress() {
progress = max(
0.0,
min(1.0, ((-scrollView.contentOffset.y) - statusBarHeight) / 90.0)
)
interactor?.shouldFinish = progress > 0.5
interactor?.update(progress)
}
private func resetTransitionProperties() {
isDismissing = false
isDragging = false
}
The interactor property used for synchronizing the animation with gesture
var interactor: Interactor?
class Interactor: UIPercentDrivenInteractiveTransition {
var hasStarted = false
var shouldFinish = false
}
Inspired by the following Kodeco tutorial
https://www.kodeco.com/books/ios-animations-by-tutorials/v6.0/chapters/25-uiviewpropertyanimator-view-controller-transitions
(Look for the Interactive view controller transitions section)
Edit
After implementing the solution below, I realized that it only works if you have sufficient content to scroll, however, if you have dynamic content wherein the contents are not guaranteed to be scrollable as was my case, you'd be better off adding a pan gesture as mentioned by #trungduc. However, there are a few improvements that we could make to their answer like detecting an upwards scroll and not letting it interfere with our gesture.
Under the changed state add the following code
let isUpwardsScroll = self.velocity(in: target).y < 0
/*
If the user is normally scrolling the view, ignore it. However,
once the interaction starts allow such gestures as they could be
dragging the interactable view back
*/
if isUpwardScroll && !interactor.hasStarted {
return
}
I have a table view with section header. I want to add refresh control below the section header and not at top of tableview.Is this possible??
No.It is not possible.Try adding an activity indicator in the section header.
If u want the activity indicator to be shown only when the user pulls down while tableview content offset is 0, then try adding a pan gesture and implementing it only for this specific condition
var panGesture = UIPanGestureRecognizer(target: self, action: #selector(swipedDown))
self.HometableView.addGestureRecognizer(panGesture)
panGesture.delegate = self as! UIGestureRecognizerDelegate
//The code to handle the pan gesture
func swipedDown(panGesture: UIPanGestureRecognizer)
{
let velocity: CGPoint = panGesture.velocity(in: HometableView)
if velocity.y > 0 {
print("gesture moving Up")
if(HometableView.contentOffset == CGPoint.zero)
{
print("************SWIPED DOWN")
//Do the logic to increase section header height and show the activity indicator
}
}
else {
print("gesture moving Bottom")
}
}
func gestureRecognizerShouldBegin(_ panGestureRecognizer: UIPanGestureRecognizer) -> Bool {
let velocity: CGPoint = panGestureRecognizer.velocity(in: HometableView)
if velocity.y > 0 {
if(HometableView.contentOffset == CGPoint.zero)
{
return true
}
}
return false
}
No it is not possible as the section header is part of tableView.
You can add the view not as a part of section header but at the top of UITableView.
I have two UICollectionVews
One of them (The parent one) is a full-screen-cell paginated collection view.
The other one (The child one) is a filter inside the "page"
Both have the same scroll direction
My problem is that when I'm scrolling the child one, and it reaches the end, the parent one starts moving. And I would like to avoid that. I tried many things
*ScrollView delegates
*Touchesbegan
Any ideas?
Thanks!
I think it's easy .
To set the parent UICollectionView
collectionView.scrollEnabled = NO;
Or it sounds unreasonable . If the parent UICollectionView could scroll, how could you archive your goal? Because UICollectionView is consisted of the cells (The child one). The cell will affect the parent UICollectionView inevitably.
Maybe The child one is located in part of the cell , you could use the
UIScrollViewDelegate's method: scrollViewDidScroll , to set the parent collectionView's scrollEnabled property.
I think you prefer this answer.
As we know a pan gesture recognizer is built in UIScrollView.
We could add an other coordinated pan gesture.
Because the apple says: 'UIScrollView's built-in pan gesture recognizer must have its
scroll view as its delegate.'
let pan = UIPanGestureRecognizer(target: self, action: #selector(ViewController.panGestureRecognizerAction(recognizer:)))
pan.delegate = self
mainScrollView.addGestureRecognizer(pan)
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
// So we can get the argus of the pan gesture while not affecting the scroll
after the setting.
var mainScrollEnabled = false
var subScrollEnabled = false
// Then we define two BOOL values to identify the scroll of the collectionView
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView == mainScrollView {
if scrollView.contentOffset.y >= maxOffsetY {
scrollView.setContentOffset(CGPoint(x: 0, y: maxOffsetY), animated: false)
mainScrollView.isScrollEnabled = false
subScrollView.isScrollEnabled = true
subScrollEnabled = true
mainScrollEnabled = false
}
}else {
if scrollView.contentOffset.y <= 0 {
scrollView.setContentOffset(CGPoint(x: 0, y: 0), animated: false)
subScrollView.isScrollEnabled = false
mainScrollView.isScrollEnabled = true
mainScrollEnabled = true
subScrollEnabled = false
}
}
}
// Then we handle the situation that the collectionView reaches the end , by the pan gesture's recognizer .
var currentPanY: CGFloat = 0
func panGestureRecognizerAction(recognizer: UIPanGestureRecognizer) {
if recognizer.state != .changed{
currentPanY = 0
// clear the data of last time after finishing the scroll
mainScrollEnabled = false
subScrollEnabled = false
}else {
let currentY = recognizer.translation(in: mainScrollView).y
// So the collectionView reaches the end.
if mainScrollEnabled || subScrollEnabled {
if currentPanY == 0 {
currentPanY = currentY //get the y
}
let offsetY = currentPanY - currentY //get the offsetY
if mainScrollEnabled {
let supposeY = maxOffsetY + offsetY
if supposeY >= 0 {
mainScrollView.contentOffset = CGPoint(x: 0, y: supposeY)
}else {
mainScrollView.contentOffset = CGPoint.zero
}
}else {
subScrollView.contentOffset = CGPoint(x: 0, y: offsetY)
}
}
}
}