One day I was inspired by a small yet crucial UI component in the Yahoo Finance iOS app. This UI component represents stock price changes in real time. The most interesting thing in this component for me it’s the accurate color accents on some parts of a price that was changed. Let’s see a brief example of AAPL stock: TL;DR GitHub repository: . https://github.com/prokhorovxo/PriceView The plan I want to recreate this component in this article but with better animations and a different aesthetic. Before the start of the development process let’s shortly describe the plan: Defining a number formatting style Describe rules for color highlighting Create model for price representation Develop SwiftUI View Number formatter I will use the US format of prices that looks like this: $1,000.00 To obtain this price format, let's develop a proper NumberFormatter. Create a file called and put this code inside: NumberFormatter+.swift // NumberFormatter+.swift import Foundation extension NumberFormatter { static let usNumberFormatter: NumberFormatter = { let formatter = NumberFormatter() formatter.locale = .init(identifier: "en_US") formatter.generatesDecimalNumbers = true formatter.currencyCode = "USD" formatter.currencySymbol = "$" formatter.numberStyle = .currency formatter.usesGroupingSeparator = true formatter.decimalSeparator = "." formatter.groupingSeparator = "," formatter.minimumFractionDigits = 2 formatter.maximumFractionDigits = 2 return formatter }() } Highlighting rules The goal is to highlight a part of the price based on its change and color this part based on whether the price change is positive or negative. In the development process I prefer to go from the specific case to common, so let’s take $1,000.00 price for example: $1,000.00 → → $1,000.0 Changed by $0.01 1 $1,000.00 → → $1,000 Changed by $0.10 .10 $1,000.00 → → $1,00 Changed by $1.00 1.00 $1,000.00 → → $1,0 Changed by $10.00 10.00 $1,000.00 → → $1 Changed by $100.00 ,100.00 $1,000.00 → → $ Changed by $1,000.00 2,000.00 Model Before we dive into the code, let's take a closer look at the individual components that make up a price. In general, a price can be broken down into the following atoms: Currency symbol. In our case is "$" for US dollars. Digits: The numerical digits that represent the value of the price. These digits may be placed before or after decimal separator. Grouping separator: The symbol used to separate groups of digits in the price. In the United States, a comma (",") is used to separate groups of thousands. Decimal separator: The symbol used to separate the whole and fractional parts of the price. In the United States, a period (".") is used as the decimal separator. By breaking down a price into these atoms, we can more easily manipulate and format the price as needed. Let’s represent $1,000.00 atomic: $1,000.00 → $ 1 , 0 0 0 . 0 0 For every component, we need to know what type it is (currency symbol, integer/decimal number, grouping separator or decimal separator), its value, and whether we should highlight this component after the price changes. Let’s create our model: // PriceComponent.swift import Foundation enum PriceComponent: Identifiable { case currencySymbol(String) case number(id: String = UUID().uuidString, value: Int, isDecimal: Bool, isHighlighted: Bool = false) case groupingSeparator(id: String = UUID().uuidString, value: String, isHighlighted: Bool = false) case decimalSeparator(id: String = UUID().uuidString, value: String, isHighlighted: Bool = false) var id: String { switch self { case .currencySymbol(let value): return "currencySymbol.\(value)" case .number(let id, _, _, _), .groupingSeparator(let id, _, _), .decimalSeparator(let id, _, _): return id } } var stringValue: String { switch self { case .currencySymbol(let value): return value case .number(_, let value, _, _): return "\(value)" case .groupingSeparator(_, let value, _): return value case .decimalSeparator(_, let value, _): return value } } var isHighlighted: Bool { switch self { case .currencySymbol: return false case .number(_, _, _, let isHighlighted), .groupingSeparator(_, _, let isHighlighted), .decimalSeparator(_, _, let isHighlighted): return isHighlighted } } } And let’s write a convenient method for converting price to an array of s that we define. Double PriceComponent // PriceComponent.swift // ... // MARK: - Price components factory extension PriceComponent { static func createPriceComponents(from price: Double, usingFormatter numberFormatter: NumberFormatter) -> [PriceComponent] { // 1. Get formatted string from price let decimalPrice = NSDecimalNumber(decimal: Decimal(price)) guard let priceString = numberFormatter.string(from: decimalPrice) else { return [] } var result: [PriceComponent] = [] // 2. True if forEach iterate over decimal separator var isDecimalSeparatorPassed = false // 3. Iterate through each character and create price component for each one priceString.forEach { char in let stringChar = String(char) switch stringChar { case numberFormatter.currencySymbol: result.append(.currencySymbol(stringChar)) case numberFormatter.groupingSeparator: result.append(.groupingSeparator(value: stringChar)) case numberFormatter.decimalSeparator: result.append(.decimalSeparator(value: stringChar)) isDecimalSeparatorPassed = true default: guard char.isNumber, let value = Int(stringChar) else { break } result.append(.number(value: value, isDecimal: isDecimalSeparatorPassed)) } } return result } } Develop SwiftUI View It's time to begin working on the UI, so let's create a new file named : PriceView.swift // PriceView.swift import SwiftUI struct PriceView: View { // 1 @Binding var price: Double // 2 @State private var priceComponents: [PriceComponent] // 3 @State private var highlightedColor: Color = .primary // 4 init(price: Binding<Double>) { self._price = price self.priceComponents = PriceComponent.createPriceComponents(from: price.wrappedValue, usingFormatter: .usNumberFormatter) } // 5 var body: some View { HStack(alignment: .bottom, spacing: .zero) { ForEach(priceComponents, id: \.id) { switch $0 { case .currencySymbol(let value): Text( AttributedString( value, attributes: AttributeContainer( [ .font: UIFont.systemFont(ofSize: 50.0, weight: .bold), .baselineOffset: 15 ] ) ) ) case .number(_, let value, let isDecimal, let isHighlighted): if isDecimal { Text( AttributedString( "\(value)", attributes: AttributeContainer( [ .font: UIFont.systemFont(ofSize: 50.0, weight: .bold), .baselineOffset: 12 ] ) ) ) .foregroundColor(isHighlighted ? highlightedColor : .primary) } else { Text("\(value)") .font(.system(size: 100.0, weight: .bold)) .foregroundColor(isHighlighted ? highlightedColor : .primary) } case .groupingSeparator(_, let value, let isHighlighted): Text("\(value)") .font(.system(size: 100.0, weight: .bold)) .foregroundColor(isHighlighted ? highlightedColor : .primary) case .decimalSeparator(_, let value, let isHighlighted): Text( AttributedString( value, attributes: AttributeContainer( [ .font: UIFont.systemFont(ofSize: 50.0, weight: .bold), .baselineOffset: 12 ] ) ) ) .foregroundColor(isHighlighted ? highlightedColor : .primary) } } } } } struct PriceView_Preview: PreviewProvider { static var previews: some View { PriceView(price: .constant(159.23)) } } We will pass a price value to PriceView from outside Array of price components that represents current price atomically It is color of highlighting, this color will change after new price has setted Init PriceView with binding double and create initial price components PriceView is a horizontal stack of price components. As you can see, in ForEach, we iterate through each price component and handle it in a switch case statement. For each specific price component we create his own Text. In my implementation I would like to use small font size for currency symbol and decimals. You can use any font and any font size, but be careful with baselineOffset value Let’s look on the Preview: Animations already looks good; let's make it even better by adding animation. To animate price updates we have to: PriceView On every price update we need to create new price components and determine which components we should highlight Set new price components Let’s dive into code. Add onChange block to HStack: // PriceView.swift import SwiftUI struct PriceView: View { // ... var body: some View { HStack(alignment: .bottom, spacing: .zero) { ForEach(priceComponents, id: \.id) { // ... } } // 1 .onChange(of: price) { newPrice in // 2 let oldPrice = PriceComponent.createPrice(from: priceComponents, usingFormatter: .usNumberFormatter) // 3 let newPriceComponents = PriceComponent.createPriceComponents(oldPriceComponents: priceComponents, newPrice: newPrice, usingFormatter: .usNumberFormatter) // 4 priceComponents = newPriceComponents // 5 highlightedColor = newPrice == oldPrice ? .primary : (newPrice > oldPrice ? .green : .red) } } } struct PriceView_Preview: PreviewProvider { static var previews: some View { PriceView(price: .constant(159.23)) } } in which we will observe newPrice onChange modifier Convert the current array of PriceComponents back to the Double type; we need to retrieve the current price because the price in this block is already new. Later, we will add this method. Create new price components based on old price components and new price Set new price components to update UI Determine which color we need to use for highlight. It depends on rise or fall of price Ok, let’s add a few methods to PriceComponent: // PriceComponent.swift // ... // MARK: - Price components factory extension PriceComponent { // ... // 1 static func createPriceComponents(oldPriceComponents: [PriceComponent], newPrice: Double, usingFormatter numberFormatter: NumberFormatter) -> [PriceComponent] { let oldPrice = PriceComponent.createPrice(from: oldPriceComponents, usingFormatter: numberFormatter) guard let oldPriceString = numberFormatter.string(from: NSDecimalNumber(decimal: Decimal(oldPrice))), let newPriceString = numberFormatter.string(from: NSDecimalNumber(decimal: Decimal(newPrice))) else { return [] } let changedIndex = oldPriceString .enumerated() .first(where: { let index = String.Index(utf16Offset: $0.offset, in: newPriceString) return $0.element != newPriceString[index] })?.offset ?? newPriceString.count - 1 var result: [PriceComponent] = [] var isDecimalSeparatorPassed = false newPriceString.enumerated().forEach { i, char in let stringChar = String(char) switch stringChar { case numberFormatter.currencySymbol: result.append(.currencySymbol(stringChar)) case numberFormatter.groupingSeparator: let nextNumberIndex = i + 1 let isHighlighted = nextNumberIndex >= changedIndex result.append( .groupingSeparator(id: isHighlighted ? UUID().uuidString : oldPriceComponents[i].id, value: stringChar, isHighlighted: isHighlighted) ) case numberFormatter.decimalSeparator: let nextNumberIndex = i + 1 let isHighlighted = nextNumberIndex >= changedIndex result.append( .decimalSeparator(id: isHighlighted ? UUID().uuidString : oldPriceComponents[i].id, value: stringChar, isHighlighted: isHighlighted) ) isDecimalSeparatorPassed = true default: guard char.isNumber, let value = Int(stringChar) else { break } let isHighlighted = i >= changedIndex result.append( .number(id: isHighlighted ? UUID().uuidString : oldPriceComponents[i].id, value: value, isDecimal: isDecimalSeparatorPassed, isHighlighted: isHighlighted) ) } } return result } } // MARK: - Double factory extension PriceComponent { // 2 static func createPrice(from priceComponents: [PriceComponent], usingFormatter numberFormatter: NumberFormatter) -> Double { let priceString = priceComponents.map { $0.stringValue }.joined() return numberFormatter.number(from: priceString)?.doubleValue ?? .zero } } This method creates PriceComponent array for the new price, while considering the old price. Considering the old price is necessary to understand what part of the new price we should highlight. Convert PriceComponent array to Double Let’s add PriceView to parent view add look on the Preview: // ContentView.swift import SwiftUI struct ContentView: View { @State var price: Double = 159.95 private let incrementValue = 0.5 var body: some View { VStack(alignment: .center, spacing: 25.0) { Spacer() PriceView(price: $price) .clipped() Spacer() // 1 Button { price = Double.random(in: (price - incrementValue)...(price + incrementValue)) } label: { Text("Update price") .font(.system(size: 17.0, weight: .semibold)) .frame(height: 44.0) .frame(maxWidth: .infinity) } .buttonStyle(.borderedProminent) .tint(.accentColor) .cornerRadius(22.0) } .padding() } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } } Button, when clicked, generates a new price with a range of $1.0 It’s ok but we don’t have animations, let’s add them // PriceView.swift import SwiftUI struct PriceView: View { // ... var body: some View { HStack(alignment: .bottom, spacing: .zero) { ForEach(priceComponents, id: \.id) { // ... } } .onChange(of: price) { newPrice in // 1 withAnimation(.easeOut(duration: 0.25)) { let oldPrice = PriceComponent.createPrice(from: priceComponents, usingFormatter: .usNumberFormatter) let newPriceComponents = PriceComponent.createPriceComponents(oldPriceComponents: priceComponents, newPrice: newPrice, usingFormatter: .usNumberFormatter) priceComponents = newPriceComponents highlightedColor = newPrice == oldPrice ? .primary : (newPrice > oldPrice ? .green : .red) // 2 DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { withAnimation(.easeIn) { highlightedColor = .primary } } } } } } // ... If you set a new value in the block, it will be set using the default animation. withAnimation After animation is completed, reset text color And last touch is “countdown” animation, it’s something about SwiftUI magic. Let’s add .transition() // PriceView.swift import SwiftUI struct PriceView: View { // ... var body: some View { HStack(alignment: .bottom, spacing: .zero) { ForEach(priceComponents, id: \.id) { switch $0 { case .currencySymbol(let value): Text( AttributedString( value, attributes: AttributeContainer( [ .font: UIFont.systemFont(ofSize: 50.0, weight: .bold), .baselineOffset: 15 ] ) ) ) case .number(_, let value, let isDecimal, let isHighlighted): if isDecimal { Text( AttributedString( "\(value)", attributes: AttributeContainer( [ .font: UIFont.systemFont(ofSize: 50.0, weight: .bold), .baselineOffset: 12 ] ) ) ) .foregroundColor(isHighlighted ? highlightedColor : .primary) .transition(.push(from: .top)) // 1 } else { Text("\(value)") .font(.system(size: 100.0, weight: .bold)) .foregroundColor(isHighlighted ? highlightedColor : .primary) .transition(.push(from: .top)) } case .groupingSeparator(_, let value, let isHighlighted): Text("\(value)") .font(.system(size: 100.0, weight: .bold)) .foregroundColor(isHighlighted ? highlightedColor : .primary) .transition(.identity) // 2 case .decimalSeparator(_, let value, let isHighlighted): Text( AttributedString( value, attributes: AttributeContainer( [ .font: UIFont.systemFont(ofSize: 50.0, weight: .bold), .baselineOffset: 12 ] ) ) ) .foregroundColor(isHighlighted ? highlightedColor : .primary) .transition(.identity) } } } .onChange(of: price) { newPrice in // ... } } } // ... New values will be animated with a top-down motion I don't think it's necessary to animate grouping and decimal separators in any way. Conclusion In this tutorial, we've explored how to create a stock price label UI component using SwiftUI. We started by defining a number formatting style and highlighting rules for the price change. Then, we created a custom type to represent the individual components of a price, and used it to format the price in our UI component. Finally, we developed a SwiftUI view that displays the stock price and its change in a visually appealing way. PriceComponent By following this tutorial, you should now have a better understanding of how to create custom UI components in SwiftUI and format prices using the class. NumberFormatter You can find the full source code for this project on our GitHub repository: . https://github.com/prokhorovxo/PriceView Thank you for reading, and happy coding!