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!
Leave a Reply