How to obtain list of property names of static/class constants of the same type?

For things like

SwiftUI.Font.Weight:

static let black: Font.Weight
static let bold: Font.Weight
static let heavy: Font.Weight
static let light: Font.Weight
static let medium: Font.Weight
static let regular: Font.Weight
static let semibold: Font.Weight
static let thin: Font.Weight
static let ultraLight: Font.Weight

(For this I want ["black", "bold", "heavy", "light", "medium", "regular", "semibold", "thin", "ultraLight"]

ListStyle

I want to get:

["automatic", "bordered", "bordered(alternateBackground: Bool)", etc]

This is the same ask: Build an array of UnitMass values … | Apple Developer Forums

How to? Use SourceKit to somehow "grep" these list of "identifiers" of all the values?

Please use UnitMass and all the constants define there like kilograms, etc.

2 Likes

As far as ListStyle goes, that's no different than actual enums with associated values, in that CaseIterable can't be automatically synthesized.


The others are more like enums without associated values.
I don't know SourceKit. If you figure another solution out, please post here.
Implementing it manually could be worse, but I don't know why Apple didn't provide the following:

Font.Weight: CaseIterable & CustomStringConvertible
// MARK: - CaseIterable
extension Font.Weight: CaseIterable {
  @available(macOS, deprecated: 14, message: "It's that time of year—check for new cases!")
  public static var allCases: [Self] {
    [ ultraLight,
      thin,
      light,
      regular,
      medium,
      semibold,
      bold,
      heavy,
      black
    ]
  }
}

// MARK: - CustomStringConvertible
extension Font.Weight: CustomStringConvertible {
  @available(macOS, deprecated: 14, message: "It's that time of year—check for new cases!")
  public var description: String {
    switch self {
    case .ultraLight: return "ultraLight"
    case .thin: return "thin"
    case .light: return "light"
    case .regular: return "regular"
    case .medium: return "medium"
    case .semibold: return "semibold"
    case .bold: return "bold"
    case .heavy: return "heavy"
    case .black: return "black"
    default: return "\(self)"
    }
  }
}
Font.Weight.allCases.map(String.init).sorted()

These are not enumerations (and UnitMass is not even a value type). Until Apple has this implemented you are on your own and you will need to maintain your own array(s) of values / names.

BTW, under the hood Font.Weight is just a Double, and if you wish so you can define your own weight in between, say, regular and medium (although as this is undocumented it can change, so do this on your own risk). You can also define your own UnitMass – UnitMass(symbol: "mine")

How? You can access the value via Mirror, but Font.Weight doesn't have a public intiializer.

Only for those brave enough using undocumented features!

Use on your own risk
extension Font.Weight {
    var rawValue: Double {
        unsafeBitCast(self, to: Double.self)
    }
    init(rawValue: Double) {
        self = unsafeBitCast(rawValue, to: Font.Weight.self)
    }
}

// example:
extension Font.Weight {
    static let extraBlack = Font.Weight(rawValue: 0.8)
    static let ultraBlack = Font.Weight(rawValue: 1.0)
}

Edit: although it doesn't buy you too much: you can add a couple of extra weights but won't be able varying font weight like you can in UIKit:

varying font weight

FontWeights

1 Like

They are all set of statically defined constants. And the reason I'm asking about this is that usually you need to present to the user this list and let them pick a value out of these, such as font weight: let the user choose. Same thing with ListStyle, SwiftUI colors.

I usually use an enum to represent this set of "enumeration-like" values, for example, for UnitMass, I define the enum:

enum MyUnitMass: Hashable, Identifiable, CaseIterable {
    case kilograms
    case grams
    case decigrams
    case centigrams
    case milligrams
    case micrograms
    case nanograms
    case picograms
    case ounces
    case pounds
    case stones
    case metricTons
    case shortTons
    case carats
    case ouncesTroy
    case slugs

    var id: Self { self }
    
    var localizedName: String {
        switch self {
        case .kilograms:
            return String(localized: "Killograms")
        case .grams:
            return String(localized: "Grams")
        case .decigrams:
            return String(localized: "Decigrams")
        case .centigrams:
            return String(localized: "Centigrams")
        case .milligrams:
            return String(localized: "Milligrams")
        case .micrograms:
            return String(localized: "Micrograms")
        case .nanograms:
            return String(localized: "Nanograms")
        case .picograms:
            return String(localized: "Picograms")
        case .ounces:
            return String(localized: "Ounces")
        case .pounds:
            return String(localized: "Pounds")
        case .stones:
            return String(localized: "Stones")
        case .metricTons:
            return String(localized: "MetricTons")
        case .shortTons:
            return String(localized: "ShortTons")
        case .carats:
            return String(localized: "Carats")
        case .ouncesTroy:
            return String(localized: "OuncesTroy")
        case .slugs:
            return String(localized: "Slugs")
        }
    }
    
    
    var unit: UnitMass {
        switch self {
        case .kilograms:
            return .kilograms
        case .grams:
            return .grams
        case .decigrams:
            return .decigrams
        case .centigrams:
            return .centigrams
        case .milligrams:
            return .milligrams
        case .micrograms:
            return .micrograms
        case .nanograms:
            return .nanograms
        case .picograms:
            return .picograms
        case .ounces:
            return .ounces
        case .pounds:
            return .pounds
        case .stones:
            return .stones
        case .metricTons:
            return .metricTons
        case .shortTons:
            return .shortTons
        case .carats:
            return .carats
        case .ouncesTroy:
            return .ouncesTroy
        case .slugs:
            return .slugs
        }
    }

    static func picker(@Binding selected: Self) -> some View {
        Picker("Unit Mass", selection: $selected) {
            ForEach(Self.allCases) {
                Text($0.localizedName).tag($0)
            }
        }
    }
}

For FontWeight, it's the same:

Summary
enum MyListStyle: String, CaseIterable, Hashable, Identifiable {
    case automatic = "Automatic"
//    case bordered = "Bordered"      // 'bordered' is unavailable in iOS
    // I don't know how to handle case with associated value :(
//    case borderAlternatesRowBackgrounds // bordered(alternatesRowBackgrounds:)
    case grouped = "Grouped"
    case inset = "Inset"
    case insetGrouped = "InsetGrouped"
    case plain = "Plain"
    case sidebar = "Sidebar"
    
    var id: Self { self }
    
    // map to SwiftUI ListStyle
    var style: any ListStyle {
        switch self {
        case .automatic:
            return .automatic
        case .grouped:
            return .grouped
        case .inset:
            return .inset
        case .insetGrouped:
            return .insetGrouped
        case .plain:
            return .plain
        case .sidebar:
            return .sidebar
        }
    }
    
    var displayName: String {
        String(localized: String.LocalizationValue(rawValue))
    }
    
    
    static func picker(@Binding selected: Self) -> some View {
        let v = Picker("List Style", selection: $selected) {
            ForEach(Self.allCases) {
                Text($0.displayName).tag($0)
            }
        }
        dump(v)
        return v
    }
}

I'm looking for ways to automate the creation of these types of enum. The first thing I need is the list of all the "cases".

ChatGPT can give me the answer:

Too bad this is not a complete list. It's missing 3 cases.

It's did it perfectly for Font.Weight:

Using your method, you can use any value 0 - 1 but the font only show weight change in discrete steps:

This hints they are building this list manually.

Yep, see my edit with video above. For a font like RobotoFlex † you can reveal a couple of extra weights (like extraBlack and ultraBlack) but to get a true varying font weight you'll need to use CoreFont + UIFont (which UIFont you can later use with SwiftUI's font modifier). Hope this can be improved in SwiftUI in the future.

† RobotoFlex is a variable font, it was used with both SwiftUI and UIKit texts in my video above.

So it's limited to what the font can provide? What if I use a variable font? So SF Pro is not a variable font?

But go here and scroll down/search for SF Pro and you can continuously vary the weight.

Here's my understanding of the current situation:

  • Of the currently listed 5 fonts on this page Fonts - Apple Developer all fonts but "SF Compact" are variable fonts.
  • The font obtained via "UIFont.systemFont(ofSize: fontSize)" (whose family name is currently revealed as ".AppleSystemUIFont") is also a variable font. It might be a version of "SF Pro".
  • SwiftUI.Font.Weight currently has 9 weights.
  • Fonts may have a number of weights (9, less than or more than 9, or a continuum for a variable font)
  • Using the above undocumented hack you can have an arbitrary weight in SwiftUI.Font.Weight and that can indeed select some extra weights (e.g. extraBlack + ultraBlack in "Roboto Flex")
  • However SwiftUI.Font.Weight can not be used to select an arbitrary weight (of a variable font).
  • Down the road it would be reasonable to have a new API that allows controlling any font "axis" variable, something like this:
	let someFont = SwiftUI.Font...
	...
	Text("Hello")
	    .font(someFont.axis(identifier, value))
  • Along with ability to enumerate axes:
    someFont.axes.forEach { axis in
    	print(axis.id)
    	print(axis.name)
    	print(axis.minValue)
    	print(axis.maxValue)
    	print(axis.defaultValue)
    }
  • You can obtain this functionality today via CTFont -> UIFont route - and use the resulting UIFont in SwiftUI via .font(_ v: UIFont) modifier.

  • Once we have that API the fate of older API like .weight(_ v: Font.Weight) is not clear. They will probably stay the way they are (for compatibility reasons) and the new API is introduced, e.g. .weight(_ v: CGFloat) which would be a shortcut for some newer:

    .axis(Font.Axis.weight, _ v: CGFloat)

