iyOmSd/Title: SwiftUI

[SwiftUI] Highlight Text만들기(일치하는 텍스트 강조 뷰)

냄수 2024. 6. 23. 16:51
반응형

이런 뷰를 만들어볼거에요

검색하는 기능이라던가

텍스트를 강조를 하고싶은 뷰에서 많이 사용하죠!

 

2가지 방법으로 만들어보려고해요

AttributedString과 Text의 조합으로 만들어 볼겁니다

 

어렵지않기때문에 코드로 같이 봐볼게요! 

 

방법1. Text의 조합으로 구현하기

핵심코드부터 먼저 봐볼게요

private var highlightingText: Text {
    guard !highlightString.isEmpty,
          let matchIndex = text.range(of: highlightString) else {
        return Text(text)
            .font(font)
            .foregroundColor(textColor)
    }
    
    let unmatchHeadString = text[text.startIndex..<matchIndex.lowerBound]
    let matchString = text[matchIndex.lowerBound..<matchIndex.upperBound]
    let unmatchTailString = text[matchIndex.upperBound..<text.endIndex]
    
    let unmatchText = Text(unmatchHeadString)
        .font(font)
        .foregroundColor(textColor)
    
    let matchText = Text(matchString)
        .font(highlightFont)
        .foregroundColor(highlightColor)
    
    let unmatchTailText = Text(unmatchTailString)
        .font(font)
        .foregroundColor(textColor)
    
    let result = unmatchText + matchText + unmatchTailText
    return result
}

매칭되는 text의 range를 구해서

string의 index를이용해서 Text를 만들어서 연결해서 반환해주는 함수에요

 

전체코드는 아래와같습니다

struct HighlightTextOnly: View {
    let text: String
    let textColor: Color
    let font: Font
    let highlightString: String
    let highlightColor: Color
    var highlightFont: Font?
    
    init(
        text: String,
        textColor: Color,
        font: Font,
        highlightString: String,
        highlightColor: Color,
        highlightFont: Font? = nil
    ) {
        self.text = text
        self.textColor = textColor
        self.font = font
        self.highlightString = highlightString
        self.highlightColor = highlightColor
        self.highlightFont = highlightFont
    }
    
    var body: some View {
        highlightingText
    }
    
    private var highlightingText: Text {
        guard !highlightString.isEmpty,
              let matchIndex = text.range(of: highlightString) else {
            return Text(text)
                .font(font)
                .foregroundColor(textColor)
        }
        
        let unmatchHeadString = text[text.startIndex..<matchIndex.lowerBound]
        let matchString = text[matchIndex.lowerBound..<matchIndex.upperBound]
        let unmatchTailString = text[matchIndex.upperBound..<text.endIndex]
        
        let unmatchText = Text(unmatchHeadString)
            .font(font)
            .foregroundColor(textColor)
        
        let matchText = Text(matchString)
            .font(highlightFont)
            .foregroundColor(highlightColor)
        
        let unmatchTailText = Text(unmatchTailString)
            .font(font)
            .foregroundColor(textColor)
        
        let result = unmatchText + matchText + unmatchTailText
        return result
    }
}

#Preview {
    HighlightTextOnly(
        text: "일반텍스트 사이에 있는 강조텍스트를 사용합니다",
        textColor: .black,
        font: .body,
        highlightString: "강조텍스트",
        highlightColor: .blue,
        highlightFont: .title2
    )
}

사용도 간단하죠!

 

이렇게 간편하게 사용할 수 있습니다!

하지만 한계점이 존재해요

중복되는 텍스트를 찾고싶을때

즉 "텍스트" 라는 단어를 찾고싶다라고한다면 

이렇게 모든 "텍스트"가아니라 

제일 처음에 매칭된 문자열만 찾아서 반환하기때문에

포함된 모든 단어를 찾기 어려운 문제가 있어요

 

 

방법2. AttributedString 사용 

private var highlightTextView: some View {
    var attributeString = AttributedString(text)
    attributeString.foregroundColor = textColor
    attributeString.font = font
    if let range = attributeString.range(of: highlightText) {
        attributeString[range].foregroundColor = highlightColor
        attributeString[range].font = highlightFont
    }
    return Text(attributeString)
}

해당 뷰를 만들기위한 코어쪽 코드만 봐볼게요 

AttributeString에 원본 텍스트와 기본 컬러, 폰트를 넣어주고

그뒤에 

range(of:) 메서드를 이용해서 매칭되는 텍스트의 Range를 얻어서

해당 범위에 해당하는 텍스트에대한 속성을 다시 재설정 해주는 작업이에요

 

전체코드는 아래와 같습니다

struct HighlightTextAttribute: View {
    let text: String
    let textColor: Color
    let font: Font
    let highlightText: String
    let highlightColor: Color
    var highlightFont: Font?
    
    init(
        text: String,
        textColor: Color,
        font: Font,
        highlightText: String,
        highlightColor: Color,
        highlightFont: Font? = nil
    ) {
        self.text = text
        self.textColor = textColor
        self.font = font
        self.highlightText = highlightText
        self.highlightColor = highlightColor
        self.highlightFont = highlightFont == nil ? font : highlightFont
    }
    
    var body: some View {
        highlightTextView
    }
    
    private var highlightTextView: some View {
        var attributeString = AttributedString(text)
        attributeString.foregroundColor = textColor
        attributeString.font = font
        if let range = attributeString.range(of: highlightText) {
            attributeString[range].foregroundColor = highlightColor
            attributeString[range].font = highlightFont
        }
        return Text(attributeString)
    }
}


#Preview {
    HighlightTextAttribute(
        text: "일반텍스트 사이에 있는 강조텍스트를 사용합니다",
        textColor: .black,
        font: .body,
        highlightText: "텍스트",
        highlightColor: .red,
        highlightFont: .title2
    )
}

코드는 방법1보다 짧고 이해하기쉽게 구현된것 같아요

하지만 이뷰도 마찬가지로 

처음 매칭되는 텍스트만 변경할 수 있는 단점은 동일하네요..!

 

AttributeString을 사용했을땐 다른 방법은 찾지못했지만

문장을 반복하며 돌면서

매칭되는 단어의 range를 따로 저장해서 적용하면 되지않을까 생각해봤습니다...!

 

 

 

이 문제를 해결하기위해서

방법1 에서 좀더 디벨롭 해보려고합니다

 

 

 

String를 이용하므로 

range(of: )대신에 ranges(of: )를 이용할 수 있어요!

따라서 매칭되는 모든 단어의 range를 받아올 수 있죠

 

range(of: "매칭됨")이라고 가정하면

range의 lowerBound은 3

range의 upperBound은 6

입니다! 이를참고해서

text.startIndex..<range.lowerBound -> 0~2(가나다)

는 일반텍스트 적용

 

range.lowerBound..<range.upperBound -> 3~5(매칭됨)

는 강조텍스트 적용

이런원리를 사용할거에요

 

코드는 아래와같이 변경될거에요

private var highlightingText: Text {
    guard !highlightString.isEmpty else {
        return unmatchText(text)
    }
    
    var resultText: Text = Text("")
    var headIndex: String.Index = text.startIndex
    text.ranges(of: highlightString).forEach { range in
        let unmatchString: String = String(text[headIndex..<range.lowerBound])
        let matchString: String = String(text[range.lowerBound..<range.upperBound])
        
        headIndex = range.upperBound
        resultText = resultText + unmatchText(unmatchString) + matchText(matchString)
    }
    // 나머지 뒷부분 이어붙임
    if headIndex < text.endIndex {
        let unmatchString: String = String(text[headIndex..<text.endIndex])
        resultText = resultText + unmatchText(unmatchString)
    }
    return resultText
}

 

resultText와 headIndex를 두고 반복문을 돌면서 업데이트 시키는 식이죠

그리고 끝나면 

마지막부분이 남아 있을수 있기때문에

있는지 검사하고 이어 붙여줍니다!

 

그렇게만들면! 

#Preview {
    HighlightTextAttribute(
        text: "일반텍스트 사이에 있는 강조텍스트를 사용합니다",
        textColor: .black,
        font: .body,
        highlightText: "텍스트",
        highlightColor: .red,
        highlightFont: .title2
    )
}

모든"텍스트"를 강조 할 수 있게 됬습니다 ~!

 

 

 

전체코드는 아래와같아요


struct HighlightTextOnly: View {
    let text: String
    let textColor: Color
    let font: Font
    let highlightString: String
    let highlightColor: Color
    var highlightFont: Font?
    
    init(
        text: String,
        textColor: Color,
        font: Font,
        highlightString: String,
        highlightColor: Color,
        highlightFont: Font? = nil
    ) {
        self.text = text
        self.textColor = textColor
        self.font = font
        self.highlightString = highlightString
        self.highlightColor = highlightColor
        self.highlightFont = highlightFont
    }
    
    var body: some View {
        highlightingText
    }
    
    private var highlightingText: Text {
        guard !highlightString.isEmpty else {
            return unmatchText(text)
        }
        
        var resultText: Text = Text("")
        var headIndex: String.Index = text.startIndex
        text.ranges(of: highlightString).forEach { range in
            let unmatchString: String = String(text[headIndex..<range.lowerBound])
            let matchString: String = String(text[range.lowerBound..<range.upperBound])
            
            headIndex = range.upperBound
            resultText = resultText + unmatchText(unmatchString) + matchText(matchString)
        }
        // 나머지 뒷부분 이어붙임
        if headIndex < text.endIndex {
            let unmatchString: String = String(text[headIndex..<text.endIndex])
            resultText = resultText + unmatchText(unmatchString)
        }
        return resultText
    }
    
    private func unmatchText(_ string: String) -> Text {
        Text(string)
            .font(font)
            .foregroundColor(textColor)
    }
    
    private func matchText(_ string: String) -> Text {
        Text(string)
            .font(highlightFont)
            .foregroundColor(highlightColor)
    }
}

#Preview {
    HighlightTextOnly(
        text: "일반텍스트 사이에 있는 강조텍스트를 사용합니다",
        textColor: .black,
        font: .body,
        highlightString: "텍스트",
        highlightColor: .blue,
        highlightFont: .title2
    )
}

 

 

반응형