When I scroll down to dismiss a sheet containing a UIFontPickerViewController
, there is some extra space instead.
import UIKit
import SwiftUI
public struct SUIFontPicker: UIViewControllerRepresentable {
@Environment(\.presentationMode) var presentationMode
private let onFontPick: (UIFontDescriptor) -> Void
public init(onFontPick: @escaping (UIFontDescriptor) -> Void) {
self.onFontPick = onFontPick
}
public func makeUIViewController(context: UIViewControllerRepresentableContext<SUIFontPicker>) -> UIFontPickerViewController {
let configuration = UIFontPickerViewController.Configuration()
configuration.includeFaces = true
configuration.displayUsingSystemFont = false
let vc = UIFontPickerViewController(configuration: configuration)
vc.delegate = context.coordinator
return vc
}
public func makeCoordinator() -> SUIFontPicker.Coordinator {
return Coordinator(self, onFontPick: self.onFontPick)
}
public class Coordinator: NSObject, UIFontPickerViewControllerDelegate {
var parent: SUIFontPicker
private let onFontPick: (UIFontDescriptor) -> Void
init(_ parent: SUIFontPicker, onFontPick: @escaping (UIFontDescriptor) -> Void) {
self.parent = parent
self.onFontPick = onFontPick
}
public func fontPickerViewControllerDidPickFont(_ viewController: UIFontPickerViewController) {
guard let descriptor = viewController.selectedFontDescriptor else { return }
onFontPick(descriptor)
parent.presentationMode.wrappedValue.dismiss()
}
public func fontPickerViewControllerDidCancel(_ viewController: UIFontPickerViewController) {
parent.presentationMode.wrappedValue.dismiss()
}
}
public func updateUIViewController(_ uiViewController: UIFontPickerViewController,
context: UIViewControllerRepresentableContext<SUIFontPicker>) {
}
}
struct FontPicker: View {
@State var isFontPickerPresented = false
@State var selectedFont: Font? = nil
var body: some View {
Button {
isFontPickerPresented = true
} label: {
HStack {
Text("Select font")
}
}
.sheet(isPresented: $isFontPickerPresented) {
SUIFontPicker { fontDescriptor in
let size = UIFont.preferredFont(forTextStyle: .body).pointSize
let newFont = UIFont(descriptor: fontDescriptor, size: size)
print(newFont)
selectedFont = Font(newFont)
}
}
.font(selectedFont)
}
}
struct FontPicker_Previews: PreviewProvider {
static var previews: some View {
Form {
FontPicker()
FontPicker()
}
}
}
I would like to know why this happens and whether the following effect can be achieved while using a .sheet
modifier.
Here, present(fontPicker, animated: true)
is used instead. There is automatically a “Choose Font” title, a “Cancel” button and the search bar is pinned.
import SwiftUI
class FontPickerViewController: UIViewController, UIFontPickerViewControllerDelegate {
var fontName: String = "Helvetica"
var fontPicker = UIFontPickerViewController()
override func viewDidLoad() {
super.viewDidLoad()
fontPicker.delegate = self
let button = UIButton(type: .system)
button.titleLabel?.font = .systemFont(ofSize: 17)
button.frame = CGRect(x: 0, y: 0, width: 100, height: 30) // how to fit content?
button.setTitle("Choose font", for: .normal)
button.addTarget(self, action: #selector(showFontPicker(sender:)), for: .touchUpInside)
view.addSubview(button)
}
func fontPickerViewControllerDidPickFont(_ viewController: UIFontPickerViewController) {
dismiss(animated: true)
guard let descriptor = viewController.selectedFontDescriptor else { return }
let font = UIFont(descriptor: descriptor, size: 17)
fontName = font.fontName
}
@objc func showFontPicker(sender: UIButton!) {
let configuration = UIFontPickerViewController.Configuration()
configuration.includeFaces = true
fontPicker = UIFontPickerViewController(configuration: configuration)
present(fontPicker, animated: true)
}
}
struct FontPickerTest: UIViewControllerRepresentable {
@Binding var fontName: String
func makeUIViewController(context: Context) -> some UIViewController {
let picker = FontPickerViewController()
picker.fontName = fontName
return picker
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {}
}
struct TestPreview: View {
var body: some View {
Form {
FontPickerTest(fontName: .constant(""))
}
}
}
struct FontPickerTest_Previews: PreviewProvider {
static var previews: some View {
TestPreview()
}
}
I don’t really know how to use a UIButton
properly and I want to be a able to use a VStack
or HStack
to display the selected font.
struct FontPicker: View {
@State var isFontPickerPresented = false
@State var selectedFont: UIFont = UIFont.preferredFont(forTextStyle: .body)
var body: some View {
Button {
isFontPickerPresented = true
} label: {
VStack(alignment: .leading) {
Text("Select font")
Text(selectedFont.fontName)
.font(.caption) // how to only change size without changing font family
}
}
.sheet(isPresented: $isFontPickerPresented) {
SUIFontPicker { fontDescriptor in
let size = UIFont.preferredFont(forTextStyle: .body).pointSize
let newFont = UIFont(descriptor: fontDescriptor, size: size)
print(newFont)
selectedFont = newFont
}
}
.font(Font(selectedFont))
}
}
A similar issue is reported here. The only solution there is to make the button that presents UIFontPickerViewController
also in UIKit, as a VC that is just a UIButton
.
class FontPickerButtonController: UIViewController, UIFontPickerViewControllerDelegate {
var onFontPick: ((UIFontDescriptor) -> Void)?
var fontPicker = UIFontPickerViewController()
override func loadView() {
super.loadView()
fontPicker.delegate = self
let button = UIButton(type: .system)
button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
button.setTitle("Choose font", for: .normal)
button.addTarget(self, action: #selector(showFontPicker), for: .touchUpInside)
button.sizeToFit()
button.backgroundColor = .clear
view = button
}
func fontPickerViewControllerDidPickFont(_ viewController: UIFontPickerViewController) {
dismiss(animated: true)
guard let descriptor = viewController.selectedFontDescriptor else { return }
onFontPick?(descriptor)
}
@objc func showFontPicker() {
present(fontPicker, animated: true)
}
}
We then wrap this in a UIViewControllerRepresentable
. Since you want a SwiftUI Button
in the Form
, you can add a Binding<Bool>
to the UIViewControllerRepresentable
that can be set to true when the SwiftUI button is pressed.
struct FontPicker: UIViewControllerRepresentable {
@Binding var isPickerPresented: Bool
public func makeCoordinator() -> Coordinator {
return Coordinator(self, onFontPick: self.onFontPick)
}
public class Coordinator {
var parent: FontPicker
let picker = FontPickerButtonController()
init(_ parent: FontPicker, onFontPick: @escaping (UIFontDescriptor) -> Void) {
self.parent = parent
picker.onFontPick = onFontPick
}
}
var onFontPick: (UIFontDescriptor) -> Void
func makeUIViewController(context: Context) -> FontPickerButtonController {
context.coordinator.picker
}
func updateUIViewController(_ uiViewController: FontPickerButtonController, context: Context) {
if isPickerPresented {
context.coordinator.picker.showFontPicker()
// semantically this should be set to false when the font picker is dismissed
// I'm being a bit lazy here
isPickerPresented = false
}
}
func sizeThatFits(_ proposal: ProposedViewSize, uiViewController: FontPickerButtonController, context: Context) -> CGSize? {
uiViewController.view.intrinsicContentSize
}
}
Usage:
@State var showFontPicker = false
var body: some View {
Form {
Button {
showFontPicker = true
} label: {
VStack {
FontPicker(isPickerPresented: $showFontPicker) { desc in
fontFamily = desc.fontAttributes[.family] as? String
}
Text(fontFamily ?? "Not Selected")
.font(customCaptionFont)
.foregroundColor(.black)
}
}
}
}
@State var fontFamily: String?
var customCaptionFont: Font {
if let fontFamily {
return Font.custom(fontFamily, size: UIFont.preferredFont(forTextStyle: .caption1).pointSize, relativeTo: .caption)
} else {
return .caption
}
}
Output: