WKWebView搜索高亮

搜索关键字、显示高亮关键字
参考:https://www.jianshu.com/p/67f37e1723da

在使用FLEX(https://github.com/FLEXTool/FLEX,版本4.7.0)抓包看Response时,默认显示是个WKWebView,如果内容比较多则无法快速找到,添加搜索栏,搜索框、上一个、下一个、搜索匹配数,高亮黄色,当前橙色

实现效果

打开网络抓包

FLEXManager.sharedManager.networkDebuggingEnabled = YES;

路径:menu -> Network History -> 找到请求 -> Response Body(如果有)

JS代码

WLSearchWebView.js基于参考稍微重命名而已

//链接:https://www.jianshu.com/p/67f37e1723da

var web_searchResultCount = 0;
var web_searchResults = [];
var web_selectSpan;
var web_switchIndex = 0;

//根据关键字查找
function web_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));
                span.appendChild(text);
                span.setAttribute("class","WebHighlight");
                span.style.backgroundColor="yellow";
                span.style.color="black";

                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;

                web_searchResultCount++; // update the counter
                web_searchResults.push(span);
            }
        } 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--) {
                    web_highlightAllOccurencesOfStringForElement(element.childNodes[i], keyword);
                }
            }
        }
    }
}

// 根据关键字查找 供WKWebView调用
function web_highlightAllOccurencesOfString(keyword) {
    web_removeAllHighlights();
    web_highlightAllOccurencesOfStringForElement(document.body, keyword.toLowerCase());
}

// 关键关键字 移除高亮显示
function web_removeAllHighlightsForElement(element) {
    if (element) {
        if (element.nodeType == 1) {
            if (element.getAttribute("class") == "WebHighlight") {
                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 (web_removeAllHighlightsForElement(element.childNodes[i])) {
                        normalize = true;
                    }
                }
                if (normalize) {
                    element.normalize();
                }
            }
        }
    }
    return false;
}

//WKWebView调用移除高亮
function web_removeAllHighlights() {
    web_searchResultCount = 0;
    web_searchResults = [];
    web_removeAllHighlightsForElement(document.body);
}

//滚动到搜索到的第一个关键字
function web_scrollToFiristResult() {
    web_scrollToIndex(0);
}

//滚动到指定下标的关键字
function web_scrollToIndex(index) {
    if (web_selectSpan) {
        web_selectSpan.style.backgroundColor = "yellow";
    }
    var span = web_searchResults[web_searchResultCount - index - 1];
    span.style.backgroundColor = "orange";
    web_selectSpan = span;
    var h = document.body.clientHeight; //获取设备的高度
    window.scrollTo(0,span.offsetTop-h/2); //滚动到屏幕中央
}

// 上移
function web_switchToUp() {
    web_switchIndex--;
    if (web_switchIndex < 0) {
        web_switchIndex = web_searchResultCount-1;
    }
    web_scrollToIndex(web_switchIndex);
    return web_switchIndex;
}

// 下移
function web_switchToDown() {
    web_switchIndex++;
    if (web_switchIndex >= web_searchResultCount) {
        web_switchIndex = 0;
    }
    web_scrollToIndex(web_switchIndex);
    return web_switchIndex;
}

OC代码

JS调用类

建了分类WKWebView+WLSearchWebViewWKWebView+WLSearchWebView.h文件如下

//
//  WKWebView+WLSearchWebView.h - WKWebView关键字搜索JS调用
//  FLEXample
//
//  Created by WeeverLu on 2022/9/4.
//  Copyright © 2022 Flipboard. All rights reserved.
//
//  https://www.jianshu.com/p/67f37e1723da

#import <WebKit/WebKit.h>

NS_ASSUME_NONNULL_BEGIN

typedef void(^WLSearchResultBlock)(NSInteger searchCount);

@interface WKWebView (WLSearchWebView)

/// 搜索关键字
/// @param str 关键字
/// @param searchResultBlock 查找的总数
- (void)search_highlightAllOccurencesOfString:(NSString*)str searchResultBlock:(WLSearchResultBlock)searchResultBlock;

/// 滚动到指定的下标
/// @param index 指定的下标
- (void)search_scrollToIndex:(NSInteger)index;

/// 上移
- (void)search_scrollToUp;

/// 下移
- (void)search_scrollDown;

