UILabel에서 텍스트의 하위 문자열에 대한 CGRect를 어떻게 찾습니까?


78

주어진에 대해 NSRange, 그 글리프에 해당하는 CGRecta 를 찾고 싶습니다 . 예를 들어, "The quick brown fox jumps over the lazy dog"라는 문장에서 "dog"이라는 단어가 포함 된를 찾고 싶습니다 .UILabelNSRangeCGRect

문제에 대한 시각적 설명

트릭은에 UILabel여러 줄이 있고 텍스트가 실제로 attributedText이므로 문자열의 정확한 위치를 찾는 것이 약간 어렵습니다.

UILabel하위 클래스 에 작성하려는 메서드는 다음과 같습니다.

 - (CGRect)rectForSubstringWithRange:(NSRange)range;

관심있는 분들을위한 세부 정보 :

이것에 대한 나의 목표는 UILabel의 정확한 모양과 위치를 가진 새로운 UILabel만들어 애니메이션화 할 수있는 것입니다. 나머지는 알아 냈지만,이 단계가 특히 저를 방해하고 있습니다.

지금까지 문제를 해결하기 위해 내가 한 일 :

  • 나는 아이폰 OS 7이 문제를 해결할 텍스트 키트의 비트가있을 거라고 기대했던,하지만 대부분은 내가 텍스트 키트 본 적이 모든 예제에 초점을 맞추고 UITextViewUITextField보다는 UILabel.
  • 여기서 문제를 해결하겠다고 약속하는 Stack Overflow에 대한 또 다른 질문을 보았습니다. 그러나 허용 된 답변은 2 년이 넘었으며 코드가 속성 텍스트로 잘 작동하지 않습니다.

이에 대한 정답에는 다음 중 하나가 포함됩니다.

  • 표준 텍스트 키트 방법을 사용하여 한 줄의 코드로이 문제를 해결합니다. 나는 그것이 관련 NSLayoutManager되고textContainerForGlyphAtIndex:effectiveRange
  • UILabel을 여러 줄로 나누는 복잡한 메서드를 작성하고 Core Text 메서드를 사용하여 한 줄 내에서 글리프의 사각형을 찾습니다. 내 현재 최선의 방법은 @mattt의 우수한 TTTAttributedLabel 을 분리 하는 것입니다.이 방법은 한 지점에서 글리프를 찾는 방법을 가지고 있습니다.

업데이트 : 다음은이 문제를 해결하기 위해 지금까지 시도한 세 가지 사항에 대한 github 요점입니다 : https://gist.github.com/bryanjclark/7036101


젠장. nslayoutmanager가 uilabel에 노출되지 않는 이유는 무엇입니까? 같은 문제를 해결하려고합니다. 해결책을 찾았습니까?
Bob Spryn 2013 년

아직이 문제로 돌아 오지는 않았지만이 시점에서 약 6 번의 다른 시도를 한 후 아래의 Joshua의 권장 사항과 유사한 것을 구현하는 것이 가장 좋습니다. 나는 내가 할 때 내가 알아 낸 것을 공유 할 것이다!
bryanjclark

1
100 % 필요한 것이 아닐 수도 있지만 github.com/SebastienThiebaud/STTweetLabel을 확인하십시오 . 그는 저장 및 계산을 위해 textview를 활용합니다. 참조로 필요한 것을 추가하는 것은 매우 간단해야합니다.
Bob Spryn 2013 년

고마워, 밥! 한 번에 STTweetLabel을 사용하는 것을 고려했지만 대신 TTTAttributedLabel에 고유 한 사용자 지정 변형을 생성했습니다. STTweetLabel을 살펴보고 문제가 어떻게 해결되었는지 살펴 보겠습니다. 올바른 방향으로 진행되고있는 것 같습니다.
bryanjclark 2013-10-29

1
Luke의 코드는 거의 항상 작동합니다. 그러나 때때로 반환 된 rect는 0입니다. 잘못된 결과를 해결하는 데 오랜 시간이 지난 후 UITextView를 사용하고 UITextView의 textContainer 및 layoutManager를 사용하는 것이 더 낫다는 것을 알았습니다. 결과가 더 귀중합니다. UILabel의 내부 textContainer와 Luke의 코드에 의해 생성 된 것과 약간의 차이가있을 수 있습니다.
댄 리

답변:


92

다음 여호수아의 대답은 코드에서, 나는 직장에 잘 보인다 다음과 함께했다 :

- (CGRect)boundingRectForCharacterRange:(NSRange)range
{
    NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:[self attributedText]];
    NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
    [textStorage addLayoutManager:layoutManager];
    NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:[self bounds].size];
    textContainer.lineFragmentPadding = 0;
    [layoutManager addTextContainer:textContainer];

    NSRange glyphRange;

    // Convert the range for glyphs.
    [layoutManager characterRangeForGlyphRange:range actualGlyphRange:&glyphRange];

    return [layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:textContainer];
}

9
아주 멋지다! 그러나 사람들은 글꼴이 현재보기 크기에 맞게 자동으로 축소 된 경우 제대로 작동하지 않는다는 것을 알고 있어야합니다. 그에 따라 글꼴 크기를 조정해야합니다.
arsenius 2014 년

이 코드는 iOS7에서 잘 작동합니다. 누구든지 iOS6에서 시도 했습니까? 내 코드에서 로그 [__NSCFType _isDefaultFace] : 인식 할 수없는 선택기가 인스턴스로 전송되면서 iOS6에서 충돌이 발생합니다. 누군가 이것에 대해 어떤 생각을 가지고 있습니까?
Richa 2014

3
좋은 대답입니다. UILabels에는 왼쪽 여백이 없으므로 textContainer.lineFragmentPadding = 0을 추가해야합니다.
colinbrash

나는 이것을 사용하고 있으며 훌륭하게 작동하지만 내가 통과하는 모든 범위에서 작동하지는 않습니다. 자세한 내용은 여기 내 SO 게시물을 참조하십시오.
Stephen

2
훌륭하지만 솔루션은 attributeText의 정렬을 고려하지 않습니다.
Gabriel.Massana 2015 년

20

Luke Rogers의 답변을 바탕으로 작성되었지만 신속하게 작성되었습니다.

스위프트 2

extension UILabel {
    func boundingRectForCharacterRange(_ range: NSRange) -> CGRect? {

        guard let attributedText = attributedText else { return nil }

        let textStorage = NSTextStorage(attributedString: attributedText)
        let layoutManager = NSLayoutManager()

        textStorage.addLayoutManager(layoutManager)

        let textContainer = NSTextContainer(size: bounds.size)
        textContainer.lineFragmentPadding = 0.0

        layoutManager.addTextContainer(textContainer)

        var glyphRange = NSRange()

        // Convert the range for glyphs.
        layoutManager.characterRangeForGlyphRange(range, actualGlyphRange: &glyphRange)

        return layoutManager.boundingRectForGlyphRange(glyphRange, inTextContainer: textContainer)        
    }
}

사용 예 (Swift 2)

let label = UILabel()
let text = "aa bb cc"
label.attributedText = NSAttributedString(string: text)
let sublayer = CALayer()
sublayer.borderWidth = 1
sublayer.frame = label.boundingRectForCharacterRange(NSRange(text.range(of: "bb")!, in: text))
label.layer.addSublayer(sublayer)

스위프트 3/4

extension UILabel {
    func boundingRect(forCharacterRange range: NSRange) -> CGRect? {

        guard let attributedText = attributedText else { return nil }

        let textStorage = NSTextStorage(attributedString: attributedText)
        let layoutManager = NSLayoutManager()

        textStorage.addLayoutManager(layoutManager)

        let textContainer = NSTextContainer(size: bounds.size)
        textContainer.lineFragmentPadding = 0.0

        layoutManager.addTextContainer(textContainer)

        var glyphRange = NSRange()

        // Convert the range for glyphs.
        layoutManager.characterRange(forGlyphRange: range, actualGlyphRange: &glyphRange)

        return layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)       
    }
}

사용 예 (Swift 3/4)

let label = UILabel()
let text = "aa bb cc"
label.attributedText = NSAttributedString(string: text)
let sublayer = CALayer()
sublayer.borderWidth = 1
sublayer.frame = label.boundingRect(forCharacterRange: NSRange(text.range(of: "bb")!, in: text))
label.layer.addSublayer(sublayer)

1
:이와 충돌을 해결var glyphRange = NSRange() layoutManager.characterRangeForGlyphRange(range, actualGlyphRange: &glyphRange)
소니아 카사스

1
@Nemanja 줄 사이의 범위를 어떻게 끊습니까? 나는 이것이 imgur.com/a/P9RzR 과 같기를 원 하지만 텍스트가 거의 끝날 때 작동하지 않습니다.
demiculus

4
그것은 키 NSAttributedStringKey.paragraphStyle 및 NSAttributedStringKey.font이 기능은 텍스트 정렬 및 글꼴을 설명 할 수 있도록 설정에 대한 귀하의 UILabel의의 기인 텍스트 속성을 가져야합니다 점에 유의하는 것이 중요합니다
Zigglzworth

3
@Hemang 예제 사용 :let t = "aa bb cc"; label.attributedText = NSAttributedString(string: t); let l = CALayer(); l.borderWidth = 1; l.frame = label.boundingRectForCharacterRange(NSRange(t.range(of: "bb")!, in: t)); label.layer.addSublayer(l);
Cœur

1
중앙에있는 텍스트에서 작동하지 않는 경우 레이블의 정렬을 사용하지 말고 속성 문자열을 중앙에 ...
Ixx

13

내 제안은 Text Kit를 사용하는 것입니다. 불행히도 우리는 a가 UILabel사용 하는 레이아웃 관리자에 액세스 할 수 없지만 복제본을 만들어 범위에 대한 rect를 가져 오는 데 사용할 수 있습니다.

내 제안은 NSTextStorage라벨에있는 것과 똑같은 속성 텍스트를 포함 하는 개체 를 만드는 것입니다. 다음으로을 생성하고 NSLayoutManager이를 텍스트 스토리지 객체에 추가합니다. 마지막으로 NSTextContainer레이블과 동일한 크기로를 만들고 레이아웃 관리자에 추가합니다.

이제 텍스트 저장소는 레이블과 동일한 텍스트를 가지며 텍스트 컨테이너는 레이블과 동일한 크기이므로 .NET을 사용하여 범위에 대한 사각형을 생성 한 레이아웃 관리자에게 요청할 수 있습니다 boundingRectForGlyphRange:inTextContainer:. 먼저 glyphRangeForCharacterRange:actualCharacterRange:레이아웃 관리자 개체를 사용하여 문자 범위를 글리프 범위로 변환해야 합니다.

레이블 내에서 지정한 범위 의 경계 CGRect 를 제공해야하는 모든 것이 잘 진행 됩니다.

나는 이것을 테스트하지 않았지만 이것이 나의 접근 방식이며 UILabel그 자체가 어떻게 작동 하는지 모방함으로써 성공할 가능성이 높습니다.


좋은 내기처럼 들립니다. 한 번 시도하고 나중에 다시보고하겠습니다! 문제가 해결되면 GitHub에서 저를 위해 작동하는 코드를 제공하겠습니다. 감사!
bryanjclark 2013-10-28

@bryanjclark 이것에 행운이 있었나요?
Joshua

정확히 작동했지만 완벽하지는 않습니다. 약간의 오프셋이있어서 터치 된 단어를 올바르게 감지하지 못하며 오른쪽에있는 단어를 터치 할 때 작동합니다 ...
Andres

5

신속한 4 솔루션, 여러 줄 문자열에서도 작동하며 경계가 intrinsicContentSize로 대체되었습니다.

extension UILabel {
func boundingRectForCharacterRange(range: NSRange) -> CGRect? {

    guard let attributedText = attributedText else { return nil }

    let textStorage = NSTextStorage(attributedString: attributedText)
    let layoutManager = NSLayoutManager()

    textStorage.addLayoutManager(layoutManager)

    let textContainer = NSTextContainer(size: intrinsicContentSize)

    textContainer.lineFragmentPadding = 0.0

    layoutManager.addTextContainer(textContainer)

    var glyphRange = NSRange()

    layoutManager.characterRange(forGlyphRange: range, actualGlyphRange: &glyphRange)

    return layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
}
}

불행히도 이것은 단순히 작동하지 않습니다 : / 단지 intrinsicContentSize (실제 글리프 모양이 아닌 "전체 인쇄 크기")를 제공합니다. 결과는 여기에서 볼 수 있습니다. stackoverflow.com/questions/56935898
Fattie

1

대신 UITextView를 기반으로 클래스를 만들 수 있습니까? 그렇다면 UiTextInput 프로토콜 메서드를 확인하십시오. 특히 지오메트리 및 히트 휴지 방법을 참조하십시오.


불행히도 나는 그것이 여기서 실용적인 선택이 될 것이라고 생각하지 않습니다. 내 UILabel은 TTTAttributedLabel의 사용자 지정 재 작성이며 가능하면이 하나의 메서드를 만들기 위해 엄청난 양의 코드를 다시 작성해야하는 것을 싫어합니다. 그래도 고맙다!
bryanjclark

@bryanjclark 당신은 해결책을 알아 낸 적이 있습니까?
Jenel Ejercito Myers

1

정답은 실제로 할 수 없다는 것입니다. 여기에서 재생성하려는 대부분의 솔루션 은 TextKit / CoreText 프레임 워크를 사용하여 텍스트를 렌더링합니다. 이전 버전에서는 실제로 WebKit을 내부적으로 사용했습니다. 미래에 무엇을 사용할지 누가 알겠습니까? TextKit으로 재현하는 것은 지금도 분명한 문제가 있습니다 (미래 보장 솔루션에 대해 이야기하지 않음). 우리는 텍스트 레이아웃과 렌더링이 실제로 어떻게 수행되는지, 즉 어떤 매개 변수가 사용되는지 잘 알지 못합니다 (또는 보장 할 수 없습니다). 언급 된 모든 경우를 살펴보십시오. 일부 솔루션은 테스트 한 간단한 경우에 대해 '우연히'작동 할 수 있습니다. 이는 현재 iOS 버전 내에서도 아무것도 보장하지 않습니다. 텍스트 렌더링이 어렵습니다. RTL 지원, 동적 유형, 이모티콘 등을 시작하지 마십시오.UILabel 은 다양한 정밀도로 내부 동작 . 최근 iOS 버전에서는UILabel

적절한 솔루션이 필요하면 UILabel. 이는 단순한 구성 요소이며 TextKit 내부가 API에 노출되지 않는다는 사실은 Apple이 개발자가이 구현 세부 사항에 의존하는 솔루션을 빌드하는 것을 원하지 않는다는 분명한 표시입니다.

또는 UITextView를 사용할 수 있습니다. 그것은 편리하게 TextKit 내부를 노출하고 매우 구성 가능하므로 거의 모든 것을 얻을 수 있습니다 UILabel.

더 낮은 수준으로 이동하여 TextKit을 직접 사용할 수도 있습니다 (또는 CoreText의 경우 더 낮음). 실제로 텍스트 위치를 찾아야하는 경우 이러한 프레임 워크에서 사용할 수있는 다른 강력한 도구가 곧 필요할 수 있습니다. 처음에는 사용하기가 다소 어렵지만 대부분의 복잡성은 실제로 텍스트 레이아웃과 렌더링이 작동하는 방식을 배우는 데서 오는 것 같습니다. 이는 매우 관련성이 높고 유용한 기술입니다. Apple의 우수한 텍스트 프레임 워크에 익숙해지면 텍스트로 훨씬 더 많은 일을 할 수 있다는 느낌이들 것입니다.


0

plain text확장 프로그램을 찾는 분들에게 !

extension UILabel {    
    func boundingRectForCharacterRange(range: NSRange) -> CGRect? {
        guard let text = text else { return nil }

        let textStorage = NSTextStorage.init(string: text)
        let layoutManager = NSLayoutManager()

        textStorage.addLayoutManager(layoutManager)

        let textContainer = NSTextContainer(size: bounds.size)
        textContainer.lineFragmentPadding = 0.0

        layoutManager.addTextContainer(textContainer)

        var glyphRange = NSRange()

        // Convert the range for glyphs.
        layoutManager.characterRange(forGlyphRange: range, actualGlyphRange: &glyphRange)

        return layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
    }
}

