0

I am having a HTML file that is loading on webView, and it has different sections or anchors for eg. How can we identify that we have webView has scrolled at a particular section.

olha
  • 2,132
  • 1
  • 18
  • 39
iMRahib
  • 532
  • 4
  • 4

1 Answers1

1

I'll share a general algorithm, without a specific (and tested :) ) implementation.

We know when webView has scrolled by overriding a delegate's method:

func registerForScrollUpdates() {
    webView!.scrollView.delegate = self
}

// MARK: UIScrollViewDelegate implementation
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    
}

We can get visible DOM elements, e.g. using this approach: https://stackoverflow.com/a/44612174/2567725

// Get all elements on the page (change this to another DOM element if you want)
var all = document.getElementsByTagName("*");

for (var i = 0, max = all.length; i < max; i++) {
    if (isHidden(all[i]))
        // hidden
    else 
        // visible
}

function isHidden(el) {
    var style = window.getComputedStyle(el);
    return ((style.display === 'none') || (style.visibility === 'hidden'))
}

Now, we can iterate visible elements and compare a class/id/whatever with the target elements' class/id/whatever.


Or, if you are interested in a specific element and it's not possible to "edit" DOM by adding additional tags, you can store a "path to root" of a given DOM element.

This JS code uniquely identifies the node in tree.

function nodePosRelativeToSiblings(dom, node) {
    var prop = dom.body.previousElementSibling ? 'previousElementSibling' : 'previousSibling';

    var i = 0;
    while (node = node[prop]) { ++i }
    return i;
}

function getNextSibling(dom, node) {
    var prop = dom.body.nextElementSibling ? 'nextElementSibling' : 'nextSibling';
    return node[prop];
}

function indentifyNodeByPath(dom, path) {
    var parentNode = dom.body;
    for (var i=0; i<path.length; i++) {
        var path_i = path[i];
        children.length=${parentNode.children.length}, path_i=${path_i}`);
        
        // this is an ungly (an unsafe) quick-fix: 
        // window.selection() returns a Range with Node which is not Element, but Text.
        // because Safari a Gesko-based browser.
        // So, ".chilren" can contain much less elements than ".childNodes"
        // our puspose is to keep backward-forward calculation consistent!  
        if ( path_i < parentNode.children.length )
        {
            parentNode = parentNode.children[path_i];    
        }
         
    }
    return parentNode;
}

// returns an array (pos to root, pos to siblings)
function getPathToRootNode(dom, node, path) {
    if (node.parentNode == null || node.constructor.name == "HTMLBodyElement") {
        return;
    }
    var posToRelative = nodePosRelativeToSiblings(dom, node);
    path.push(posToRelative);
    
    return getPathToRootNode(dom, node.parentNode, path);
} 

So, in scrollViewDidScroll you can get the list of all "target" elements which are currently visible, by calling:

webView.evaluateJavaScript("getVisibleTargetElements();") { (result, error) in
    print("script evaluated")
}

where getVisibleTargetElements is JS func like

func getVisibleTargetElements() {
    window.alert(`TARGET_ELEMENTS_${el1}_${el2}_${el3}`);
}

and iOS will get this alert

func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
        
        completionHandler();

        if message.starts(with: "TARGET_ELEMENTS_") {
            // parse the message
        }
}

Please note that using window.alert instead of the callback in webView.evaluateJavaScript("getVisibleTargetElements();") will allow us to call this getVisibleTargetElements even from JS and still receive the result in if message.starts(with: "TARGET_ELEMENTS_"). So it's more ugly, but more flexible. :)

olha
  • 2,132
  • 1
  • 18
  • 39