with a fallback to older method for fixed fonts or fonts with no wght axis.


All in all, I won't recommend spending too much time today on trying to automatically generate those tables. Just do what ChatGPT apparently did – build those tables manually. In two year's time you'll need to revise the whole thing anyway.

SwiftUI font full variable font control is not doable today. But soon we may be able to but need new API? Okay, good to know.

You can fully control all the axis of a variable font:

import SwiftUI

public enum FontVariations: Int, CustomStringConvertible {
    case weight = 2003265652
    case width = 2003072104
    case opticalSize = 1869640570
    case grad = 1196572996
    case slant = 1936486004
    case xtra = 1481921089
    case xopq = 1481592913
    case yopq = 1498370129
    case ytlc = 1498696771
    case ytuc = 1498699075
    case ytas = 1498693971
    case ytde = 1498694725
    case ytfi = 1498695241

    public var description: String {
        switch self {
        case .weight:
            return "Weight"
        case .width:
            return "Width"
        case .opticalSize:
            return "Optical Size"
        case .grad:
            return "Grad"
        case .slant:
            return "Slant"
        case .xtra:
            return "Xtra"
        case .xopq:
            return "Xopq"
        case .yopq:
            return "Yopq"
        case .ytlc:
            return "Ytlc"
        case .ytuc:
            return "Ytuc"
        case .ytas:
            return "Ytas"
        case .ytde:
            return "Ytde"
        case .ytfi:
            return "Ytfi"
        }
    }

}

public extension Font {
    static func variableFont(_ size: CGFloat, axis: [Int: Int] = [:]) -> Font {
        let uiFontDescriptor = UIFontDescriptor(fontAttributes: [.name: "Roboto Flex", kCTFontVariationAttribute as UIFontDescriptor.AttributeName: axis])
        let newUIFont = UIFont(descriptor: uiFontDescriptor, size: size)
        return Font(newUIFont)
    }
}

I found this I think on GitHub but I forgot.

Does anyone know where to download the “downloadable” Apple System Fonts?