1

How can I find text in uiwebview page and highlight it and go to its position in the page using swift ?

For example chrome find text :

search example

But I'm using alert view instead :

my search example in my app !!!

3 Answers3

3

You have to use JavaScript for this.
I done the same thing with code below (but in Objective C - you can rewrite it to Swift).

Usage (only in webViewDidFinishLoad - it's important!):

-(void)webViewDidFinishLoad:(UIWebView *)webView {
    if (self.searchString != nil ) {
        [self.webView highlightAllOccurencesOfString:self.searchString];
        int position = self.foundedStringsCount - self.selectedStringNumber - 1;
        [self.webView scrollTo:position];
    }
}

Create and add this files to your code:

SearchWebView.h:

#import <Foundation/Foundation.h>

@interface SearchWebView : UIWebView

- (NSInteger)highlightAllOccurencesOfString:(NSString*)str;
- (void)scrollTo:(int)index;
- (void)removeAllHighlights;

@end

SearchWebView.m:

#import "SearchWebView.h"

@implementation SearchWebView

- (NSInteger)highlightAllOccurencesOfString:(NSString*)str
{
    NSString *path = [[NSBundle mainBundle] pathForResource:@"UIWebViewSearch" ofType:@"js"];
    NSString *jsCode = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
    [self stringByEvaluatingJavaScriptFromString:jsCode];

    NSString *startSearch = [NSString stringWithFormat:@"uiWebview_HighlightAllOccurencesOfString('%@')",str];
    [self stringByEvaluatingJavaScriptFromString:startSearch];

    NSString *result = [self stringByEvaluatingJavaScriptFromString:@"uiWebview_SearchResultCount"];
    return [result integerValue];
}

- (void)scrollTo:(int)index {
    [self stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"uiWebview_ScrollTo('%d')",index]];
}

- (void)removeAllHighlights
{
    [self stringByEvaluatingJavaScriptFromString:@"uiWebview_RemoveAllHighlights()"];
}

@end

UIWebViewSearch.js:

var uiWebview_SearchResultCount = 0;

/*!
 @method     uiWebview_HighlightAllOccurencesOfStringForElement
 @abstract   // helper function, recursively searches in elements and their child nodes
 @discussion // helper function, recursively searches in elements and their child nodes

 element    - HTML elements
 keyword    - string to search
 */

function uiWebview_HighlightAllOccurencesOfStringForElement(element,keyword) {
    if (element) {
        if (element.nodeType == 3) {        // Text node

            var count = 0;
            var elementTmp = element;
            while (true) {
                var value = elementTmp.nodeValue;  // Search for keyword in text node
                var idx = value.toLowerCase().indexOf(keyword);

                if (idx < 0) break;

                count++;
                elementTmp = document.createTextNode(value.substr(idx+keyword.length));
            }

            uiWebview_SearchResultCount += count;

            var index = uiWebview_SearchResultCount;
            while (true) {
                var value = element.nodeValue;  // Search for keyword in text node
                var idx = value.toLowerCase().indexOf(keyword);

                if (idx < 0) break;             // not found, abort

                //we create a SPAN element for every parts of matched keywords
                var span = document.createElement("span");
                var text = document.createTextNode(value.substr(idx,keyword.length));
                span.appendChild(text);

                span.setAttribute("class","uiWebviewHighlight");
                span.style.backgroundColor="yellow";
                span.style.color="black";

                index--;
                span.setAttribute("id", "SEARCH WORD"+(index));
                //span.setAttribute("id", "SEARCH WORD"+uiWebview_SearchResultCount);

                //element.parentNode.setAttribute("id", "SEARCH WORD"+uiWebview_SearchResultCount);

                //uiWebview_SearchResultCount++;    // update the counter

                text = document.createTextNode(value.substr(idx+keyword.length));
                element.deleteData(idx, value.length - idx);

                var next = element.nextSibling;
                //alert(element.parentNode);
                element.parentNode.insertBefore(span, next);
                element.parentNode.insertBefore(text, next);
                element = text;
            }


        } else if (element.nodeType == 1) { // Element node
            if (element.style.display != "none" && element.nodeName.toLowerCase() != 'select') {
                for (var i=element.childNodes.length-1; i>=0; i--) {
                    uiWebview_HighlightAllOccurencesOfStringForElement(element.childNodes[i],keyword);
                }
            }
        }
    }
}

// the main entry point to start the search
function uiWebview_HighlightAllOccurencesOfString(keyword) {
    uiWebview_RemoveAllHighlights();
    uiWebview_HighlightAllOccurencesOfStringForElement(document.body, keyword.toLowerCase());
}

// helper function, recursively removes the highlights in elements and their childs
function uiWebview_RemoveAllHighlightsForElement(element) {
    if (element) {
        if (element.nodeType == 1) {
            if (element.getAttribute("class") == "uiWebviewHighlight") {
                var text = element.removeChild(element.firstChild);
                element.parentNode.insertBefore(text,element);
                element.parentNode.removeChild(element);
                return true;
            } else {
                var normalize = false;
                for (var i=element.childNodes.length-1; i>=0; i--) {
                    if (uiWebview_RemoveAllHighlightsForElement(element.childNodes[i])) {
                        normalize = true;
                    }
                }
                if (normalize) {
                    element.normalize();
                }
            }
        }
    }
    return false;
}

// the main entry point to remove the highlights
function uiWebview_RemoveAllHighlights() {
    uiWebview_SearchResultCount = 0;
    uiWebview_RemoveAllHighlightsForElement(document.body);
}

function uiWebview_ScrollTo(idx) {
    var scrollTo = document.getElementById("SEARCH WORD" + idx);
    if (scrollTo) scrollTo.scrollIntoView();
}
Igor
  • 12,165
  • 4
  • 57
  • 73
  • Better to check whether ___keyword___ parameter is null or empty in ___uiWebview_HighlightAllOccurencesOfString___ function like this, ___uiWebview_RemoveAllHighlights(); if (keyword) { uiWebview_HighlightAllOccurencesOfStringForElement(document.body, keyword.toLowerCase()); }___ – Hanushka Suren Feb 13 '20 at 14:16
3

Here is the swift code:

1) add UIWebViewSearch.js file to your project

func highlightAllOccurencesOfString(str:String) -> Int {

    let path = Bundle.main.path(forResource: "UIWebViewSearch", ofType: "js")

    var jsCode = ""

    do{

        jsCode = try String(contentsOfFile: path!)

    }catch
    {
    // do something

    }

    self.newsWebView.stringByEvaluatingJavaScript(from: jsCode)

    let startSearch = "uiWebview_HighlightAllOccurencesOfString('\(str)')"

    self.newsWebView .stringByEvaluatingJavaScript(from: startSearch)

    let  result = self.newsWebView.stringByEvaluatingJavaScript(from: "uiWebview_SearchResultCount")

    return  Int(result!)!


}

func scrollTo(index:Int)  {

    self.newsWebView.stringByEvaluatingJavaScript(from: "uiWebview_ScrollTo('\(index)')")

}

func removeAllHighlights() {

self.newsWebView.stringByEvaluatingJavaScript(from: "uiWebview_RemoveAllHighlights()")

} 
Dibin77
  • 136
  • 6
0

A javascript file may be injected using WKUserContentController, as described in https://medium.com/capital-one-tech/javascript-manipulation-on-ios-using-webkit-2b1115e7e405

Since uiWebview_HighlightAllOccurencesOfStringForElement(element,keyword) is called recursively, the index for the matched text may not be unique. As seen in https://github.com/sstahurski/SearchWKWebView/blob/master/WKWebView%20Extention/SearchWebView.js, we can replace it with a global variable. So uiWebview_ScrollTo(idx) can work correctly.

And javascript functions can be run as needed by subscribing to publishers corresponding to the search text and the index for matched text.

search field with Stepper

Here is my implementation of makeNSView(context:) in NSViewRepresentable, wherein the javascript file is added to WKWebView and subscribers are attached to search text and index.

func makeNSView(context: NSViewRepresentableContext<WebView>) -> WKWebView {
        let configuration = WKWebViewConfiguration()

        // Add a javascript file to WKUserContentController
        if let path = Bundle.main.path(forResource: "UIWebViewSearch", ofType: "js"), let jsString = try? String(contentsOfFile: path, encoding: .utf8) {
            let userContentController = WKUserContentController()
            let userScript = WKUserScript(source: jsString, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
            userContentController.addUserScript(userScript)
            configuration.userContentController = userContentController
        }
        
        let request = URLRequest(url: url)
        let webView = WKWebView(frame: CGRect.zero, configuration: configuration)
        webView.load(request)
        webView.uiDelegate = context.coordinator
        webView.navigationDelegate = context.coordinator
        
        // Observe the text to search
        viewModel.$searchString
            .sink {
                if !$0.isEmpty {
                    
                    let startSearch = "uiWebview_HighlightAllOccurencesOfString('\($0)')"
                    
                    webView.find($0) { result in
                        if result.matchFound {
                            webView.evaluateJavaScript(startSearch) { result, error in
                                if error != nil {
                                    print("uiWebview_HighlightAllOccurencesOfString: \(String(describing: error))")
                                    return
                                }

                                // Get the total number of matches
                                webView.evaluateJavaScript("uiWebview_SearchResultTotalCount") { result, error in
                                    if error != nil {
                                        print("uiWebview_SearchResultTotalCount: \(String(describing: error))")
                                        return
                                    }
                                    
                                    if let result = result as? Int {
                                        viewModel.searchResultTotalCount = result
                                        viewModel.searchResultCounter = 1
                                        webView.evaluateJavaScript("uiWebview_ScrollTo(\(result))")
                                    }
                                }
                            }
                        }
                    }
                } else {
                    viewModel.searchResultTotalCount = 0
                    viewModel.searchResultCounter = 0
                     
webView.evaluateJavaScript("uiWebview_RemoveAllHighlights()")
                }
            }
            .store(in: &viewModel.subscriptions)
        // Observe the index to scroll to
        viewModel.$searchResultCounter
            .sink {
                let index = viewModel.searchResultTotalCount - $0 + 1
                
                webView.evaluateJavaScript("uiWebview_ScrollTo(\(index))")
            }
            .store(in: &viewModel.subscriptions)
        
        return webView
    }