UIWebView中contentEditable/designMode时的自动滚动

16
我正在尝试为我正在开发的iPhone应用程序设计一个富文本编辑器(具有HTML导出功能),并决定使用iOS 5的WebKit支持 contentEditable / designMode 。 我遇到了一个问题,这个问题对我来说很棘手。在UIWebView中编辑内容时,没有自动滚动以跟随光标,例如在UITextView中。当输入时,光标继续在scrollView下面,用户必须手动向上滚动。
下面是相关代码:
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
    NSString *string = @"document.body.contentEditable=true;document.designMode='on';void(0)";
    [webView stringByEvaluatingJavaScriptFromString:string];

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
}

任何想法如何解决这个问题?我不确定这是否也发生在Safari中,或者仅发生在UIWebView的WebKit实现中。
如果您遇到此问题,请务必转到https://bugreport.apple.com并复制rdar:// 16455638
5个回答

11

经过数小时的研究,我找到了一种更好的解决方法,因此我想分享一下。

iOS 5 的 contentEditable 实现存在一个 bug,即主滚动视图不会随着光标滚动。如果将 body 标签(或任何动态大小的元素)设置为 contentEditable,则光标将始终超出屏幕。

如果将可编辑的 div 设置为溢出滚动,您将注意到 div 将滚动。该 div 的滚动默认情况下不会“反弹”或具有滚动条,但是您可以将 -webkit-overflow-scrolling: touch 属性应用于 div 来修复此问题。

有了这些信息,您可以通过以下包装器来修复它:

<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width; initial-scale=1.0; maximum-scale=1.0; user-scalable=no;"/>
<style type="text/css">
    html, body {height: 100%;}
    body {
        margin: 0;
    }
    #wrap {
        height: 100%;
        width: 100%;
        overflow-y: scroll;
        -webkit-overflow-scrolling: touch;
    }
    #editor {
        padding: 10px;
    }
</style>
</head>
<body>
<div id="wrap">
    <div id="editor" contenteditable="true">

    </div>
</div>
</body>
</html>

基本上你正在滚动div而不是文档。

不幸的是,div的“scrollView”不知道虚拟键盘,所以光标会消失在键盘后面。但是,你会注意到光标位置仍然在屏幕底部,位于键盘后面。因此,为了解决这个问题,减小div / UIWebView的高度以适应键盘。

你可能还想做的另一件事是禁用主scrollView上的滚动:

webView.scrollView.scrollEnabled = NO;

主要的scrollView不应该滚动,但它应该防止任何滚动故障。


嗨,Luke,我会把你的答案设为采纳的答案。我自己尝试过这个(没有-webkit-overflow-scrolling),但不幸的是它还是有点 bug。我接受它是因为它最接近我在问题中所需的。 - Léo Natan
我想补充一点,你不一定需要一个#wrap,你也可以把所有的CSS都放到body上,那样也可以工作。 - Michael Heilemann
1
很棒的解决方案!但我注意到一个小问题 - 如果我选择(高亮)一个单词并开始滚动,那么高亮状态会在滚动时保持不变,并且只有在滚动结束时才消失。有什么办法可以克服这个问题吗? - vivek241
你如何根据按键动态调整webview的大小? - Bourne
3
当针对iOS 7时,有更好的解决方案吗? - catlan

4
为了回答我的问题,我们最终需要在多个iOS版本上进行大量工作以确保其正确运行。我们发现所有的滚动事件都是由于UIWebView尝试管理其UIWebScrollView引起的。我们决定不使用UIWebView,而是将内部的UIWebDocumentView/UIWebBrowserView添加到我们自己的滚动视图中。这使我们能够自己管理内容大小和滚动,并消除了我们之前遇到的大多数问题。

我想探索这条路线,但我担心因为那些是私有类(没有真正的文档)。你提交这个应用程序时有遇到任何问题吗?有什么“安全”的方法和需要避免的事情吗?例如,您是否必须创建UIWebView,然后重新设置视图的父级,而不是直接创建这些其他类 - 以避免应用商店拒绝? - eselk
1
@eselk 我们使用许多技巧,例如方法交换、ISA交换等。在这种情况下,更简单的方法是从公共API中获取滚动视图的子视图,并将其添加到另一个我们控制的滚动视图中。 - Léo Natan
抱歉,实际编写此代码需要太多时间。这不是一个简单的任务。 - Léo Natan

