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.)