iOS/SwiftUI

SwiftUI 텍스트 포맷 빠르게 알아보기

이기회 2024. 6. 14. 11:20

💫 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/