SwiftUI
SwiftUI Text에 stroke 적용하기 (feat. UIKit)
GREEN.1229
2025. 1. 2. 18:49
안녕하세요. 그린입니다 🍏
이번 포스팅에서는 SwiftUI Text에 stroke를 적용하는 두번째 방법에 대해 알아보겠습니다 🙋🏻
이전 첫번째 방법에 대해 기술한적이 있어요.
해당 방법에서는 SwiftUI Text 자체에 shadow를 적절히 주어 표현했었는데요.
한계가 있었습니다.
stroke 두께를 늘릴수록 shadow가 크게 적용되고 그에 따라 사실 상 aliasing, 계단 현상으로 디자인이 매끄럽지 못하게 나타나는 한계가 있었어요 🥲
그래서, 사실 SwiftUI만으로는 매끄럽게 구현하기가 어려워 이번 두번째 방법을 소개해보려 합니다!
SwiftUI Text에 stroke 적용하기 (UIKit을 곁들인)
이번 방법은 UIKit을 곁들여 SwiftUI에서 보다 매끄럽고 편하게 사용하는 방법입니다.
코드를 먼저 보면서 하나씩 살펴보겠습니다 😃
가장 크게 세가지 핵심적인 컴포넌트가 필요합니다.
1️⃣ StrokedLabel - UILabel을 통해 실제 외곽선 드로잉을 담당
2️⃣ StrokedText - UIKit의 StrokedLabel을 SwiftUI에서 사용할 수 있도록 래핑
2️⃣ StrokedText Extension - SwiftUI 스타일의 뷰 모디파이어 지원
그럼 하나씩 살펴볼까요?
StrokedLabel
public class StrokedLabel: UILabel {
var strokeWidth: CGFloat = 5.0 {
didSet { setNeedsDisplay() }
}
var strokeColor: UIColor = .black {
didSet { setNeedsDisplay() }
}
public override func drawText(in rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
context?.setLineWidth(strokeWidth)
context?.setLineJoin(.round)
// 외곽선 그리기
context?.setTextDrawingMode(.stroke)
textColor = strokeColor
super.drawText(in: rect)
// 외곽선 내부 채우기
context?.setTextDrawingMode(.fill)
textColor = strokeColor.withAlphaComponent(0)
super.drawText(in: rect)
// 실제 텍스트 그리기
textColor = .white
super.drawText(in: rect)
}
}
먼저 UILabel을 상속받아 외곽선을 그리는 커스텀한 UILabel을 만들어야 합니다.
drawText(in:) 메서드를 오버라이드하여 다음과 같이 세 단계로 텍스트를 그려줘요.
1️⃣ 외곽선 그리기 (.stroke 모드)
2️⃣ 외곽선 내부 채우기 (.fill 모드)
3️⃣ 실제 텍스트 그리기
이렇게 커스텀한 UILabel이 준비되었다면 SwiftUI에서 사용할 수 있도록 만듭니다.
StrokedText
public struct StrokedText: UIViewRepresentable {
let text: String
let strokeWidth: CGFloat
let strokeColor: UIColor
let foregroundColor: UIColor
let font: UIFont
var numberOfLines: Int
var kerning: CGFloat
var lineHeight: CGFloat?
var textAlignment: NSTextAlignment
// UIViewRepresentable 구현
public func makeUIView(context: Context) -> StrokedLabel {
let label = StrokedLabel()
updateUIView(label, context: context)
return label
}
public func updateUIView(_ label: StrokedLabel, context: Context) {
// 기본 속성 설정
label.strokeWidth = strokeWidth
label.strokeColor = strokeColor
label.font = font
// AttributedString 설정
let attributedString = NSMutableAttributedString(string: text)
let range = NSRange(location: 0, length: text.count)
var attributes: [NSAttributedString.Key: Any] = [
.kern: kerning,
.foregroundColor: foregroundColor,
.font: font
]
// 줄 높이 설정
if let lineHeight = lineHeight {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.minimumLineHeight = lineHeight
paragraphStyle.maximumLineHeight = lineHeight
paragraphStyle.alignment = textAlignment
paragraphStyle.lineBreakMode = .byTruncatingTail
attributes[.paragraphStyle] = paragraphStyle
}
attributedString.addAttributes(attributes, range: range)
label.attributedText = attributedString
label.numberOfLines = numberOfLines
label.textAlignment = textAlignment
}
}
UIViewRepresentable을 구현해줘요.
여기서 기본 속성이나 AttributedString을 설정하고 행간, 자간에 대해 설정해주고 표현해줍니다.
더 나아가서 SwiftUI에 올려서 사용할 때 라인 수나 행/자간 등 추가적인 속성을 뷰 모디파이어로 설정할 수 있도록 해당 StrokedText를 확장해 뷰 모디파이어를 구현합니다.
StrokedText Extension
public extension StrokedText {
func lineLimit(_ limit: Int?) -> StrokedText {
var view = self
view.numberOfLines = limit ?? 0
return view
}
func multilineTextAlignment(_ alignment: TextAlignment) -> StrokedText {
var view = self
switch alignment {
case .leading:
view.textAlignment = .left
case .trailing:
view.textAlignment = .right
case .center:
view.textAlignment = .center
}
return view
}
func kerning(_ value: CGFloat) -> StrokedText {
var view = self
view.kerning = value
return view
}
func lineHeight(_ value: CGFloat) -> StrokedText {
var view = self
view.lineHeight = value
return view
}
}
간단하죠?
그럼 SwiftUI 환경에서 사용해볼까요?
import SwiftUI
struct ContentView: View {
var body: some View {
StrokedText(
text: "Hello, Green!",
strokeWidth: 5,
strokeColor: .black,
foregroundColor: .white,
font: .systemFont(ofSize: 24, weight: .bold)
)
.lineLimit(1)
.multilineTextAlignment(.center)
.kerning(2)
}
}
이렇게 간단히 해당 커스텀 컴포넌트를 호출하여 뷰 모디파이어도 적용하고 사용할 수 있어요!
이렇게 입맛에 맞도록 stroke 색상이나 두께 그리고 text 색상들도 자유롭게 변경할 수 있습니다 😃
마무리
SwiftUI에서 이런 기본적인것을 제공해주면 얼마나 좋을까요? 🥲
점차 발전은 해가는것 같지만 정말 기초적인 이런 부분들의 제공은 아직 없는것 같아서 아쉽습니다.