I have a migration issue from UIViewView to WKWebView, detecting the Scroll View reached bottom when using WKWebView. Prior to WKWebView I used the UIScrollViewDelegate detecting wether the User had seen all of the content by scrolling till the end of the WebView. If he did, the "confirm" button was enabled. iPhone - knowing if a UIScrollView reached the top or bottom
Now with WKWebView this doesn't work anymore. I guess the reason is, when using a WKWebView and load a html string, it scales the view down for full visiblity of the content. So I had to set the viewport by appending it to the html string. This displays the content in the same way, the UIWebView did, providing the html string, without setting a viewport.
But now the UIScrollViewDelegate on load always tells that the bottom already reached. I guess, that the WKWebView loads the html, scales it at full visiblity, the scrollViewDelegate recognizes, that the content was fully visible, after that the viewport comes in and scales the page up, so a vertical scroll is needed to display the full content. But at this time, my "confirm" button is already enabled.
Code Snippet
override func scrollViewDidScroll(_ scrollView: UIScrollView){
let scrollViewHeight = scrollView.frame.size.height;
let scrollContentSizeHeight = scrollView.contentSize.height;
let scrollOffset = scrollView.contentOffset.y;
if (scrollOffset + scrollViewHeight == scrollContentSizeHeight)
{
self.confirmButton.isEnabled = true;
}
}
With WKWebView, the scrollContentSizeHeight always is the same as scrollViewHeight on load, but after the scrollViewDidScroll delegate function invokes mulitple times (without scrolling) the scrollContentSizeHeight is larger than the scrollViewHeight at real size.
But now the UIScrollViewDelegate on load always tells that the bottom
already reached.
The issue in the particular case is UIScrollView delegate is getting called before the WKWebView is loaded completely.
Take one private instance variable to check if the URL is loaded completely or not.
var isURLLoaded = false
Confirm WKWebView delegates to your viewController.
webView.scrollView.delegate = self
webView.navigationDelegate = self
And override these delegate methods:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if isURLLoaded {
if scrollView.contentOffset.y >= (scrollView.contentSize.height - scrollView.frame.size.height) {
confirmButton.isEnabled = true
} else if scrollView.contentOffset.y < scrollView.contentSize.height {
confirmButton.isEnabled = false
}
}
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
isURLLoaded = true
}
I wrote scrollview extension for Wkwebview using to read javascript code.through this you can use to check webview reaches to end of page or not. Before use, Set webview scroll delegate.
extension BaseWebViewController: UIScrollViewDelegate {
func scrollViewDidEndDragging(_ scrollView: UIScrollView,
willDecelerate decelerate: Bool) {
webView.evaluateJavaScript("document.readyState", completionHandler: { (complete, error) in
if complete != nil {
self.webView.evaluateJavaScript("document.body.scrollHeight", completionHandler: { (height, error) in
let bodyScrollHeight = height as! CGFloat
var bodyoffsetheight: CGFloat = 0
var htmloffsetheight: CGFloat = 0
var htmlclientheight: CGFloat = 0
var htmlscrollheight: CGFloat = 0
var wininnerheight: CGFloat = 0
var winpageoffset: CGFloat = 0
var winheight: CGFloat = 0
//body.offsetHeight
self.webView.evaluateJavaScript("document.body.offsetHeight", completionHandler: { (offsetHeight, error) in
bodyoffsetheight = offsetHeight as! CGFloat
self.webView.evaluateJavaScript("document.documentElement.offsetHeight", completionHandler: { (offsetHeight, error) in
htmloffsetheight = offsetHeight as! CGFloat
self.webView.evaluateJavaScript("document.documentElement.clientHeight", completionHandler: { (clientHeight, error) in
htmlclientheight = clientHeight as! CGFloat
self.webView.evaluateJavaScript("document.documentElement.scrollHeight", completionHandler: { (scrollHeight, error) in
htmlscrollheight = scrollHeight as! CGFloat
self.webView.evaluateJavaScript("window.innerHeight", completionHandler: { (winHeight, error) in
if error != nil {
wininnerheight = -1
} else {
wininnerheight = winHeight as! CGFloat
}
self.webView.evaluateJavaScript("window.pageYOffset", completionHandler: { (winpageOffset, error) in
winpageoffset = winpageOffset as! CGFloat
let docHeight = max(bodyScrollHeight, bodyoffsetheight, htmlclientheight, htmlscrollheight,htmloffsetheight)
winheight = wininnerheight >= 0 ? wininnerheight : htmloffsetheight
let winBottom = winheight + winpageoffset
if (winBottom >= docHeight) {
print("end scrolling")
}
})
})
})
})
})
})
})
}
})
}
}
After scroll reaches to end you will see in console "end scrolling"
Create a ScrollView inside the class, WKWebView has a scrollview embedded inside, make the scrollview you created relative with the embedded scrollview and implemente the method :
func scrollViewDidScroll(_ scrollView: UIScrollView){
if (scrollView.contentOffset.y + 1) >= (scrollView.contentSize.height - scrollView.frame.size.height) {
//bottom reached
your code here
}
}
and then, make your class inherit from UIScrollViewDelegate :
class : UIViewController,
func updateButtonState(_ scrollView : UIScrollView){
if isURLLoaded {
if scrollView.contentOffset.y >= (scrollView.contentSize.height - scrollView.frame.size.height) {
confirmButton.isEnabled = true
} else if scrollView.contentOffset.y < scrollView.contentSize.height {
confirmButton.isEnabled = false
}
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.updateButtonState(scrollView);
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
isURLLoaded = true
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500), execute:{
self.updateButtonState(webView.scrollView)
});
}
Unfortunatley I can't find a callback where the scrollView is ready to evaluate, so I set a timeout of 500ms, with this modification it works as expected
Related
I have a scrollView with the delegate method set.
private let scrollView: UIScrollView = {
let scrollView = UIScrollView(frame: .zero).usingAutoLayout()
scrollView.isPagingEnabled = true
scrollView.showsVerticalScrollIndicator = false
scrollView.showsHorizontalScrollIndicator = false
return scrollView
}()
I'm trying to making only scroll to the left to mimic a "delete cell", like in the phone book. I don't want the user to be able to scroll to the right. I have this, which kinda works:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.x < 0 {
scrollView.contentOffset.x = 0
}
The problem is that if I swipe fast the contentOffSet is set to positive values, which makes the scrollView scroll in the opposite direction. This usually happens after I finish the swipe gesture. This makes me think it has to do with the bounce, but even setting it to false, it still occurs.
Was able to come up with a solution:
extension SwipeableCollectionViewCell: UIScrollViewDelegate {
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
if scrollView.panGestureRecognizer.translation(in: scrollView.superview).x > 0 {
self.scrollDirection = .rigth
} else {
self.scrollDirection = .left
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if self.scrollDirection == .rigth {
scrollView.contentOffset.x = 0
}
}
}
private enum ScrollDirection {
case rigth
case left
}
I have rotated my collectionView upside down so it would load the content at the top instead of at the bottom and I added this:
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
let currentOffset = scrollView.contentOffset.y
let maxOffset = scrollView.contentSize.height - scrollView.frame.size.height
if maxOffset - currentOffset <= 10{
fetch()
}
}
but nothing gets fetch even though on the console it's printing every time I go reach the bottom. fetch() works, calling it doesn't
I did the same thing using this and it works fine.
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y > scrollView.contentSize.height - scrollView.frame.height + 50 /* 50 its just example for your like */ {
DispatchQueue.main.async {
fetch()
}
}
}
I have an app that displays a bunch of pictures when the user presses the button from a text list view screen. I have a specific subclass that allows the user to pinch and zoom in, but I am curious as to what code I need to enter in my particular swift file to allow the user to double tap to zoom in and out. I get three errors using this code. Two say "Value of type 'ZoomingScrollView' has no member 'scrollview' or 'imageview' " and also " 'CGRectZero' is unavaiable in Swift" My code for the subclass that allows the user to zoom in is below along with the code I am using to double tap to zoom.
import UIKit
class ZoomingScrollView: UIScrollView, UIScrollViewDelegate {
#IBOutlet weak var viewForZooming: UIView? = nil {
didSet {
self.delegate = self
} }
func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? {
return viewForZooming }
#IBAction func userDoubleTappedScrollview(recognizer: UITapGestureRecognizer) {
if let scrollV = self.scrollview {
if (scrollV.zoomScale > scrollV.minimumZoomScale) {
scrollV.setZoomScale(scrollV.minimumZoomScale, animated: true)
}
else {
//(I divide by 3.0 since I don't wan't to zoom to the max upon the double tap)
let zoomRect = self.zoomRectForScale(scrollV.maximumZoomScale / 3.0, center: recognizer.locationInView(recognizer.view))
self.scrollview?.zoomToRect(zoomRect, animated: true)
}
} }
func zoomRectForScale(scale : CGFloat, center : CGPoint) -> CGRect {
var zoomRect = CGRectZero
if let imageV = self.imageView {
zoomRect.size.height = imageV.frame.size.height / scale;
zoomRect.size.width = imageV.frame.size.width / scale;
let newCenter = imageV.convertPoint(center, fromView: self.scrollview)
zoomRect.origin.x = newCenter.x - ((zoomRect.size.width / 2.0));
zoomRect.origin.y = newCenter.y - ((zoomRect.size.height / 2.0));
}
return zoomRect; }}
The issue seems to be that you copied some code from a view controller that had a scrollView and imageView outlet. I'm guessing your viewForZooming: UIView is your image view that you are zooming on. You also don't have to reference self.scrollView anymore because self is the scroll view since you're subclassing scroll view :) I think the code below should fix your problem (note: it's in the latest swift syntax. what you posted was not, so you may have to switch the code back to the old style if necessary. You should try to use the latest Swift though). Good luck and let me know if you have questions.
import UIKit
class ZoomingScrollView: UIScrollView, UIScrollViewDelegate {
#IBOutlet weak var viewForZooming: UIView? = nil {
didSet {
self.delegate = self
} }
func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? {
return viewForZooming }
#IBAction func userDoubleTappedScrollview(recognizer: UITapGestureRecognizer) {
if (zoomScale > minimumZoomScale) {
setZoomScale(minimumZoomScale, animated: true)
}
else {
//(I divide by 3.0 since I don't wan't to zoom to the max upon the double tap)
let zoomRect = zoomRectForScale(scale: maximumZoomScale / 3.0, center: recognizer.location(in: recognizer.view))
zoom(to: zoomRect, animated: true)
}
}
func zoomRectForScale(scale : CGFloat, center : CGPoint) -> CGRect {
var zoomRect = CGRect.zero
if let imageV = self.viewForZooming {
zoomRect.size.height = imageV.frame.size.height / scale;
zoomRect.size.width = imageV.frame.size.width / scale;
let newCenter = imageV.convert(center, from: self)
zoomRect.origin.x = newCenter.x - ((zoomRect.size.width / 2.0));
zoomRect.origin.y = newCenter.y - ((zoomRect.size.height / 2.0));
}
return zoomRect;
}
}
in my app i am using small menu at the bottom of uiwebview. and i want to make like when user scroll downside that view must be hide. and when scrolling upside view must be unhide.
Like Safari.
this is what i tried
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
print("Going Down")
viewbottom.hidden = true
viewHieght.constant = 0
}
func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {
print("Going Up")
viewbottom.hidden = false
viewHieght.constant = 45
}
but by using this code its continuously showing up and down.
Use scroll view pan gesture recogniser to determine the direction.
func scrollViewWillBeginDragging(scrollView: UIScrollView) {
if scrollView.panGestureRecognizer.translationInView(scrollView.superview).y > 0 {
// scrolls down
} else {
// scrolls up
}
}
keep tracking the scrollView.contentOffset.y value and compare the last with the current value like so:
in your mainView add: var lastScrollOffset = CGFloat()
compare the last value with the current one in func scrollViewDidScroll(scrollView: UIScrollView)
func scrollViewDidScroll(scrollView: UIScrollView) {
let scrollOffset = scrollView.contentOffset.y
if lastScrollOffset < scrollOffset{
//scrolling down
}else if lastScrollOffset > scrollOffset {
//scrolling up
}else{
//going crazy
}
lastScrollOffset = scrollOffset
}
I am using a UIPageViewController, and I need to get the scroll position of the ViewController as the users swipe so I can partially fade some assets while the view is transitioning to the next UIViewController.
The delegate and datasource methods of UIPageViewController don't seem to provide any access to this, and internally I'm assuming that the UIPageViewController must be using a scroll view somewhere, but it doesn't seem to directly subclass it so I'm not able to call
func scrollViewDidScroll(scrollView: UIScrollView) {
}
I've seen some other posts suggestion to grab a reference to the pageViewController!.view.subviews and then the first index is a scrollView, but this seems very hacky. I'm wondering if there is a more standard way to handle this.
You can search for the UIScrollView inside your UIPageViewController. To do that, you will have to implement the UIScrollViewDelegate.
After that you can get your scrollView:
for v in pageViewController.view.subviews{
if v.isKindOfClass(UIScrollView){
(v as UIScrollView).delegate = self
}
}
After that, you are able to use all the UIScrollViewDelegate-methods and so you can override the scrollViewDidScroll method where you can get the scrollPosition:
func scrollViewDidScroll(scrollView: UIScrollView) {
//your Code
}
Or if you want a one-liner:
let scrollView = view.subviews.filter { $0 is UIScrollView }.first as! UIScrollView
scrollView.delegate = self
UIPageViewController scroll doesn't work like normal scrollview and you can't get scrollView.contentOffset like other scrollViews.
so here is a trick to get what's going on when user scrolls :
first you have to find scrollview and set delegate to current viewController like other answers said.
class YourViewController : UIPageViewController {
var startOffset = CGFloat(0) //define this
override func viewDidLoad() {
super.viewDidLoad()
//from other answers
for v in view.subviews{
if v is UIScrollView {
(v as! UIScrollView).delegate = self
}
}
}
.
.
.
}
extension YourViewController : UIScrollViewDelegate{
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
startOffset = scrollView.contentOffset.x
}
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
var direction = 0 //scroll stopped
if startOffset < scrollView.contentOffset.x {
direction = 1 //going right
}else if startOffset > scrollView.contentOffset.x {
direction = -1 //going left
}
let positionFromStartOfCurrentPage = abs(startOffset - scrollView.contentOffset.x)
let percent = positionFromStartOfCurrentPage / self.view.frame.width
//you can decide what to do with scroll
}
}
Similar to Christian's answer but a bit more Swift-like (and not unnecessarily continuing to loop through view.subviews):
for view in self.view.subviews {
if let view = view as? UIScrollView {
view.delegate = self
break
}
}
As of iOS 13, the UIPageViewController seems to reset the scrollview's contentOffset once it transitions to another view controller. Here is a working solution:
Find the child scrollView and set its delegate to self, as other answers suggested
Keep track of the current page index of the pageViewController:
var currentPageIndex = 0
// The pageViewController's viewControllers
let orderredViewControllers: [UIViewController] = [controller1, controller2, ...]
pageViewController.delegate = self
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
guard completed, let currentViewController = pageViewController.viewControllers?.first else { return }
currentPageIndex = orderredViewControllers.firstIndex(of: currentViewController)!
}
Get the progress that ranges from 0 to 1
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let contentOffsetX = scrollView.contentOffset.x
let width = scrollView.frame.size.width
let offset = CGFloat(currentPageIndex) / CGFloat(orderredViewControllers.count - 1)
let progress = (contentOffsetX - width) / width + offset
}
var pageViewController: PageViewController? {
didSet {
pageViewController?.dataSource = self
pageViewController?.delegate = self
scrollView?.delegate = self
}
}
lazy var scrollView: UIScrollView? = {
for subview in pageViewController?.view?.subviews ?? [] {
if let scrollView = subview as? UIScrollView {
return scrollView
}
}
return nil
}()
extension BaseFeedViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offset = scrollView.contentOffset.x
let bounds = scrollView.bounds.width
let page = CGFloat(self.currentPage)
let count = CGFloat(viewControllers.count)
let percentage = (offset - bounds + page * bounds) / (count * bounds - bounds)
print(abs(percentage))
}
}
To make the code as readable and separated as possible, I would define an extension on UIPageViewController:
extension UIPageViewController {
var scrollView: UIScrollView? {
view.subviews.first(where: { $0 is UIScrollView }) as? UIScrollView
}
}
It's quite easy to set yourself as the delegate for scroll view events, as so:
pageViewController.scrollView?.delegate = self