Search and highlight text in UIWebView

Several iPhone Apps (like my “iCab Mobile” or “NewsTap” Apps) provide a search feature which allows to search for text in the content that is currently displayed within a UIWebView. The found occurrences of the searched text are highlighted with a yellow background, so the search result can be visually located very easy.

This blog post describes how this can be implemented. I’m implementing this feature as a category for the UIWebView class, so you can use the new search feature for all UIWebView objects in your Apps very easily.

First of all, UIWebView doesn’t allow us to access its content directly, so we have to use JavaScript again. But if you’ve read my other blog posts, you already know this approach.

Our goal is to implement two methods for our new UIWebView category. One method should start the search and highlights the search results. As a result this method should return the number of occurrences of the text we’ve searched. The other method should remove all the highlighted search results again to restore the original layout of the web page.

What we need to do is to write the main code in JavaScript and a wrapper code in Objective C which is simply calling the JavaScript code.

We start with the JavaScript code that is doing the real work. As I’ve already described in the blog post WebKit on the iPhone, the JavaScript code will be saved as resource file in the XCode project. This way it can be loaded from within the Objective C code from the application bundle very easily, and we don’t mix the code of multiple programming languages (JavaScript and Objective C) in the same files.

The following code is the the JavaScript implementation; below I’ll explain what it is doing and how it works:


// We're using a global variable to store the number of occurrences
var MyApp_SearchResultCount = 0;

// helper function, recursively searches in elements and their child nodes
function MyApp_HighlightAllOccurencesOfStringForElement(element,keyword) {
  if (element) {
    if (element.nodeType == 3) {        // Text node
      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

        var span = document.createElement("span");
        var text = document.createTextNode(value.substr(idx,keyword.length));
        text = document.createTextNode(value.substr(idx+keyword.length));
        element.deleteData(idx, value.length - idx);
        var next = element.nextSibling;
        element.parentNode.insertBefore(span, next);
        element.parentNode.insertBefore(text, next);
        element = text;
        MyApp_SearchResultCount++;	// update the counter
    } else if (element.nodeType == 1) { // Element node
      if ( != "none" && element.nodeName.toLowerCase() != 'select') {
        for (var i=element.childNodes.length-1; i>=0; i--) {

// the main entry point to start the search
function MyApp_HighlightAllOccurencesOfString(keyword) {
  MyApp_HighlightAllOccurencesOfStringForElement(document.body, keyword.toLowerCase());

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

// the main entry point to remove the highlights
function MyApp_RemoveAllHighlights() {
  MyApp_SearchResultCount = 0;

The basic principle of searching the text and removing the highlighted search results is the same: We’re working at DOM level (Document Object Model), which means the HTML document is represented as a tree structure where each HTML element, text, comment etc. is represented as a node. All the nodes are linked together with parent and child connections. The root element of each HTML document is the element that is created by the HTML tag. This element has usually two children: The HEAD element and the BODY element. Only the content of the BODY element is visible and displayed on screen, so we only need to process this part of the document tree.

What we need to do is to start with the body element and traverse all of its child nodes. From within the child nodes we need to go to their child nodes as well, and so on until we reach a leaf nodes, which has no child elements. Text nodes are always leaf nodes and text nodes are the nodes which might contain the text we’re looking for.

Traversing the whole HTML tree searching for all text nodes can be done by a recursive algorithm called Depth-First-Search (DFS). The DFS algorithm will traverse the tree structure starting from a root element (in our case the BODY element) to the first leaf node in a branch of the tree (for example going to the first child of the root first, from there again going to the first child, etc until a leaf node is reached). Then the algorithm goes back (backtracking) to the last node where not all child nodes were traversed yet and continues with the next unvisited child nodes etc. This way all nodes of the whole tree are traversed and we are able to find all the text nodes in which we are looking for the text we are earching.

The functions “MyApp_HighlightAllOccurencesOfStringForElement(element,keyword)” and “MyApp_RemoveAllHighlightsForElement(element)” are both implementations of this DFS algorithm. These functions are called from MyApp_HighlightAllOccurencesOfString(keyword)” and “MyApp_RemoveAllHighlights()” which are doing the necessary initialization and provide the DFS functions with the proper root element (the BODY element). The initialization for a new search is to make sure than no highlighted text from a previous search is present, so we simple call the function to remove all text highlights.

When searching for a text, we check if the currently inspected node is a text node or if it is an element node. If it is an element node it can have child nodes, and these must be inspected as well. If it is a text node, we have to find out if the text of this node contains the text we’re searching. If yes, we insert the text highlight, otherwise we are finished with this node. Also if the node is neither a text node nor an element node, there aren’t any child nodes which are interesting for us, so we are finished with this node as well.

When the text of a text node contains the searched text, we have to split the text into three parts. Part one will contain the text up to the searched text, part two contains the searched text itself and part three contains the rest of the text. A new element will be created (a SPAN element) and the second part (the searched text) will become a child node of this new element. Now we can assign StyleSheet rules to the newly created SPAN element to create the highlight effect (setting the background color to yellow, setting the text color to black, you can even increase the font size, if you want to). Now the new element is linked with part one and three so it becomes a part of the tree strucuture of the HTML tree. And because the searched text might be found multiple times in the original text node, we continue to search for the searched text in the third part of the original text node. If we find another occurrence of the searched text, we split this third part again in three parts, otherwise we are finished. When we create a SPAN element for the highlight effect, we also assign a special value (here “MyAppHighlight”) for the class attribute. This is important to be able to find these elements later again when we want to remove the highlight effects using the function “MyApp_RemoveAllHighlights()”. For this task we traverse the tree as well, but now we’re looking for elements whose class attribute has this special value. To restore the original state of the HTML document, we have to remove the elements we’ve inserted before (the ones with the special value of the class attribute) and we need to concatenate the text node we’ve split. JavaScript can help us to concatenate the text nodes again, because it provides the “normalize()” function which can do this for us.

In JavaScript we can find out the type of a node with the “nodeType” property. A value of 1 means that the node is a normal element node (like the “body” node, a “span” node etc.). A value of 3 means that the node is a text node. In this case the property nodeValue contains the text of the node. Other values for nodeType represent comment nodes (in HTML these are written as “<!– Comment –>”), attribute nodes (for HTML attributes like for example the “HREF” attribute for the “A” tag), document nodes and some more. In our case only the values 1 (element node) and 3 (text node) are important.

In the above implementation, we count the number of found occurrences in a global variable.

Note: You’ll notice that the JavaScript function names and variables and also the value for the class attribute I’m using in the above code are very lengthy and they do also have a prefix like “MyApp_”. The reason for this is to avoid any conflicts with existing function and variable names of the web page in which we inject our JavaScript code. If you’re generating the HTML code yourself that is displayed in the UIWebView object, you can choose shorter and simpler names. But if you have to deal with HTML and JavaScript code of any web pages (like in a web browser like iCab Mobile), you should use longer names and also add the name of your app as Prefix to all function and variable names to avoid any conflicts.

The Cocoa/Objective C part of the implementation is very simple. We only need to declare the interface and write a simple wrapper which loads and calls the JavaScript code that is actually doing all the hard work. The interface is also simple, we only need two methods: one to start the search and which highlights the found text and one which removes all the highlights again.


@interface UIWebView (SearchWebView)

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


The typical use case would be to provide a search field where the user can enter some text. This text would be passed to the method “highlightAllOccurencesOfString:”. And when the user shakes the device, the App could call the method “removeAllHighlights” to remove all the highlighted search results again.

The implementation would look like this:


@implementation UIWebView (SearchWebView)

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

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

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

- (void)removeAllHighlights
    [self stringByEvaluatingJavaScriptFromString:@"MyApp_RemoveAllHighlights()"];


The first thing we’re doing in the method “highlightAllOccurencesOfString:” is to load the JavaScript file we’ve written above from the application bundle and inject it into the web page that is currently displayed in UIWebView. Because we’re implementing this as a category for UIWebView, we can use “self” to call the method “stringByEvaluatingJavaScriptFromString:” of the UIWebView instances.

After we’ve injected the JavaScript code we simply call the JavaScript function we’ve defined above to do the search.

And finally we access the variable we’ve defined in the JavaScript code, which represents the number of occurrences of the string that were found, and we return its integer value as the result of the method.

In the method “removeAllHighlights” we only need to call the corresponding JavaScript function we’ve defined in the JavaScript code from above. Loading the external JavaScript file and injecting it into the Web page is not necessary here. If we’ve started a search before, the code is already injected and we don’t need to do this again. And if we haven’t started a search before, we just don’t need the JavaScript code because there are no highlights which have to be removed.

As you can see, the Objective C code for the UIWebView category is just a simple wrapper code for the JavaScript code. Now, when you have a UIWebView object in your App, you can simply search for text in its content by calling “highlightAllOccurencesOfString:”, like in this example where we’re searching for the text “keyword”:

  // webView is an instance of UIWebView
  [webView highlightAllOccurencesOfString:@"keyword"];

Additional notes: In case your App has to deal with web pages which can have frames, you have to add some additional code that looks for frames. You have to traverse all the documents of all the frames to find all the text nodes in all frames. The code from above isn’t doing this to keep it as simple as possible.

318 thoughts on “Search and highlight text in UIWebView

  1. Hi it’s me again, i have a question but i’m not sure this is possible. You know the button next-previous, for the search button, like in Safari Mobile ? Well i would like to know if it’s possible to make the javascript tell the button to go to that and that higlighted, like that you don’t have to search for the highlighted results 🙂 or just make the page move to the first result ? Thanks !

  2. @Raphael
    You can do this. You have to know the currently highlighted result and when the users clicks on next/previous, you have to search for the next or previous occurrence and then highlight this. You can do this by collecting all found elements in an array and all you need to do is to maintain a index for the currently selected element. the previous button will decrease the index and the next button will increase the index. Each time you change the index, you just remove the selection from the currently selected element and then highlight the newly selected element.

  3. Hi,
    I encountered a problem when i read js file. I print nsstring after reading of js file. and it shows null. No content after reading js file. i exactly copy your code and try to run.

  4. Hi ALexander
    in UIwebView i have loaded the RTF and its showing underline for particular text also (this is rtf text \\plain\\f0\\fs20\\cf0\\b\\ul Past Medical History:) , in my java script i have used following code to find underline
    textdecoration =window.getComputedStyle(span, false).textDecoration;
    but im getting textdecoration as “None”, any idea why !!!!!!!????

  5. @sud
    No, I’m sorry. I haven’t done very much with RTF files yet, so I’m not yet sure how these are converted into HTML. So it might be possible that you’ve found the wrong “span” element.

  6. hi! how i can put info from UITextField to [theWebView highlightAllOccurencesOfString:@”here”];

    Thanks for any help.

  7. @nik1004
    Just use the “text” method of the UITextField object to get the text that was entered and use it as parameter for the highlightAllOccurencesOfString: method.

    The delegate methods of UITextField will allow you to get notified when the user has hit the return key, so you know what you can get the text.

  8. @Alexander :-
    hey i have one HTML content in that it contains buttons in it, after click on that i have push UIViewController, how it can be done?

  9. @sud
    When you press the button, you need to open a URL using “location.href” in order to get back to your Cocoa code. Opening a URL will call the delegate method “webView:shouldStartLoadWithRequest:navigationType:”. You should use a custom URL scheme for this, which is not “http” in order to make it easy to identify these special requests in the delegate method. And in the delegate method you can push your UIViewController and don’t forget to return NO to tell UIWebvIew that the request should not be processed by UIWebView.

  10. Hi Alexander

    where should i write my onclick action , in separate “.js” file, if its so how and when to call it please advice
    theWebView=[[UIWebView alloc]initWithFrame:theRect];
    NSString *theString=@”The selected file format is not supported. “;

    [theWebView setUserInteractionEnabled:YES];
    theWebView.opaque = NO;
    theWebView.backgroundColor = [UIColor whiteColor];
    [theWebView setScalesPageToFit:YES];
    [theWebView loadHTMLString:theString baseURL:nil];
    [self.view addSubview:theWebView];
    [theWebView release];

  11. this is my html string which i am loading
    NSString *theString=@”The selected file format is not supported. “;

  12. Alexander: regarding Raphael’s early question on stepping through all instances of found text, and your suggestion to build an array of found elements, precisely where in your SearchWebView.js code should that be done? Just after the “if (idx < 0)" check? Or just before incrementing "MyApp_SearchResultCount"? And when the app steps to the next or previous element, should "blur" be used to undo the existing selection and "focus" be used to set a new selection? Will calling "focus" scroll the UIWebView to the new selection? Thanks for your time.

  13. @Frank Natoli

    Adding the elements to the array when incrementing “MyApp_SearchResultCount” should be fine. This is where you add the “highlight”, so this new “highlight” element is exactly where you want to scroll to later.

    “blur” can’t be used to remove the “highlight”, because this is not a normal text selection for copy & paste. What is done here is to set the background color for the text segments using CSS, and so you have to remove all the stuff you’ve added before to (this includes the helper node that was added to be able to set the background color). The function “MyApp_RemoveAllHighlightsForElement()” from above gives you an idea what needs to be done.

    And “focus” won’t work either, because this is not anything that can get the focus.

  14. Dear Alexander,

    Your code works a treat, so thanks for sharing. I’m just curious why you have made your code to normalize the current DOM element conditional on changes committed to childNodes of the current element?

    if (normalize) { element.normalize(); }

    The only reason I can think of is that you wish the prevent the overhead of normalization when normalization is not necessary, but I wonder at what stage (number of childNodes) the overhead if offset by the number of boolean checks that need to be made?

  15. @Marcel Oomens
    Yes, the normalization should only be done when there are actually changes within the child nodes. And the normalization, even if not necessary and would not result in any modification of the DOM tree, is probably always much more expensive than the boolean check. The normalization would require to traverse part of the DOM tree to check for any adjacent text nodes, and this is definitely more than a single if condition with a boolean value.

  16. hi,
    i am using your code in my app and i want to add two button for zoom in and zoom out
    when clicked on, webview text should zoom in or out

    how can i do this?

    please tell me.


  17. @srinivas
    IMHO, zooming should be done as in any other App by pinch gestures or by double-tapping. Using buttons is probably not the best way to do this.

    Changing the zoom level manually is not that easy. You can do this by adding or changing a META tag for the “viewport” in the HTML code. Also the following Javascript code should work:;

  18. hy, Thank you for this blog, for posting the code and for the clear explanation, it was extremely useful, but what I need is to search in several html files not only one, I was wondering if it was doable, and if u give me a hint for doing it,
    Thanks again.

  19. thanks alexander for your clarification,
    i am also against this option but client wants it 🙁

    one suggestion: impliment threaded views from wp admin area so that comments looks well and easy to understand.


  20. Hy again! Old problem solved and another problem occured. If the nodes in my html file are made of one word each, how can modify the search so I can search for several words together.
    thanks in advance.

  21. @coder
    Searching will be more difficult in this case. You need to keep track of partly found search terms and check if adjacent text nodes contain the other parts of the search term. There are multiple ways to implement this. None is really simple. One simple approach might be to first combine all texts in all nodes into a single string, and also create a list or array of nodes and offsets which will tell you where within this string the nodes of the HTML string are starting and ending. This way the searching is easy, because you only have one single string. and getting to the actual nodes is also not that complicated anymore.

  22. @Alex
    Search frames is done in the same way as searching the main document. You only need to iterate through all of the frames and search here in the same way. frames are also “window” objects in JavaScript. And the “document” property that is used in my code directly is just a short form for “window.document”, so you can use this document for frame objects as well.
    To get the frames of a window (or frame) use the “frames” property of this window object (window.frames). This property is an array and lists all frames of that window/frame.

  23. Thanks Alexander.
    yeah, I did it as you said, but it doesn’t work in this situation: the main page and the iFrame/Frame page are from different domain.
    I try to find some cross-domain solution, but i get nothing.
    it seems need to call the private APIs of webkit. but Apple not allow to do this.
    It made me mad!

  24. Thanks Alexander, I considered your solution but thought could be time consuming for large html files in fact what I did is I stored the set of words to be searched for in an array and as I go over the nodes recursively I check the words in the array consecutively if one fails I restore the index to start the search for the words all over again, if all the nodes verify the words consecutively so its a match. The hard part was to delete the words and replace them highlighted ones but at the end it worked.
    Thank you very much for your solution it also inspired me for other functions.

  25. @Alexander
    Did you hear about UC Web Browse? A Chinese company’s product, it can show the context menu when you long press at the cross-domain-iframe area. I don’t know how they do that. They announce that they has their own web-engine base on webkit, and the html5test shows that UC’s scores is different from the safari mobile. But I’m not sure if they REALLY use their own engine.

  26. It looks like the UC Browser only uses the standard contextual menu of UIWebView, and this does always work, even with cross-domain iframes. But it only has the items “Copy ” and “Open”.

  27. @Alexander:

    No, Maybe we run the different version of UC, It doesn’t show the context menu of “Open” and “Copy”, it shows the context menu of “Open in New Tab”, “Open in background”, “Page Properties”.
    The version of UC that I installed is the newest Chinese version.

  28. @Alex
    I did only checked the English version. But it looks like the Chinese version is really either using a self-made web engine (which Apple does not allow) or is using lots of private API calls (which is also not allowed). In any case, the web engine of this browser is doing things, which can’t be done with UIWebView and its public API (and is also not doing things, which UIWebView does by default, like supporting the Copy&Paste feature).

  29. Hy again, thank you for your constant assistance, I need your advise on a new issue. I’m working on highlight and I want to save the range so when I reload the html file again into the webview I can show the highlight again. the problem is you cannot return an object from javascript and if I save it inside javascript I will lose it when I reload the html file.
    I thought of saving the focusOffset but it only gives the offset as per node, so if you can give me any hint I would truly appreciate,
    thanks in advance

  30. @coder
    When reloading the file you have to deal with the possibility that the file has changed. So all the data you’ve saved before might no longer match. If the file has not changed, you might be able to restore the old selection by saving and restoring the “path” to your selection (like a list of tags and child indexes you need to travel to to find your nodes again). Just an idea. I haven’t implemented it yet, but it doesn’t look too complicated.

  31. hy Alexander, in fact the file is an html file that doesn’t change, I took your idea and saved the number of nodes to each highlight and at the beginning it worked but when I make more then one highlight things start to get weird, the problem is that, each time I make highlight I’m inserting the span node so the number of nodes is varying, any suggestion for this problem?
    Thank u for ur help,

  32. @coder
    Of course, if you keep adding nodes, you need to make sure the data you’ve already collected is being adapted to the new content. So which each node you add, you have to check if this affects the nodes you’ve remembered so far. Which measn if the path to the newly added node is a prefix of a previously stored path, then you need to adapt the previously stored path so it will take into account the new node or child that was inserted in the DOM tree.

  33. I found this really helpful. Can I ask what the copyright situation is with the code you posted? Is it essentially open source and free to use in any way?

    Thanks very much

  34. Hy Alexander, your tips were very useful thank u. Well facing a new prob. i’m trying to do a night theme I used ([NSString stringWithFormat:@”document.getElementsByTagName(‘body’)[0].style.color= ‘lightGray'”];)
    to change the font color but the problem is that not all the body gets modified, (titles for example stay the same color) although when I use
    [NSString stringWithFormat:@”document.getElementsByTagName(‘body’)[0].style.webkitTextSizeAdjust= ‘%d%%'”, font];
    the whole text font size gets modified even the titles.
    Any idea why this is happening?

    Another question: as an alternative way I tried your code above went over the nodes one by one to change the font color it worked but the problem while I have the night mode the user cannot make a highlight anymore.

    Advise please,
    thx a in advance

  35. @coder
    Your approach works for all child elements of body for with there’s no color defined. But if a web page defines a color for headings for example, these headings will use their own color, unless you re-define these colors as well. Also depening of how well done the code of the web page is, the browser might use the “Quirks-Mode” rendering, which is using the rendering that was done in the past, which has also some additional oddities, like inheritance of CSS rules stops at table boundaries.

  36. Hy Alexander, well i’m working with ebooks, in fact the books that contain a class with

    doesn’t change the color with the first approach I use. How can I redefine the style?
    Thank you for your advices

  37. Alexander one more question can I make a span for a node that is already span. cause when I try to highlight an already highlighted word its not allowing me.

  38. @coder
    With CSS you can apply any css rules to any HTML element. So when a certain element does not respond to the CSS rule you’ve applied to the boy element, then it is likely that there’s already a more specific CSS rule for this element present which does define its color. In this case you need to locate this element and get a reference to it, so you can apply your own color to it directly, just like you’ve done this for the body element.

    You can nest SPAN elements, but please note, that this might not necessarily help you. If you have something like <span><span>text</span></span>, with the standard layout, both spans will have the same size and are sitting on top of each other, so the top (inner) one would totally cover the bottom (outer) one.
    Of course with CSS you can format both SPANs individually, move them around, change their size and colors etc. and combine them visually. So having multiple SPANs can be useful for certain tasks.
    For the simple highlighting, which is simply a background color, having multiple nested SPANs will probably not help, because the top (inner) SPAN with a background color applied to will completely cover the outer SPAN.
    So depending of what you want to do, you might try nested SPANs.

    But my sample code here is not really meant to be used like this.

  39. Hy Alexander, I would truly like to thank u for ur help, finally its working now, I used the css instead of nested span.
    Anyway, I have another issue now, I’m trying to make an inApp purchase. I was wondering if u have any tutorial about this subject. Thanks in advance.

  40. hy again, look I just need an advise. To make my search faster I’m saving the webviews I load into strings, so everytime I want to search in them I won’t have to load them again, and indeed it made things much quicker but I’m worried memory wise. A user may have 50 books and in each books lets say 20 chap and taking worst case senario in each chap like 100 pages, so I’m saving into strings 100 000 pages. how much would that be in memory? and is it good practice
    thanks in advance
    Awaiting ur reply

Leave a Reply

Your email address will not be published. Required fields are marked *