1

首先,您需要注册键盘通知,在keyboardWillShow方法中,触发一个0.1秒的滚动方法。

 -(void) keyboardWillShow:(NSNotification *)note
    {

        timer   =   [NSTimer    scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(scroll) userInfo:nil repeats:YES];

    }

    -(void) keyboardWillHide:(NSNotification *)note
    {
        [timer invalidate];

    }

在 .h 文件中创建一个成员变量,并在 init() 方法中为其分配内存。
  NSString    *prevStr;

在滚动方法内,执行以下操作。
-(void)scroll{

    if (![prevStr isEqualToString:[WebView stringByEvaluatingJavaScriptFromString:@"document.body.innerHTML"]]) {
        prevStr  =   [[WebView stringByEvaluatingJavaScriptFromString:@"document.body.innerHTML"] retain];
        NSInteger height = [[WebView stringByEvaluatingJavaScriptFromString:@"document.body.offsetHeight;"] intValue];
        NSString* javascript = [NSString stringWithFormat:@"window.scrollBy(0, %d);", height];   
        [WebView stringByEvaluatingJavaScriptFromString:javascript];   
    }

}

这将允许您在编辑内容时滚动到底部,当内容大于WebView框架时。而其他时间,您将能够滚动到页面顶部(此时自动滚动会被暂停)。


嗨 Selvin,那不会将 Webview 滚动到底部吗?这不是我始终需要的功能。例如,如果我有一段很长的文本,并且我在文本中间点击,光标应该停留在我点击的地方,而不是滚动到底部。或者我可能误解了? - Léo Natan
嗨@Leo。很遗憾我不知道如何做到这一点。因为当您将光标放在文本中间时,我无法获取滚动视图的当前偏移量。也许我们应该尝试获取当前内容偏移量,如果它不接近底部,我们就必须停止自动滚动。 - Selvin

0

你可以尝试使用一些巧妙的技巧来获取插入符号位置,并手动滚动到可见矩形区域。

// Call this after every change in editable HTML document
func bodyDidChanged() {
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
        let caretRect = self.caretRect(inView: self.webView)
        self.webView.scrollView.scrollRectToVisible(caretRect.insetBy(dx: -2, dy: -2), animated: true)
    }
}

private func caretRect(inView parentView: UIView? ) -> CGRect {
    guard let parentView = parentView else {
        return CGRect.null
    }

    var rect: CGRect?
    for view in parentView.subviews {
        if view.isKind(of: NSClassFromString("UITextSelectionView")!) {
            // If have a selected text, then we seeking last blue dot (grabber) inside rangeView
            var rangeView = view.subviews.first?.subviews.last
            if rangeView == nil {
                // If text not selected then carret frame is same as rangeView frame
                rangeView = view.subviews.first
            }
            rect = rangeView?.frame
            break
        } else {
            rect = caretRect(inView: view)
            if let rect = rect, !rect.isNull {
                break
            }
        }
    }
    return rect ?? CGRect.null
}

0

我无法使用您的技巧设置"overflow: scroll",因为它会破坏我们已经很好的CSS(如果我们在单击可编辑div时更改了溢出,则布局会混乱)。 我选择了这个:

$(this).on('keypress',function(e){
    //$(this) is the contenteditable div
    var lineHeight = parseInt($(this).css('line-height')) + 2;
    //gets the height of the div        
    newh=$(this).height();
    if (typeof oldh === 'undefined')
        oldh=newh;//saves the current height
    if(oldh!=newh)//checks if the height changes
    {
        //if height changes, scroll up by 1 line (plus a little 2 px)            
        window.scrollBy(0,lineHeight);
        oldh=newh;//resave the saved height since it changed
    }
});

希望这能帮助到某个人。PITA

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接