이런 뷰를 만들어볼거에요
검색하는 기능이라던가
텍스트를 강조를 하고싶은 뷰에서 많이 사용하죠!
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
)
}
'iyOmSd > Title: SwiftUI' 카테고리의 다른 글
[SwiftUI] WWDC24 Swift Charts: Vectorized and function plots (2) | 2024.11.19 |
---|---|
[SwiftUI] WWDC24 SwiftUI essentials 정리 (0) | 2024.11.08 |
[SwiftUI] NavigationStack 화면전환 방법 고민정리 (feat. Router 구현) (2) | 2024.04.26 |
[SwiftUI] ChipView(iOS16+, iOS16-) tag view 구현하기 (1) | 2024.03.13 |
[SwiftUI] WWDC23 Demystify SwiftUI Performance (0) | 2023.11.30 |