PS 업데이트 된 Noodle of Death`s 답변 .


.attributedText는 .text와 호환됩니다. 동일한 CGRect를 얻을 수 있습니다.
Cœur

불행히도 이것은 단순히 작동하지 않습니다. 여기에서 출력을 볼 수 있습니다. stackoverflow.com/questions/56935898
Fattie

-1

자동 글꼴 크기 조정이 활성화 된 경우이를 수행하는 또 다른 방법은 다음과 같습니다.

let stringLength: Int = countElements(self.attributedText!.string)
let substring = (self.attributedText!.string as NSString).substringWithRange(substringRange)

//First, confirm that the range is within the size of the attributed label
if (substringRange.location + substringRange.length  > stringLength)
{
    return CGRectZero
}

//Second, get the rect of the label as a whole.
let textRect: CGRect = self.textRectForBounds(self.bounds, limitedToNumberOfLines: self.numberOfLines)

let path: CGMutablePathRef = CGPathCreateMutable()
CGPathAddRect(path, nil, textRect)
let framesetter = CTFramesetterCreateWithAttributedString(self.attributedText)
let tempFrame: CTFrameRef = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, stringLength), path, nil)
if (CFArrayGetCount(CTFrameGetLines(tempFrame)) == 0)
{
    return CGRectZero
}

let lines: CFArrayRef = CTFrameGetLines(tempFrame)
let numberOfLines: Int = self.numberOfLines > 0 ? min(self.numberOfLines, CFArrayGetCount(lines)) :  CFArrayGetCount(lines)
if (numberOfLines == 0)
{
    return CGRectZero
}

var returnRect: CGRect = CGRectZero

let nsLinesArray: NSArray = CTFrameGetLines(tempFrame) // Use NSArray to bridge to Array
let ctLinesArray = nsLinesArray as Array
var lineOriginsArray = [CGPoint](count:ctLinesArray.count, repeatedValue: CGPointZero)

CTFrameGetLineOrigins(tempFrame, CFRangeMake(0, numberOfLines), &lineOriginsArray)

for (var lineIndex: CFIndex = 0; lineIndex < numberOfLines; lineIndex++)
{
    let lineOrigin: CGPoint = lineOriginsArray[lineIndex]
    let line: CTLineRef = unsafeBitCast(CFArrayGetValueAtIndex(lines, lineIndex), CTLineRef.self) //CFArrayGetValueAtIndex(lines, lineIndex) 
    let lineRange: CFRange = CTLineGetStringRange(line)

    if ((lineRange.location <= substringRange.location) && (lineRange.location + lineRange.length >= substringRange.location + substringRange.length))
    {
        var charIndex: CFIndex = substringRange.location - lineRange.location; // That's the relative location of the line
        var secondary: CGFloat = 0.0
        let xOffset: CGFloat = CTLineGetOffsetForStringIndex(line, charIndex, &secondary);

        // Get bounding information of line

        var ascent: CGFloat = 0.0
        var descent: CGFloat = 0.0
        var leading: CGFloat = 0.0

        let width: Double = CTLineGetTypographicBounds(line, &ascent, &descent, &leading)
        let yMin: CGFloat = floor(lineOrigin.y - descent);
        let yMax: CGFloat = ceil(lineOrigin.y + ascent);

        let yOffset: CGFloat = ((yMax - yMin) * CGFloat(lineIndex))

        returnRect = (substring as NSString).boundingRect(with: CGSize(width: Double.greatestFiniteMagnitude, height: Double.greatestFiniteMagnitude), options: .usesLineFragmentOrigin, attributes: [.font: self.font ?? UIFont.systemFont(ofSize: 1)], context: nil)
        returnRect.origin.x = xOffset + self.frame.origin.x
        returnRect.origin.y = yOffset + self.frame.origin.y + ((self.frame.size.height - textRect.size.height) / 2)

        break
    }
}

return returnRect

1
"CalculationHelper"는 무엇입니까?
Carmelo Gallo

그것은 내가 문자열 rect를 계산하기 위해 쓴 클래스
일뿐입니다

2
네, 수업이라는 것을 압니다. :) 공유해 주시겠습니까? 그렇지 않으면 코드가 불완전)
멜로 갈로

2
boundingRectWithSize :를 의미하지는 않았지만 CalculationHelper 클래스를 공유 할 수 있다면. 어쨌든, 나는 나 자신에 의해 알아 냈어요)
멜로 갈로

boundingRectWithSize에 대한 @freshking 링크는 이제 developer.apple.com/documentation/foundation/nsstring/… 입니다. 불완전한 코드를 피하기 위해 직접 답변을 포함했습니다.
Cœur
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.