SwiftUI 텍스트 포맷 빠르게 알아보기
💫 Currency
struct CurrencyView: View {
private let 💰 = 100_000
var body: some View {
VStack {
Text(💰, format: .currency(code: "KRW"))
Text(💰, format: .currency(code: "USD"))
Text(💰, format: .currency(code: "GBP"))
}
}
}
code에는 ISO-4217 코드를 찾아서 입력하면 된다. 원화는 소숫점을 표시하지 않지만 달러나 파운드 스털링같은 경우에는 소숫점 둘째자리까지 표시해준다.
💫 Percent
struct PercentView: View {
@State private var opacityValue: Double = 0
private let maxValue: Double = 255
var body: some View {
VStack {
let opacity: Double = Double(opacityValue) / maxValue
Text("\(Int(opacityValue)) / \(Int(maxValue))")
Text(opacity, format: .percent)
Slider(value: $opacityValue, in: 0...maxValue, step: 1) {
Text("label")
}
}
}
}
퍼센트를 표시해주는 포맷터인데 소숫점 자리수를 지정할 수 없는 불편함이 있는것 같다.
💫 Measurement
struct MeasurementView: View {
var body: some View {
VStack {
let length = Measurement(value: 100, unit: UnitLength.miles)
Text(length, format: .measurement(width: .wide, usage: .road))
}
}
}
단위를 변환해 주는 포맷터
💫 Date
struct DateView: View {
var body: some View {
VStack {
let now = Date()
Text(now, format: Date.FormatStyle(date: .complete))
Text(now, format: Date.FormatStyle(date: .omitted))
Text(now, format: Date.FormatStyle(date: .long))
Text(now, format: Date.FormatStyle(date: .abbreviated))
Text(now, format: Date.FormatStyle(date: .numeric))
Divider()
Text(now, format: Date.FormatStyle(time: .complete))
Text(now, format: Date.FormatStyle(time: .omitted))
Text(now, format: Date.FormatStyle(time: .standard))
Text(now, format: Date.FormatStyle(time: .shortened))
}
}
}
날짜나 시간을 변환해 주는 포맷터
💫 List
struct ListView: View {
var body: some View {
VStack {
let items: [Int] = [100, 1000, 10000, 100000, 1000000]
Text(items, format: .list(memberStyle: .number, type: .and))
Text(items, format: .list(memberStyle: .number, type: .or))
}
}
}
배열을 문자열로 하나씩 나열할때 사용하는 포맷터로 그냥 마지막에 and/or 만 붙이는 것 같은 느낌,,,
💫 PersonName
struct PersonNameView: View {
let nameUS = PersonNameComponents(
namePrefix: "Dr.",
givenName: "Timothy",
middleName: "Donald",
familyName: "Cook",
nameSuffix: "Jr.",
nickname: "Tom"
)
let nameKR = PersonNameComponents(
namePrefix: "미스터",
givenName: "수한무",
middleName: "거북이와",
familyName: "김",
nameSuffix: "두루미",
nickname: "삼천갑자"
)
var body: some View {
HStack {
ForEach([nameUS, nameKR], id: \.self) { name in
VStack {
Text(name, format: .name(style: .long))
Text(name, format: .name(style: .medium))
Text(name, format: .name(style: .short))
Text(name, format: .name(style: .abbreviated))
}
}
}
}
}
PersonNameComponents로 생성한 이름을 변환해 주는 포맷터로 영문이름으로 생성했을 때와 한글이름으로 생성했을 때 abbreviated 스타일이나 long, medium 스타일에서 보여지는 순서가 다르다. medium 스타일의 경우 영문은 givenName familyName 순서로 보여지지만 성을 먼저 쓰는 한글은 familyName givenName 순서로 보여진다.
💫 Inflection
struct InflectionView: View {
@State private var count = 1
var body: some View {
VStack {
HStack {
Button("-") { count -= 1 }
.buttonStyle(.bordered)
Spacer()
Text("^[\(count) company](inflect: true)")
Spacer()
Button("+") { count += 1 }
.buttonStyle(.bordered)
}
HStack {
Button("-") { count -= 1 }
.buttonStyle(.bordered)
Spacer()
Text("^[\(count + 1) company](inflect: true)")
Spacer()
Button("+") { count += 1 }
.buttonStyle(.bordered)
}
}
}
}
포맷터가 맞나 싶지만 복수/단수를 표현해 주는 표현식이다. 1개일 때는 단수로 company가 표시되지만 2개 이상이 되면 companies로 바꿔준다. 이번에 알게된 사실로 0은 복수로 써야 된다는 사실..! (0 companies)
테스트 케이스
["knife", "roof", "woman", "goose", "child", "ox", "mouse", "person", "sheep", "fish", "datum", "data", "medium", "companies"]

