Skip to content

Instantly share code, notes, and snippets.

@maximkrouk
Created December 13, 2022 20:42
Show Gist options
  • Save maximkrouk/e8c2c362715756077e6382ffa0e4ea59 to your computer and use it in GitHub Desktop.
Save maximkrouk/e8c2c362715756077e6382ffa0e4ea59 to your computer and use it in GitHub Desktop.
Conversions between UIKit and SwiftUI fonts
import UIKit
import SwiftUI
extension Font {
public init(uiFont: UIFont) {
self.init(uiFont as CTFont)
}
public func toUIFont() -> UIFont? {
var font: UIFont?
inspect(self) { label, value in
guard label == "provider" else { return }
inspect(value) { label, value in
guard label == "base" else { return }
guard let provider = SwiftUIFontProvider(from: value) else {
return assertionFailure("Could not create font provider")
}
font = provider.uiFont()
}
}
return font
}
}
private enum SwiftUIFontProvider {
case system(size: CGFloat, weight: Font.Weight?, design: Font.Design?)
case textStyle(Font.TextStyle, weight: Font.Weight?, design: Font.Design?)
case platform(CTFont)
func uiFont() -> UIFont? {
switch self {
case let .system(size, weight, _):
return weight?.toUIFontWeight()
.map { .systemFont(ofSize: size, weight: $0) }
?? .systemFont(ofSize: size)
case let .textStyle(textStyle, _, _):
return textStyle.toUIFontTextStyle()
.map(UIFont.preferredFont(forTextStyle:))
case let .platform(font):
return font as UIFont
}
}
init?(from reflection: Any) {
switch String(describing: type(of: reflection)) {
case "SystemProvider":
var props: (
size: CGFloat?,
weight: Font.Weight?,
design: Font.Design?
) = (nil, nil, nil)
inspect(reflection) { label, value in
switch label {
case "size":
props.size = value as? CGFloat
case "weight":
props.weight = value as? Font.Weight
case "design":
props.design = value as? Font.Design
default:
return
}
}
guard let size = props.size
else { return nil }
self = .system(
size: size,
weight: props.weight,
design: props.design
)
case "TextStyleProvider":
var props: (
style: Font.TextStyle?,
weight: Font.Weight?,
design: Font.Design?
) = (nil, nil, nil)
inspect(reflection) { label, value in
switch label {
case "style":
props.style = value as? Font.TextStyle
case "weight":
props.weight = value as? Font.Weight
case "design":
props.design = value as? Font.Design
default:
return
}
}
guard let style = props.style
else { return nil }
self = .textStyle(
style,
weight: props.weight,
design: props.design
)
case "PlatformFontProvider":
var font: CTFont?
inspect(reflection) { label, value in
guard label == "font" else { return }
font = (value as? CTFont?)?.flatMap { $0 }
}
guard let font else { return nil }
self = .platform(font)
default:
return nil
}
}
}
extension Font.TextStyle {
fileprivate func toUIFontTextStyle() -> UIFont.TextStyle? {
switch self {
case .largeTitle:
return .largeTitle
case .title:
return .title1
case .headline:
return .headline
case .subheadline:
return .subheadline
case .body:
return .body
case .callout:
return .callout
case .footnote:
return .footnote
case .caption:
return .caption1
default:
switch self {
case .title2:
return .title2
case .title3:
return .title3
case .caption2:
return .caption2
default:
assertionFailure()
return .body
}
}
}
}
extension SwiftUI.Font.Weight {
fileprivate func toUIFontWeight() -> UIFont.Weight? {
var rawValue: CGFloat? = nil
inspect(self) { label, value in
guard label == "value" else { return }
rawValue = value as? CGFloat
}
guard let rawValue else { return nil }
return .init(rawValue)
}
}
private func inspect(_ object: Any, with action: (Mirror.Child) -> Void) {
Mirror(reflecting: object).children.forEach(action)
}
@maximkrouk
Copy link
Author

maximkrouk commented Dec 13, 2022

For now it's just a snippet, but this code works on iOS14 🤡

func testFonts() {
    let fonts: [(String, Font?)] = [
        ("Font.system(size:)", Font.system(size: 12)),
        ("Font.system(size:weight:design:)", Font.system(size: 12, weight: .bold, design: .rounded)),
        ("Font.system(_:design:)", Font.system(.title, design: .rounded)),
        ("Font.system(_:design:weight:)", {
            if #available(iOS 16.0, *) {
                return .some(.system(.title, design: .rounded, weight: .bold))
            } else {
                return .none
            }
        }()),
        ("Font.body", Font.body),
        ("Font.caption", Font.caption),
        ("Font.init(UIFont.system(size:weight:width:) as CTFont)", {
            if #available(iOS 16.0, *) {
                return .some(.init(UIFont.systemFont(ofSize: 12, weight: .light, width: .condensed) as CTFont))
            } else {
                return .none
            }
        }()),
        ("Font.init(UIFont.system(size:weight) as CTFont)", Font.init(UIFont.systemFont(ofSize: 12, weight: .light) as CTFont)),
        ("Font.init(UIFont.init(name:size:) as CTFont)", Font.init(UIFont(name: "HelveticaNeue-Medium", size: 24)! as CTFont))
    ]

    fonts.compactMap { label, font in
        font.map { (label, $0) }
    }.forEach { label, font in
        print(label, "is_convertable:", font.toUIFont() != nil)
    }
}

Output:

Font.system(size:) is_convertable: true
Font.system(size:weight:design:) is_convertable: true
Font.system(_:design:) is_convertable: true
Font.system(_:design:weight:) is_convertable: true
Font.body is_convertable: true
Font.caption is_convertable: true
Font.init(UIFont.system(size:weight:width:) as CTFont) is_convertable: true
Font.init(UIFont.system(size:weight) as CTFont) is_convertable: true
Font.init(UIFont.init(name:size:) as CTFont) is_convertable: true

@LeoNatan
Copy link

I've added the following to support custom fonts:

//private enum SwiftUIFontProvider
case ready(UIFont)

...

//func uiFont() -> UIFont?
case let .ready(font):
	return font

...

//init?(from reflection: Any)
case "NamedProvider":
	var name: String? = nil
	var size: CGFloat? = nil
	var textStyle: SwiftUI.Font.TextStyle? = nil

	inspect(reflection) { label, value in
		switch label {
		case "name":
			name = value as? String
			break
		case "size":
			size = value as? CGFloat
			break
		case "textStyle":
			textStyle = value as? SwiftUI.Font.TextStyle
			break
		default:
			break
		}
	}
			
	guard let name, let size else { return nil }
			
	let font = UIFont(name: name, size: size)
	guard var font else { return nil }
			
	if let textStyle = textStyle?.toUIFontTextStyle() {
		font = UIFontMetrics(forTextStyle: textStyle).scaledFont(for: font)
	}
			
	self = .ready(font)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment