I am attempting to create a custom font picker (similar to the one in Pages) using SwiftUI, because the current UIFontPickerViewController
isn't sufficient enough for my app.
When running the app and presenting FontPickerView
in a sheet, the app seems to pause, the sheet doesn't appear, and a lot of dialog continuously pops up in the Xcode console.
This is an example of what keeps showing:
CoreText note: Client requested name ".SFUI-Regular", it will get TimesNewRomanPSMT rather than the intended font. All system UI font access should be through proper APIs such as CTFontCreateUIFontForLanguage() or +[UIFont systemFontOfSize:].
The .SFUI
string changes with different font styles, for example -Bold
, -Compressed
, -SemiExpandedLight
...
I believe the problem lies when accessing the UIFont
family and font name properties and methods.
Is there a reason why this is happening? A possible solution?
Help is appreciated.
Here is some code you can test (Xcode 13 beta 3):
extension Character {
var isUppercase: Bool {
String(self).uppercased() == String(self)
}
}
extension UIFont {
// Will show dialog from here
class func familyName(forFontName fontName: String) -> String {
var familyName = ""
familyNames.forEach { family in
fontNames(forFamilyName: family).forEach { font in
if font == fontName {
familyName = family
}
}
}
return familyName
}
}
struct FontPickerView: View {
@Environment(\.dismiss) private var dismiss
@ScaledMetric private var linkPaddingLength = 24
@State private var searchText = ""
@State private var linkSelection: String?
@Binding var selectedFontName: String
// Will show dialog from here
private var familyNames: [String] {
UIFont.familyNames.filter {
$0.contains(searchText) || searchText.isEmpty
}
}
// Will show dialog from here
private var familyPickerBinding: Binding<String> {
Binding {
UIFont.familyName(forFontName: selectedFontName)
} set: {
selectedFontName = UIFont.fontNames(forFamilyName: $0)[0]
}
}
var body: some View {
NavigationView {
List {
Picker("All Fonts", selection: familyPickerBinding) {
ForEach(familyNames, id: \.self, content: pickerRow)
}
.labelsHidden()
.pickerStyle(.inline)
}
.listStyle(.insetGrouped)
.searchable(text: $searchText, placement: .navigationBarDrawer)
.navigationTitle("Fonts")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
Button("Cancel", action: dismiss.callAsFunction)
}
}
}
private func linkPadding(forFamilyName familyName: String) -> CGFloat {
familyPickerBinding.wrappedValue == familyName ? 0 : linkPaddingLength
}
private func familyText(forFamilyName familyName: String) -> some View {
Text(familyName)
.font(.custom(familyName, size: UIFont.labelFontSize))
}
private func familyTextWithStyles(forFamilyName familyName: String) -> some View {
ZStack(alignment: .leading) {
NavigationLink("Style options", tag: familyName, selection: $linkSelection) {
FontStylesView(selection: $selectedFontName, family: familyName)
}
.hidden()
HStack {
familyText(forFamilyName: familyName)
Spacer()
Button(action: { linkSelection = familyName }) {
Label("Style options", systemImage: "info.circle")
.labelStyle(.iconOnly)
.imageScale(.large)
}
.buttonStyle(.borderless)
.padding(.trailing, linkPadding(forFamilyName: familyName))
}
}
}
private func pickerRow(forFamilyName familyName: String) -> some View {
Group {
if UIFont.fontNames(forFamilyName: familyName).count == 1 {
familyText(forFamilyName: familyName)
} else {
familyTextWithStyles(forFamilyName: familyName)
}
}
}
}
struct FontStylesView: View {
@Binding var selection: String
let family: String
var body: some View {
List {
Picker("Font Styles", selection: $selection) {
// Will show dialog from here
ForEach(UIFont.fontNames(forFamilyName: family), id: \.self) { font in
Text(fontType(forFontName: font))
.font(.custom(font, size: UIFont.labelFontSize))
}
}
.labelsHidden()
.pickerStyle(.inline)
}
.navigationTitle(family)
}
private func fontType(forFontName fontName: String) -> String {
if let index = fontName.lastIndex(of: "-") {
var text = String(fontName.suffix(from: index).dropFirst())
// Add spaces between words.
let indexes = text.enumerated().filter { $0.element.isUppercase }.map { $0.offset }
var count = 0
indexes.forEach { index in
guard index > 0 else { return }
text.insert(" ", at: text.index(text.startIndex, offsetBy: index + count))
count += 1
}
return text
} else {
return "Regular"
}
}
}
(There are a few bugs that I am going to fix.)