UITextView firstRectForRange返回错误的帧

时间:2014-08-05 19:31:09

标签: ios objective-c

修改

简单的解决方案是将任何帧计算从viewDidLoad移动到viewDidAppear:


我很难让下面的代码正常工作

代码返回UITextView中给定NSRange的第一帧。

如果没有换行符,它会起作用,但是当我在UITextView中添加换行符时,我会遇到一些奇怪的行为。

@implementation UITextView (TextFrame)

- (CGRect)frameOfTextRange:(NSRange)range {
    UITextPosition *beginning = self.beginningOfDocument;
    UITextPosition *start = [self positionFromPosition:beginning offset:range.location];
    UITextPosition *end = [self positionFromPosition:start offset:range.length];
    UITextRange *textRange = [self textRangeFromPosition:start toPosition:end];
    CGRect rect = [self firstRectForRange:textRange];

    return [self convertRect:rect fromView:self.textInputView];
}

@end

@implementation DetailViewController

- (void)viewDidLoad 
{
    [super viewDidLoad];
   if (self.searchString) {
        CGRect rect = [self.textView frameOfTextRange:[self.textView.text rangeOfString:self.searchString]];
        ...
    }
}

7 个答案:

答案 0 :(得分:5)

我找到了以下解决方案。我使用firstRectForRange:,而不是使用只返回一个矩形的selectionRectsForRange:,然后遍历所有rects并使用CGRectUnion组合它们。

- (CGRect)frameOfTextRange:(NSRange)range
{
   UITextPosition *beginning = self.beginningOfDocument;
   UITextPosition *start = [self positionFromPosition: beginning offset: range.location];
   UITextPosition *end = [self positionFromPosition: start offset: range.length];

   UITextRange *textRange = [self textRangeFromPosition: start toPosition: end];

   CGRect  rect = CGRectZero;

   NSArray *selectionRects = [self selectionRectsForRange: textRange];

   for (UITextSelectionRect *selectionRect in selectionRects)
   {
      rect = CGRectUnion(rect, selectionRect.rect);
   }

   return rect;
}

答案 1 :(得分:3)

显然似乎firstRectForRange:无效viewDidLoad

将计算从viewDidLoad移至viewDidAppear: 解决了问题

旧代码

- (void)viewDidLoad 
{
    [super viewDidLoad];

    if (self.searchString) {
        CGRect rect = [self.textView frameOfTextRange:[self.textView.text rangeOfString:self.searchString]];
        [self.textView scrollRangeToVisible:[self.textView.text rangeOfString:self.searchString]];
        UIView *markerView = [[UIView alloc] initWithFrame:rect];
        markerView.backgroundColor = [UIColor colorWithRed:0.810 green:0.735 blue:0.000 alpha:0.660];
        [self.textView addSubview:markerView];
        _searchString = nil;
    }
}

新功能代码

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];

    if (self.searchString) {
        CGRect rect = [self.textView frameOfTextRange:[self.textView.text rangeOfString:self.searchString]];
        [self.textView scrollRangeToVisible:[self.textView.text rangeOfString:self.searchString]];
        UIView *markerView = [[UIView alloc] initWithFrame:rect];
        markerView.backgroundColor = [UIColor colorWithRed:0.810 green:0.735 blue:0.000 alpha:0.660];
        [self.textView addSubview:markerView];
        _searchString = nil;
    }
}

答案 2 :(得分:2)

对于我来说,旧项目中的换行符('\n')可以正常使用。

@implementation UITextView (TextFrame)

- (CGRect)frameOfTextRange:(NSRange)range
{
    UITextPosition *beginning = self.beginningOfDocument;

    UITextPosition *start = [self positionFromPosition:beginning offset:range.location];
    UITextPosition *end = [self positionFromPosition:start offset:range.length];
    UITextRange *textRange = [self textRangeFromPosition:start toPosition:end];
    CGRect rect = [self firstRectForRange:textRange];  

    return [self convertRect:rect fromView:self.textInputView];
}

@end

这是我的Swift翻译

extension UITextView {

    func frameOfTextRange(range:NSRange) -> CGRect {

        let beginning = self.beginningOfDocument;
        let start = self.positionFromPosition(beginning, offset: range.location)
        let end = self.positionFromPosition(start, offset: range.length)
        let textRange = self.textRangeFromPosition(start, toPosition: end)

        let rect = self.firstRectForRange(textRange)
        return self.convertRect(rect, fromView: self.textInputView)
    }
}

答案 3 :(得分:2)

您可以强制self.textView立即确保布局:

[self.textView.layoutManager ensureLayoutForTextContainer:self.textView.textContainer];

frameOfTextRange:来电之前放置它会给你正确的矩阵。

当然,您可以在frameOfTextRange:方法中调用它:

- (CGRect)frameOfTextRange:(NSRange)range {
    [self.layoutManager ensureLayoutForTextContainer:self.textContainer];
    ...

这样可以从任何地方安全地调用frameOfTextRange:方法。

答案 4 :(得分:1)

从nib或viewDidLoad:方法加载视图后立即调用

loadView。它的尺寸没有根据它放置的容器进行调整。

如果您正在寻找最佳位置,那么它似乎是-[UIViewController viewDidLayoutSubviews]方法:

- (void)viewDidLayoutSubviews
{
    if (self.searchString) 
    {
        CGRect rect = [self.textView frameOfTextRange:[self.textView.text rangeOfString:self.searchString]];
        [self.textView scrollRangeToVisible:[self.textView.text rangeOfString:self.searchString]];
        UIView *markerView = [[UIView alloc] initWithFrame:rect];
        markerView.backgroundColor = [UIColor colorWithRed:0.810 green:0.735 blue:0.000 alpha:0.660];
        [self.textView addSubview:markerView];
        _searchString = nil;
    }
}

您可能还想添加一个属性来保存'marketView',这样如果它再次布局(响应旋转或其他内容),可以重新定位它而不是添加新视图。

答案 5 :(得分:0)

viewDidLoad中的控制器视图具有任意的非确定帧,因为父视图控制器的可用大小,状态栏状态等尚未应用。

进行精确帧计算的最佳位置是viewWillAppear,即加载的视图已添加到父控制器/窗口视图层次结构且其frame是“确定的”。

答案 6 :(得分:0)

在 Swift 中对我有用的解决方案基于 Koen's solution,但有一个关键区别。我没有根据选定的范围计算 UITextRange,而是使用 selectedTextRange 属性。此属性已返回为所选文本范围计算的 UITextRange

Swift 5.3(经测试可能适用于较低版本):

import UIKit

extension UITextView {
    /**
     Returns selected text frame position
     */
    func frameOfSelectedTextRange() -> CGRect? {
        guard let selectedTextRange = self.selectedTextRange,
              !selectedTextRange.isEmpty else { return nil }
        let rects = self.selectionRects(for: selectedTextRange)
        
        let rect = rects.reduce(into: CGRect.zero) { (res: inout CGRect, next: UITextSelectionRect) in
            res = res.union(next.rect)
        }
        
        return rect
    }
}

注意 B.S's solution 使用 firstRectForRange 返回给我我所选文本的错误帧位置。