Search code examples
iosswiftwkwebview

WKWebView setContentOffset does not work with animated = false: no scrolling occurs


I am attempting to create an EPUB reader. Since the chapters in an EPUB are represented as whole HTML files with no page breaks, I will display one page at a time in a WKWebView, loading the string with loadHTMLString. Then, when the user swipes to the next page of the UIPageViewController, I will make a new WKWebView and scroll down a page using the setContentOffset method. However, the setContentOffset method has no effect on the displayed content. tldr - how can I tell when this setContentOffset method is safe to call?

Here is my page view controller:

class MyPageViewController: UIPageViewController, UIPageViewControllerDelegate, UIPageViewControllerDataSource {
    var page = 0
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        dataSource = self
        delegate = self
        
        setViewControllers([Page(page: 0)], direction: .forward, animated: false, completion: nil)
    }
    
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
        return nil
    }
    
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
        return Page(page: page + 1)
    }
    
    func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
        if completed {
            page += 1
        }
   }
}

and here is my Page class:

class Page: UIViewController, WKNavigationDelegate, WKUIDelegate {
    var page: Int
    var webView: WKWebView!
    
    let testString = "<h1>00000000000000000000000000000000000000000000000000</h1><br><h1>11111111111111111111111111111111111111111111111111</h1><br><h1>22222222222222222222222222222222222222222222222222</h1><br><h1>33333333333333333333333333333333333333333333333333</h1><br><h1>44444444444444444444444444444444444444444444444444</h1><br><h1>55555555555555555555555555555555555555555555555555</h1><br><h1>66666666666666666666666666666666666666666666666666</h1><br><h1>77777777777777777777777777777777777777777777777777</h1><br><h1>88888888888888888888888888888888888888888888888888</h1><br><h1>99999999999999999999999999999999999999999999999999</h1>"

    init(page: Int) {
        self.page = page
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        webView = WKWebView(frame: view.bounds)
        webView.navigationDelegate = self
        
        view.addSubview(webView)
        
        load()
    }
    
    func load() {
        webView.loadHTMLString(testString, baseURL: nil)
    }
    
    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        self.scroll()
    }
    
    func scroll() {
        let scrollPoint = CGPoint(x: 0, y: 100 * CGFloat(page))
        webView.scrollView.setContentOffset(scrollPoint, animated: false)
    }
}

I want the scroll to be instantaneous so that the user cannot tell it is scrolling, hence the animated: false. However, this simply does not change the view in any way. If I use animated: true it works fine, and if I wrap self.scroll() in a DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) it works as well. But the user can see a bit of a jarring scroll if I do this. I have tried calling .setNeedsLayout() and .layoutIfNeeded() to no avail; the view still does not change.

So clearly there is something that is not ready to scroll the webView, even after the didFinish delegate method is called. How can I tell when this webView is actually ready to scroll? Or is there another reason that animated: false is not working?


Solution

  • After researching the issue, I came to a similar conclusion as https://stackoverflow.com/a/76145772/8010366. It appears that there is an issue with the initial scrolling.


    However, if you still need to manually scroll without the animation, you can use the following low-level code:

    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        let yOffset = 20 * page
        webView.evaluateJavaScript("document.body.scrollTop = \(100 * yOffset)")
    }
    

    This executes a JavaScript code to scroll the HTML document. Additionally, you should add more content to enable scrolling.


    References: