lastminute.com logo

Technology

SwiftUI and the Text concatenations super powers

fabrizio_duroni
fabrizio duroni
marco_de_lucchi
marco de lucchi

Do you need a way to compose beautiful text with images and custom font like you are used with Attributed String. The Text component has everything we need to create some sort of 'attributed text' directly in SwiftUI. Let's go!!!


In some recent posts (have a look here and here) we talked about a beautiful project we created during our Friyay at lastminute.com: some widgets for our iOS apps. The second widget we developed shows to our users their booking’s information. We faced a challenge to display the information of hotels. Our mobile app designer Rafael de Sena Martinez asked us to display the hotel name and the hotel rating as if it was a single text, with the rating represented by a number of stars matching the rating itself.

The layout of the hotel widget
The layout of the hotel widget

From iOS 15 the Text component supports the new AttributedString from the Foundation framework as a parameter. But given that AttributedString are not always so easy to use, and we wanted a more “SwiftUI native” way to create our custom text, we wondered if there was another way to do our implementation.
Luckily we discovered that in SwiftUI the + is overloaded and does some incredible magic emoji-crystal_ball. It basically concatenates each Text content while keeping each own specific formatting emoji-scream. It’s like having AttributedString directly implemented in SwiftUI emoji-rocket.
After this discovery, we were ready to implement our own custom layout above. So lets no longer wait and jump into the implementation right now!!! emoji-rocket

Implementation

All our texts use a custom font called Ubuntu. So first we had to find a way to apply this custom font to all the Text views in our code in a smart way (without reapplying the entire font modifier every time).
The overloaded + operator we discussed in the introduction is targeted on Text instances. This means that:

  • if we create a custom modifier to apply the custom font, it should return a Text instance and not the opaque data type some View
  • all the standard SwiftUI modifier applied to the concatenated Texts should be the one that return again Text instance, not the opaque data type some View

This is why we decided to create an extension of Text that applies our custom font.

enum TextWeight: String {
  case normal = "Ubuntu-Regular"
  case bold = "Ubuntu-Bold"
}

extension Text {
  func ubuntu(
    size: Double = 14.0,
    color: Color = Color("TextColorGray"),
    weight: TextWeight = TextWeight.normal
  ) -> Text {
    self
      .font(Font.custom(weight.rawValue, size: size))
      .foregroundColor(color)
  }
}

Now we are ready to create our custom layout. To create it, we needed to create a new SwiftUI view that contains the name and the rating stars. We named it HotelNameWithStars. This new view receives as parameters:

  • the name of the hotel as a String
  • the rating of the hotel as an Int

The text is separated in two parts:

  • the name, a dark gray text with font size 14 and font weight bold
  • the rating stars, a yellow sequence of stars icons with font size 14

For the name, it was easy, we just create a Text instance that contains the hotel name and a space (to separate it from the stars). We put it in a function named formattedName.

  //... other code

  private func formattedName() -> Text {
    return Text("\(name) ").ubuntu(size: 14.0, weight: TextWeight.bold)
  }

  //... other code

For the stars, it was a little bit trickier. We needed to generate a text that contains a number of stars matching the rating. The star itself was a custom image in our bundle assets. We basically needed to “loop over the rating” and generate an instance of Textcontaining the yellow stars. Which is function that given a sequence of elements and a combining operation gives you a return value of a new type? Reduce emoji-heart. So what we did:

  • we created a Range data structure using the rating as upper bound
  • we applied the reduce to this range, combining the current accumulated stars as text with a new one, to which we applied the custom formatting described above.
  //... other code

  private func formattedStars() -> Text {
    return (0..<rating).reduce(Text("")) { toBeDisplayed, _ in
      toBeDisplayed + Text(Image("icon_star")).foregroundColor(Color.yellow).ubuntu(size: 14.0)
    }
  }

  //... other code

Now we were ready to combine all our Texts together. Obviously after combining multiple Texts, you can apply additional modifiers to the obtained text. These modifiers will be applied to the entire string content. In our case we needed to set the lineLimit to 3 and the lineSpacing. We also have an addition fixedSize modifier that we need to tell to the component where this will be used that the text should not be truncated vertical.
That’s it. Below, you can find the complete implementation.

fileprivate struct HotelNameWithStars: View {
  let name: String
  let rating: Int

  var body: some View {
    (formattedName() + formattedStars())
    .lineLimit(3)
    .lineSpacing(3)
    .fixedSize(horizontal: false, vertical: true)
  }

  private func formattedName() -> Text {
    return Text("\(name) ").ubuntu(size: 14.0, weight: TextWeight.bold)
  }

  private func formattedStars() -> Text {
    return (0..<rating).reduce(Text("")) { toBeDisplayed, _ in
      toBeDisplayed + Text(Image("icon_star")).foregroundColor(Color.yellow).ubuntu(size: 14.0)
    }
  }
}

Conclusion

We love SwiftUI emoji-heart. With every release, Apple is making the app developer life easier than ever emoji-rocket. Also with the new additions during WWDC23, SwiftData and Macro above all, developers will have some fun in the near future emoji-rocket.


About fabrizio duroni

fabrizio_duroni
Software Engineer

Fabrizio is a Software Developer with 15+ years of experience. He specialised in mobile application development, computer graphics and web development. He ❤️ computers 💻, music 🎸, tattoo, videogames 👾 and drawing ✏️. He is also the maintainer of this blog 👷‍.

About marco de lucchi

marco_de_lucchi
Software Engineer

Marco is a Mobile Software Developer at lastminute.com, having joined the App Team in 2019. He is passionate about Apple and iOS development 📱, always focused on creating exceptional user experiences. Fun fact: you can quiz him about SwiftUI or Taylor Swift!


Read next

React Universe 2024

React Universe 2024

fabrizio_duroni
fabrizio duroni
sam_campisi
sam campisi

Let's dive into the talks from React Universe 2024 that stood out to us the most and share the key insights we gained. From innovative debugging tools to cross-platform development strategies, we’ll walk you through what we found valuable and how it’s shaping our approach to React and React Native development. [...]

Tech Radar As a Collaboration Tool

Tech Radar As a Collaboration Tool

rabbani_kajamohideen
rabbani kajamohideen

A tech radar is a visual and strategic tool used by organizations to assess and communicate the status and future direction of various technologies, frameworks, tools, and platforms. [...]