강의를 듣다가 NSAttributedString의 존재를 알게 되었다
(이 강의 개인적으로 추천. 단, Rx와 MVVM에 대한 이해가 있어야 함)
문자열의 일부분에 다른 스타일을 적용할 수 있는 String이라고 생각하면 된다
두가지가 있는데
Attribute(스타일)를 객체 생성 후에 추가 할 수 있는지 아닌지에 따라 선택하면 된다
NSMutableAttributedString(string:attributes:)로 객체 생성을 한 후
해당 객체에 addAttributes(_ attrs:range:)를 통해 다른 스타일과 그 스타일을 적용할 범위를 지정하면 된다
그 후 UITextView 등 text를 표시할 수 있는 View의 attributedText로 할당해주면 완료
import UIKit
let sampleString = "문장의 단어를 강조해 봅시다"
class ViewController: UIViewController {
private var textView: UITextView = {
let view = UITextView()
// 여기 주목 👹👹👹👹
let text = NSMutableAttributedString(
string: sampleString,
attributes: [.font : UIFont.systemFont(ofSize: 16)])
let loc = sampleString.index(of: "강조")!.distance(in: sampleString) // 문장 내에 있는 "강조" 문자열을 찾기
text.addAttributes([
.font: UIFont.systemFont(ofSize: 24, weight: .bold),
.foregroundColor: UIColor.systemRed
], range: NSMakeRange(loc, 2))
view.attributedText = text
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
layout()
}
private func layout() {
view.addSubview(textView)
textView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
textView.widthAnchor.constraint(equalTo: view.safeAreaLayoutGuide.widthAnchor, multiplier: 0.5),
textView.heightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.heightAnchor, multiplier: 0.5),
textView.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
textView.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor)
])
}
}
// indexing을 간결하게 하기 위해 추가
// from: https://stackoverflow.com/questions/32305891/index-of-a-substring-in-a-string-with-swift
extension StringProtocol {
func index<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> Index? {
range(of: string, options: options)?.lowerBound
}
func endIndex<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> Index? {
range(of: string, options: options)?.upperBound
}
func indices<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> [Index] {
ranges(of: string, options: options).map(\.lowerBound)
}
func ranges<S: StringProtocol>(of string: S, options: String.CompareOptions = []) -> [Range<Index>] {
var result: [Range<Index>] = []
var startIndex = self.startIndex
while startIndex < endIndex,
let range = self[startIndex...]
.range(of: string, options: options) {
result.append(range)
startIndex = range.lowerBound < range.upperBound ? range.upperBound :
index(range.lowerBound, offsetBy: 1, limitedBy: endIndex) ?? endIndex
}
return result
}
}
// from: https://stackoverflow.com/questions/34540185/how-to-convert-index-to-type-int-in-swift
extension StringProtocol {
func distance(of element: Element) -> Int? { firstIndex(of: element)?.distance(in: self) }
func distance<S: StringProtocol>(of string: S) -> Int? { range(of: string)?.lowerBound.distance(in: self) }
}
extension Collection {
func distance(to index: Index) -> Int { distance(from: startIndex, to: index) }
}
extension String.Index {
func distance<S: StringProtocol>(in string: S) -> Int { string.distance(to: self) }
}
https://developer.apple.com/documentation/foundation/nsattributedstring
https://developer.apple.com/documentation/foundation/nsmutableattributedstring
https://stackoverflow.com/questions/32305891/index-of-a-substring-in-a-string-with-swift
https://stackoverflow.com/questions/34540185/how-to-convert-index-to-type-int-in-swift
특정 문자열에 변화를 주고 싶을 때 몇번 사용해본 경험이 있는 것 같아 반가웠습니다ㅎㅎ
iOS 15이상부터는 AttributedString을 지원하더라구요! SwiftUI에서 NSAttributedString을 사용하기 위해 여러번 꼬아서 엮거나 텍스트를 각각으로 만들어 이어붙이거나 했던 것 같은데 이제 간편히 Text(AttributedString)만 넣어도 되서 편리해진 것 같아요.
또 앱 타겟버전에 따라 분기처리를 하면 너무 유용할 것 같아요! 😊