How to format Date Components with AttributedString

In iOS 15, and the rest of the 2021 operating systems, Apple introduced a new way to format date components. Let's take a look:

struct WatchFace: View {
    @State var now = Date.now
        
    var dateString: AttributedString {
        var attrDate = now.formatted(.dateTime.hour(.twoDigits(amPM: .wide)).minute(.twoDigits).attributed)
        
        let hours = AttributeContainer.dateField(.hour)
        let hoursStyle = AttributeContainer.foregroundColor(.indigo)
        
        let minute = AttributeContainer.dateField(.minute)
        let minuteStyle = AttributeContainer.foregroundColor(.cyan)
        
        let amPM = AttributeContainer.dateField(.amPM)
        let amPMStyle = AttributeContainer.foregroundColor(.pink).font(.caption)
    
        attrDate.replaceAttributes(hours, with: hoursStyle)
        attrDate.replaceAttributes(minute, with: minuteStyle)
        attrDate.replaceAttributes(amPM, with: amPMStyle)

        return attrDate
    }
    
    var body: some View {
        Text(dateString)
            .multilineTextAlignment(.trailing)
            .font(.largeTitle)
            .foregroundColor(Color.white)
            
    }
}

The code above defines a simple WatchFace:

Now, when we have the big picture, let's dissect the code. First, we extract the time components from the current date using the  formatted() method (You can learn more about new formatting APIs from the How to format Dates, TimeIntervals, Numbers, and Lists in iOS 15 article). What's cool about this method, it can also output an Attributed String.

If you used Attributed Strings before, you know that you can format parts of those strings. To do this, you need to specify the range of the substring that needs to be formatted. Finding the range of the substring is the trickiest part, especially with the dates and times. Depends on the user's Locale, each  DateTime component can have a different format and position.

Fortunately, in iOS 15, Apple solved this problem for us. Now, we can specify individual components using the AttributeContainers. Those containers can hold attributes without being attached to any particular characters in a string. If we want to select any of the DateTime components, we can use the .dateField(_)  attribute. For example, this is how to select .amPM component:

let amPM = AttributeContainer.dateField(.amPM)

Similarly, we can use AttributeContainer to specify the component's style:

 let amPMStyle = AttributeContainer.foregroundColor(.pink).font(.caption)

Now, we can apply the style to the component using the replaceAttributes() method:

attrDate.replaceAttributes(amPM, with: amPMStyle)

That's it. As you can see, DateComponents formatting is now fairly straightforward. Although it covers most cases, it is not perfect. For example, we cannot use this API to format date or time separators. In this case, we have to do this manually. We have to find the range (position and length) of the separator and replace the style of this subrange using the replaceSubrange() method.