Categories
Blog

Converting a project to SwiftUI

I think the best way to get more knowledge about an API is to do a real project with it. So to improve in SwiftUI, I decided to convert one of my hobby projects, FontMetrics, to use it.

FontMetrics is a straightforward application that lists all the fonts available on iOS and visualizes the typography metrics of each one.

The project was all implemented using UIKit with the help of Storyboards. It consisted of a master-detail view, wherein the master screen you select the font and then see the details metrics of the font. On the detail screen, there was a custom UITextField class that shows lines for each metric overlay on top of the text.

I started by converting the Master-Detail structure from a UINavigationViewController/UISplitViewController to a NavigationView. It was easy to do the parallel between UINavigationController with NavigationView, but only after a while, I understood that it also replaced UISplitViewController

If you write this:

NavigationView {
  List {
    Text("Master")
    NavigationLink("Detail", DetailView())
  }
}

On the iPhone, it will use a standard navigation view stack, but on the iPad, it will use a split view.

So this brought me took me to another question, how to define the default detail view? You add another top element to your NavigationView like this:

NavigationView {
  List {
    Text("Master")
    NavigationLink("Detail", DetailView())
  }
  DetailView() // this will be the default view
}

So that second top element will be your default detail view. If you always want to have stack navigation, you can overwrite the defaults by setting the .navigationViewStyle modifier to StackNavigationViewStyle

Moving on to the list of fonts, in UIKit, I was using a UITableViewController, in SwiftUI this maps neatly to a List view.

Here is the code:

List {
  ForEach(fontSource, id: \.self) { familyName in
    Section(header: Text(familyName)) {
      ForEach( self.fontFamilies[familyName]!, 
               id: \.self) 
        { fontName in
          NavigationLink(fontName, 
            destination: FontDetailView(
                  fontName: fontName)
              .navigationBarTitle(fontName))
        }
      }
    }
  }

Notice how much shorter is this code comparing to the UITableViewController implementation!

The only remaining thing to do on the list screen was to implement the search functionality. I found out that SwiftUI doesn’t provide a UISearchBar functionality like UIKit. One option was to wrap around the UISearchBar component using a UIViewRepresentable wrapper, but I decided to go with a custom approach:

List {
  if searchVisible {
    Section(header: Text("Search")) {
      TextField("Search", text: $searchQuery)
    }
  }
  ...
}.navigationBarItems(trailing: Button(action: {
        self.searchVisible.toggle()
      }, label: {
        Image(systemName: "magnifyingglass")
      }))

As you can see above, I added a search icon to the navigation bar that I use to toggle a text field. When visible, I use the text field to get the search word. It does not match the behaviour of a search bar entirely, but it’s good enough.

Now that we have the font list complete, let’s move for the detail screen. For the detail screen, I was also using an UITableViewController this time with a static number or rows. I could have used a List view for this element, but this time a Form view looked more appropriate.

Form {
  Section(header: Text("Preview")) {
    HStack {
      Spacer()
      FontMetricsTextView(placeholder: "Metrics", 
        text: content.wrappedValue)
      .font(font.projectedValue)
      Spacer()
    }
  }
  Section(header: Text("Size")) {
    VStack {
      Slider(value: size.projectedValue, in: 10...60, step: 1, onEditingChanged: { changing in
            self.font.wrappedValue = UIFont(name: self.fontName, size: CGFloat(self.size.wrappedValue))!
            })
      Text("\(Int(size.wrappedValue))pt")
    }
  }
  Section(header: Text("Metrics")) {
    FontMetricRowView(label: "Size:", value: font.wrappedValue.pointSize, color: .clear)
    ...
  }
}

Again look how small is this code comparing to an UITableViewController implementation.

The only surprise on this screen was that SwiftUI doesn’t offer any ready to use row types like UIKit, but it’s so easy to implement your custom rows, that it doesn’t have any impact. After all rows in SwiftUI can be any View so you can do this:

struct FontMetricRowView: View {
  let label: String
  let value: CGFloat
  let color: Color

  var body: some View {
    HStack {
      Image(systemName: "square.fill").foregroundColor(color)
      Text(label)
      Spacer()
      Text("\(value as NSNumber, formatter: Style.metricFormatter)")
        .font(Font.body.monospacedDigit())
    }
  }
}

That easy!

The only remaining work was to implement my custom UITextField to show the metrics. For this, I decided to “cheat” a bit and used a UIViewRepresentable to wrap around my custom UIKit view. Here is the code

struct FontMetricsTextView: UIViewRepresentable {

  var font: Binding<UIFont> = .constant(UIFont.systemFont(ofSize: 12))

  let placeholder: String

  let text: String

  func makeUIView(context: Context) -> FontMetricsView {
    let view = FontMetricsView()
    view.textAlignment = .center
    view.attributedPlaceholder = NSAttributedString(string: placeholder, attributes: [.font: font.wrappedValue])
    view.text = text
    return view
  }

  func updateUIView(_ uiView: FontMetricsView, context: Context) {
    uiView.font = font.wrappedValue
    uiView.attributedPlaceholder = NSAttributedString(string: placeholder, attributes: [.font: font.wrappedValue])
  }

  func font(_ value: Binding<UIFont>) -> FontMetricsTextView {
    var new = self
    new.font = value
    return new
  }
}

And with that, all the work was done! 

You can see the result of all the steps above on this PR of the FontMetrics repo in Github.

As you can see from the PR above, the end result reduces the number of lines of code by 400, and I think that the end code is simpler and easier to understand and maintain.

These are still early days for SwiftUI, and this application is very straightforward, but it looks very promising!

By Sérgio Estêvão

Hello my name is Sérgio and I’m an experienced Mobile Software Architect living in Edinburgh.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s