Leave a rating/review
OK! We’re ready to start working on the image version that renders a “letter square”. We could make fancy letter graphics from scratch by combining text and shapes and things with SwiftUI, but SF Symbols already has some really nice options for us.
In the SF Symbols app, check out the “Indices” category and you should see that every letter of the alphabet has circle and square symbols. Notice the naming convention - the letter, lowercased, then a dot, then the shape.
Back in Xcode, to make the appropriate letter square dynamically based on a title, we’ll make a custom initializer for SwiftUI’s Image structure.
To start, extend SwiftUI’s Image structure.
Book.Image()
}
}
extension Image {
}
Make a new initializer that takes in a title String.
extension Image {
init(title: String) {
}
}
This initializer won’t always work, for a couple of reasons. First of all, the title might be empty! To represent that, add a question mark after the keyword init.
init?(title: String) {
This creates a failable initializer - it will either create an Image, or nil. Now, we can check to make sure we’ve got a character to work with, at the beginning of the title…
init?(title: String) {
guard let character = title.first else {
<#statements#>
}
}
Otherwise, return nil.
guard let character = title.first else {
return nil
}
If we’ve got a character, create a symbolName
String from it, appending “dot square” afterwards.
return nil
}
let symbolName = "\(character).square"
}
Then initialize the Image with that system name.
let symbolName = "\(character).square"
self.init(systemName: symbolName)
}
This is great! But it won’t always work, and when it doesn’t work it won’t return nil. With a SwiftUI Image, if you use a string that doesn’t map to an SF Symbol, you just won’t get an Image. This is our next point of potential failure.
The SF symbols we want all require a lowercase letter, but most titles start with a capital letter, and that would put us in a situation where the Image made from the title is not nil, but it doesn’t render anything, either.
Easy fix. Call lowercased
on the character.
let symbolName = "\(character.lowercased()).square"
Cool! That will handle most titles. I’ll show you one more edge case in a minute, but let’s get the preview working, first, and use this custom Image initializer above in Book’s Image view.
First, a Book Image is going to need a title
String property, to do its job.
struct Image: View {
let title: String
var body: some View {
For the preview, and in ContentView, pass in the title for a default book, to get rid of the errors.
static var previews: some View {
Book.Image(title: Book().title)
Book.Image(title: Book().title)
But that’s still rendering a book symbol. The default book has the name “Title”, so we should expect a T, here, instead.
To get that working, start off with a Symbol constant, which will be a SwiftUI Image, made using the view’s title property passed into our fancy new initializer.
var body: some View {
let symbol =
SwiftUI.Image(title: title)
SwiftUI.Image(systemName: "book")
The initializer is failable! So if it returns nil, set the symbol to the book on the next line, instead, using nil coalescing.
let symbol =
SwiftUI.Image(title: title)
?? SwiftUI.Image(systemName: "book")
.resizable()
This second SwiftUI.Image
is not actually necessary. The compiler knows that the types have to match, on either side of the operator. So you can just write “dot init” instead, and let Swift infer the type.
?? 🟩.init(systemName: "book")
Then, use the symbol with all of those modifiers, below.
?? SwiftUI.Image(systemName: "book")
symbol
.resizable()
The preview should be rendering a “T” letter square now! But what about our empty title scenario? Embed what’s already in the preview in a VStack.
static var previews: some View {
VStack {
Book.Image(title: Book().title)
}
}
Then, add a book with an empty title.
VStack {
Book.Image(title: Book().title)
Book.Image(title: "")
}
But what about if the title doesn’t have a first character that can be lowercased? What if the title is an emoji?
Book.Image(title: "")
Book.Image(title: "📖")
}
We’re back to a place where our initializer isn’t returning a useful Image, but it’s also not failing like we want it to. This is sort of an edge case, but we can handle it, so head back to the initializer!
As we’ve seen SwiftUI’s Image can’t really help us check if a system image actually exists, right now. But UIKit’s UIImage class can. So, move this symbol name up to be charge of the guard statement…
init?(title: String) {
guard
let character = title.first🟩,
case let symbolName = "\(character.lowercased()).square",
Then check to see if a UIImage made with the symbol name is not nil. We don’t need to store it, we just need to know if we could do it.
let symbolName = "\(character.lowercased()).square"
guard UIImage(systemName: symbolName) != nil else {
<#statements#>
}
self.init(systemName: symbolName)
If the result of that is nil, return nil!
guard UIImage(systemName: symbolName) != nil else {
return nil
}
You should see an error at this point. When you try to guard let
with something that isn’t an optional - like this symbol name, here, you need an extra keyword. It’s a bit unintuitive, but it’s case
. Just add case
in front of let
.
guard
let character = title.first,
🟩case let symbolName = "\(character.lowercased()).square",
UIImage(systemName: symbolName) != nil
Now our Book Image can handle most titles with a matching letter square, empty titles with a book, and titles that start with unusual characters are also handled with a book!
With those three images displaying in the preview, your Books, and Book Images, are ready to go!