Can’t dismiss sheet containing a font picker

When I scroll down to dismiss a sheet containing a UIFontPickerViewController, there is some extra space instead.

dismiss sheet

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.

dismiss sheet

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:

enter image description here

Leave a Comment