여러 불규칙 복수형을 테스트 해봤을 때 대부분 정확하게 변환이 되었지만 황소를 뜻하는 ox의 복수형은 oxen이지만 ox가 그대로 보이고 있고 medium은 media를 기대했지만 mediums가 되었다. 그리고 company가 아닌 companies를 넣어줬을때는 단수형인 company가 되지 않으므로 항상 단수형을 넣어줘야 변환이 되는 것을 확인했다.
💫 Custom
👉 JSON 포맷터 만들기
struct Todo: Encodable, Equatable {
let title: String
let completed: Bool
}
struct TodoJSONFormat: FormatStyle {
func format(_ value: Todo) -> String {
let jsonEncoder = JSONEncoder()
jsonEncoder.outputFormatting = .prettyPrinted
let jsonData = try! jsonEncoder.encode(value)
let json = String(data: jsonData, encoding: .utf8)!
return json
}
}
extension FormatStyle where Self == TodoJSONFormat {
static var json: TodoJSONFormat { .init() }
}
struct JSONPrintView: View {
let todo = Todo(title: "산책 가기", completed: false)
var body: some View {
Text(todo, format: .json)
}
}
👉 소숫점 제한이 있는 Percent 포맷터 만들기
struct MaxFractionPercentFormat: FormatStyle {
let maxFractionLength: Int
func format(_ value: Double) -> String {
String(format: "%.\(maxFractionLength)f", value * 100) + "%"
}
}
extension FormatStyle where Self == MaxFractionPercentFormat {
static func percent(maxFractionLength: Int) -> MaxFractionPercentFormat {
.init(maxFractionLength: maxFractionLength)
}
}
struct MaxFractionPercentView: View {
@State private var opacityValue: Double = 0
private let maxValue: Double = 255
var body: some View {
VStack {
let opacity: CGFloat = Double(opacityValue) / maxValue
Text("\(Int(opacityValue)) / \(Int(maxValue))")
Text(opacity, format: .percent)
Text(opacity, format: .percent(maxFractionLength: 0))
Text(opacity, format: .percent(maxFractionLength: 2))
Text(opacity, format: .percent(maxFractionLength: 10))
Slider(value: $opacityValue, in: 0...maxValue, step: 1) {
Text("label")
}
}
}
}
👉 Padding 포맷터 만들기
struct PadStartFormat: FormatStyle {
let paddedLength: Int
let pad: String
let padStart: Int
func format(_ value: String) -> String {
let padded = value.padding(toLength: max(value.count, paddedLength), withPad: pad, startingAt: padStart)
return "".padding(toLength: paddedLength, withPad: padded, startingAt: value.count % paddedLength)
}
}
struct PadEndFormat: FormatStyle {
let paddedLength: Int
let pad: String
let padStart: Int
func format(_ value: String) -> String {
value.padding(toLength: paddedLength, withPad: pad, startingAt: padStart)
}
}
extension FormatStyle where Self == PadStartFormat {
static func padStart(
toLength paddedLength: Int,
withPad pad: String,
startingAt padStart: Int = 0
) -> PadStartFormat {
.init(paddedLength: paddedLength, pad: pad, padStart: padStart)
}
}
extension FormatStyle where Self == PadEndFormat {
static func padEnd(
toLength paddedLength: Int,
withPad pad: String,
startingAt padStart: Int = 0
) -> PadEndFormat {
.init(paddedLength: paddedLength, pad: pad, padStart: padStart)
}
}
struct PaddingView: View {
var body: some View {
VStack {
Text("123", format: .padStart(toLength: 5, withPad: "0"))
Text("123", format: .padEnd(toLength: 5, withPad: "_"))
}
}
}
👉 가격표시 포맷터 만들기
struct Product {
let title: String
let price: Decimal
let oldPrice: Decimal?
}
extension Product {
func formatted<Style: FormatStyle>(
_ style: Style
) -> Style.FormatOutput where Style.FormatInput == Self {
style.format(self)
}
}
struct ProductPriceFormat: FormatStyle {
func format(_ value: Product) -> AttributedString {
guard let oldPrice = value.oldPrice else {
return value.price.formatted(.number.attributed)
}
let priceFormatted = value.price.formatted()
let oldPriceFormatted = oldPrice.formatted()
var string = AttributedString("\(priceFormatted) \(oldPriceFormatted)")
if let range = string.range(of: oldPriceFormatted) {
string[range].inlinePresentationIntent = .strikethrough
}
return string
}
}
extension FormatStyle where Self == ProductPriceFormat {
static var price: ProductPriceFormat { .init() }
}
struct PriceView: View {
let product = Product(title: "iPhone", price: 1000, oldPrice: 1500)
var body: some View {
// Text(product, format: .price) // ❗️Initializer 'init(_:format:)' requires the types 'AttributedString' and 'String' be equivalent
Text(product.formatted(.price))
}
}
FormatStyle의 FormatOutput타입이 AttributedString이면 format을 사용한 Text생성자를 사용할 수 없고 formatted 함수를 추가하여 사용해야 한다.
참고:
https://www.createwithswift.com/formatting-data-as-text-in-a-text-view-in-swiftui/
https://swiftwithmajid.com/2023/07/04/mastering-swift-foundation-formatter-api-custom-format-styles/