/// 移除高亮
- (void)search_removeAllHighlights;

@end

NS_ASSUME_NONNULL_END

WKWebView+WLSearchWebView.m文件如下

//
//  WKWebView+WLSearchWebView.m - WKWebView关键字搜索JS调用
//  FLEXample
//
//  Created by WeeverLu on 2022/9/4.
//  Copyright © 2022 Flipboard. All rights reserved.
//

#import "WKWebView+WLSearchWebView.h"

#define kWLJSFileName @"WLSearchWebView"

@implementation WKWebView (WLSearchWebView)

- (void)loadSearchJavaScript {
    static NSString *searchJS = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSString *path = [[NSBundle bundleForClass:NSClassFromString(@"FLEXWebViewController")] pathForResource:kWLJSFileName ofType:@"js"];
        searchJS = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
        //NSLog(@"js == %@", js);
    });
    if (searchJS) {
        [self evaluateJavaScript:searchJS completionHandler:nil];
    }
}

- (void)search_highlightAllOccurencesOfString:(NSString *)string searchResultBlock:(WLSearchResultBlock)searchResultBlock {
    [self loadSearchJavaScript];

    __weak typeof(self) weakSelf = self;
    NSString *startSearch = [NSString stringWithFormat:@"web_highlightAllOccurencesOfString('%@')", string];
    [self evaluateJavaScript:startSearch completionHandler:^(id _Nullable result, NSError * _Nullable error) {
        [weakSelf evaluateJavaScript:@"web_scrollToFiristResult()" completionHandler:nil];
    }];

    [self evaluateJavaScript:@"web_searchResultCount" completionHandler:^(id _Nullable result, NSError * _Nullable error) {
        searchResultBlock([result integerValue]);
    }];
}

- (void)search_scrollToIndex:(NSInteger)index {
    NSString *js = [NSString stringWithFormat:@"web_scrollToIndex(%ld)", (long)index];
    [self evaluateJavaScript:js completionHandler:nil];
}

- (void)search_scrollToUp {
    [self evaluateJavaScript:@"web_switchToUp()" completionHandler:nil];
}

- (void)search_scrollDown {
    [self evaluateJavaScript:@"web_switchToDown()" completionHandler:nil];
}

- (void)search_removeAllHighlights {
    [self evaluateJavaScript:@"web_removeAllHighlights()" completionHandler:nil];
}

@end

FLEX相关结合

Podfile使用framework,加入use_frameworks!
FLEX.podspec添加资源,加入spec.resource = "Classes/**/*.{png,plist,xcassets,json,js}"

搜索栏

WLWebViewSearchToolbar.h文件如下:

//
//  WLSearchWebView.h - WKWebView关键字搜索Toolbar封装
//  FLEX
//
//  Created by WeeverLu on 2022/9/4.
//

#import <UIKit/UIKit.h>
#import <WebKit/WebKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface WLWebViewSearchToolbar : UIView

@property (nonatomic, strong) UIView *parentView;
@property (nonatomic, strong) UINavigationController *navigationController;

@property (nonatomic, assign, getter=isShow) BOOL show;

- (instancetype)initWithWebView:(WKWebView *)webView;

- (void)showSearchView;
- (void)hideSearchView;

@end

NS_ASSUME_NONNULL_END

WLWebViewSearchToolbar.m文件如下:

//
//  WLSearchWebView.m - WKWebView关键字搜索Toolbar封装
//  FLEX
//
//  Created by WeeverLu on 2022/9/4.
//
//  -----------------------------
//   |     textField     | ← → 0
//  -----------------------------

#import "WLWebViewSearchToolbar.h"
#import "WKWebView+WLSearchWebView.h"

@interface WLWebViewSearchToolbar () <UITextFieldDelegate, UIScrollViewDelegate>
@property (nonatomic, strong) UIToolbar *toolbar;
@property (nonatomic, strong) UITextField *textField;
@property (nonatomic, weak) WKWebView *webView;
@property (nonatomic, assign) NSInteger numberOfSearchCount;
@property (nonatomic, strong) UILabel *numberOfSearchCountLbl;
@end

@implementation WLWebViewSearchToolbar

- (void)dealloc {
    [NSNotificationCenter.defaultCenter removeObserver:self];
}

- (instancetype)initWithWebView:(WKWebView *)webView {
    self = [super init];
    if (self) {
        self.autoresizingMask = UIViewAutoresizingFlexibleWidth;
        
        self.webView = webView;
        self.webView.scrollView.delegate = self;
        
        [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(textFieldTextDidChange:) name:UITextFieldTextDidChangeNotification object:nil];
    }
    return self;
}

- (void)layoutSubviews {
    [super layoutSubviews];
    
    //旋转处理
    CGFloat height = 44;
    CGFloat y = UIApplication.sharedApplication.statusBarFrame.size.height + self.navigationController.navigationBar.frame.size.height;
    self.frame = CGRectMake(0, y, self.parentView.bounds.size.width, height);
    
    CGRect textFieldRect = self.textField.frame;
    textFieldRect.size.width = self.toolbar.bounds.size.width - 45*(self.toolbar.items.count-1);
    self.textField.frame = textFieldRect;
}

#pragma mark - Action
- (void)previousItemDidTap {
    if (self.numberOfSearchCount > 0) {
        [self.webView search_scrollToUp];
    }
}

- (void)nextItemDidTap {
    if (self.numberOfSearchCount > 0) {
        [self.webView search_scrollDown];
    }
}

- (void)handleTextDidChange {
    NSString *text = self.textField.text;
    if (!text || text.length <= 0) {
        [self resetSearchView];
        return;
    }
    
    __weak typeof(self) weakSelf = self;
    [self.webView search_highlightAllOccurencesOfString:text searchResultBlock:^(NSInteger searchCount) {
        weakSelf.numberOfSearchCount = searchCount;
    }];
}

- (void)resetSearchView {
    self.textField.text = @"";
    self.numberOfSearchCount = 0;
    [self.webView search_removeAllHighlights];
}

- (void)showSearchView {
    if (self.isShow) {
        return;
    }
    
    self.show = YES;
    CGFloat height = 44;
    CGFloat y = UIApplication.sharedApplication.statusBarFrame.size.height + self.navigationController.navigationBar.frame.size.height;
    self.frame = CGRectMake(0, y, self.parentView.bounds.size.width, height);
    [self.parentView addSubview:self];
    
    [self addSubview:self.toolbar];
    [self resetSearchView];
    
    self.alpha = 0;
    [UIView animateWithDuration:0.25 animations:^{
        self.webView.scrollView.contentOffset = CGPointMake(0, self.webView.scrollView.contentOffset.y-height);
        self.webView.scrollView.contentInset = UIEdgeInsetsMake(height, 0, 0, 0);
        self.webView.scrollView.scrollIndicatorInsets = UIEdgeInsetsMake(height, 0, 0, 0);
        self.alpha = 1;
    } completion:^(BOOL finished) {
        [self.textField becomeFirstResponder];
    }];
}

- (void)hideSearchView {
    self.show = NO;
    [self resetSearchView];
    
    [UIView animateWithDuration:0.25 animations:^{
        self.webView.scrollView.contentInset = UIEdgeInsetsZero;
        self.webView.scrollView.scrollIndicatorInsets = UIEdgeInsetsZero;
        self.alpha = 0;
    } completion:^(BOOL finished) {
        [self removeFromSuperview];
    }];
}

- (void)scrollToFiristResult {
    [self.webView search_scrollToIndex:0];
}

- (void)hideKeyboard {
    [self.textField resignFirstResponder];
}

#pragma mark - UITextFieldDelegate
- (BOOL)textFieldShouldClear:(UITextField *)textField {
    [self handleTextDidChange];
    return YES;
}

- (BOOL)textFieldShouldReturn:(UITextField *)textField {
    [self handleTextDidChange];
    [self hideKeyboard];
    return YES;
}

- (void)textFieldTextDidChange:(UITextField *)textField {
    [self handleTextDidChange];
}

#pragma mark - UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    [self hideKeyboard];
}

#pragma mark - Setter/Getter
- (UIToolbar *)toolbar {
    if (!_toolbar) {
        _toolbar = [[UIToolbar alloc] initWithFrame:self.bounds];
        _toolbar.autoresizingMask = UIViewAutoresizingFlexibleWidth;
        UIBarButtonItem *textItem = [[UIBarButtonItem alloc] initWithCustomView:self.textField]; //textField
        UIBarButtonItem *previousItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemRewind target:self action:@selector(previousItemDidTap)]; //上一个
        UIBarButtonItem *nextItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFastForward target:self action:@selector(nextItemDidTap)]; //下一个
        UIBarButtonItem *matchesItem = [[UIBarButtonItem alloc] initWithCustomView:self.numberOfSearchCountLbl]; //搜索匹配数
        
        _toolbar.items = @[ textItem, previousItem, nextItem, matchesItem ];
    }
    return _toolbar;
}

- (UITextField *)textField {
    if (!_textField) {
        _textField = [[UITextField alloc] initWithFrame:CGRectMake(0, 0, 0, 30)];
        _textField.placeholder = @"请输入搜索文本";
        _textField.delegate = self;
        _textField.clearButtonMode = UITextFieldViewModeWhileEditing;
        _textField.returnKeyType = UIReturnKeyDone;
        _textField.autoresizingMask = UIViewAutoresizingFlexibleWidth;
    }
    return _textField;
}

- (UILabel *)numberOfSearchCountLbl {
    if (!_numberOfSearchCountLbl) {
        UILabel *countLbl = ({
            UILabel *lbl = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 40, 40)];
            lbl.textAlignment = NSTextAlignmentCenter;
            lbl.userInteractionEnabled = YES;
            lbl.adjustsFontSizeToFitWidth = YES;
            UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(scrollToFiristResult)]; //双击回到第一个
            tap.numberOfTapsRequired = 2;
            [lbl addGestureRecognizer:tap];
            lbl;
        });
        _numberOfSearchCountLbl = countLbl;
    }
    return _numberOfSearchCountLbl;
}

- (void)setNumberOfSearchCount:(NSInteger)numberOfSearchCount {
    _numberOfSearchCount = numberOfSearchCount;
    self.numberOfSearchCountLbl.text = [NSString stringWithFormat:@"%ld", (long)numberOfSearchCount];
}

@end

修改FLEXWebViewController.m

添加引入

#import "WLWebViewSearchToolbar.h"
#import "FLEXActivityViewController.h"

添加搜索栏

@property (nonatomic, strong) WLWebViewSearchToolbar *searchView;

- (WLWebViewSearchToolbar *)searchView {
    if (!_searchView) {
        _searchView = [[WLWebViewSearchToolbar alloc] initWithWebView:self.webView];
        _searchView.parentView = self.view;
        _searchView.navigationController = self.navigationController;
    }
    return _searchView;
}

修改右上角的Copy,改为Sheet选择,复制、分享、搜索

UIBarButtonItem *shareItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAction target:self action:@selector(shareButtonTapped:)];
self.navigationItem.rightBarButtonItem = shareItem;


- (void)shareButtonTapped:(id)sender {
    [FLEXAlert makeSheet:^(FLEXAlert * _Nonnull make) {
        make.button(@"复制").handler(^(NSArray<NSString *> * _Nonnull strings) {
            [UIPasteboard.generalPasteboard setString:self.originalText];
        });
        make.button(@"分享").handler(^(NSArray<NSString *> * _Nonnull strings) {
            UIViewController *shareSheet = [FLEXActivityViewController sharing:@[self.originalText] source:sender];
            [self presentViewController:shareSheet animated:true completion:nil];
        });
        NSString *searchTitle = self.searchView.isShow ? @"关闭搜索" : @"显示搜索";
        make.button(searchTitle).handler(^(NSArray<NSString *> * _Nonnull strings) {
            if (self.searchView.isShow) {
                [self.searchView hideSearchView];
            } else {
                [self.searchView showSearchView];
            }
        });
        make.button(@"取消").cancelStyle();
    } showFrom:self source:sender];
}

FLEXExplorerViewController.m
弹出全屏,修改模式toPresent.modalPresentationStyle = UIModalPresentationFullScreen;
修改点如下:

- (void)presentViewController:(UIViewController *)toPresent
                               animated:(BOOL)animated
                             completion:(void (^)(void))completion {
    // ....

    // Show the view controller
    toPresent.modalPresentationStyle = UIModalPresentationFullScreen;
    [super presentViewController:toPresent animated:animated completion:completion];
}

目录结构

FLEX目录结构
WLSearchWebView文件夹
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,290评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,399评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,021评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,034评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,412评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,651评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,902评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,605评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,339评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,586评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,076评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,400评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,060评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,851评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,685评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,595评论 2 270

推荐阅读更多精彩内容