Why so many API is internal in native libraries?

I think we've all come across situations where the API was more or less opaque than the authors intended. If it's more opaque, it's not a big deal to fix, but when it's less opaque, you get all sorts of issues when you want to fix it e.g. if you unintentionally expose some internal detail that you really need to change to improve performance or something and it turns out lots of client code depends on that detail.

3 Likes

From the client's point of view, the situation is exactly the opposite. If the API is less opaque than you need, it isn't a big deal, because you won't be using all the available details without a real need. But if they are more opaque than you need, this can lead to the fact that the task facing you becomes more complicated or becomes completely unsolvable. I quite often have to fork because of this, but this is only possible for open source libraries.
And in this I see a real problem for which there is no universal solution.
But for changing api there is such a solution - versioning. So I think you're overstating the api-chaging issue.

4 Likes

I conditionally distinguish two ways of using libraries.

  1. Use in the final product, default way.
  2. Use as a tool when creating your own library.

The second way requires less opacity than the first. But as my experience shows, many developers think only about the first way when creating libraries.
IMHO when developing libraries, it's important not to forget that they can be used not only for their intended purpose. They can be built upon, wrapped, used as auxiliary tools, and so on.
If you want to create a great tool make it extendable.

Bạn nói rất hay rất chính xác .

Nguyen Pham

Vào ngày 27 thg 3, 2023, lúc 20:10, Dankinsoid via Swift Forums notifications@swift.discoursemail.com đã viết:

| dankinsoid
March 27 |

  • | - |

I often come across situations when I need access to the internal API of libraries. Sometimes it happens when the scenario I have is different from those that the author of the library foresaw, and this is a separate topic.
But sometimes I don't have access to the API, which obviously can be useful.
For example, Swift has many private environment variables, such as foregroundColor. Many structs (Text, Color, Font etc) are completely closed and there is not even always a way to convert them to similar ones in UIKit. I studied the structure of the Font (I had to convert it to UIFont) using reflection and did not see any reason why its content should be private. I was very surprised to find that the components of the Сolor are not so easy to get, even more difficult than from UIColor.

1 Like

True, but that usually happens before you (the client) start work. Ask for the feature to be exposed or find another way or write your own way (forking is a subset of the latter).

But for changing api there is such a solution - versioning. So I think you're overstating the api-chaging issue.

Versioning doesn't solve the problem, it just tells you (the client) that there is going to be one. It also allows you to freeze the API at a certain point but that adds technical debt. At some point in the future, the version you freeze at will be end of life'd and then it will stop working altogether.

Removing features is a bigger deal than adding them and this is acknowledged in semantic versioning where breaking changes require a major version bump but adding new functionality does not.

2 Likes

Disagree, why? It happens anytime

If versioning doesn't solve the problem then opaque doesn't either

Just compare two problems
On the one hand you have changed API that can be easily resolved by updating your code
On the other hand you have changed API too but a little less and a task that you cannot resolve at all or using ugly workarounds

Yeah.. My house my rules, I understand.

Opaque API is a good thing in general, just when I say "overly opaque" I mean it is a step or a few steps too far, and way beyond common sense. Hopefully the example below explains what I mean.

Asking Apple for the feature doesn't work in practice (tried quite a few times), and the alternatives are typically either bad or too complicated (and thus bad).

Take "Font" for example (and the corresponding "font" modifier). Its "getters" are currently private (perhaps internal, but effectively private for us). IMHO the litmus test for making these getters public or private should be this:

Obviously to make a working .font implementation I would need to do something along these lines (pseudocode):

  1. get the underlying font settings (all of the parameters)
  2. construct the corresponding UIFont or CGFont or CTFont
  3. apply it to the view

Step 1 requires getters to be available. Sure, not everything, just some minimal amount that allows to fully reconstruct the font.

With the current opaque getters it's a very daunting and time consuming task of picking through "Font" internals (by Mirror or Obj-c API, etc) – and the code that's doing so is inevitably complicated and fragile as it now depends upon undocumented features to do what it needs to do and can break anytime).

Hence by having an overly "opaque API Font API" in this example we've got:

  1. a happy Apple developer (who is now free to mess with the getters as (s)he pleases as they are opaque
  2. an unhappy third party developer(s) spending way too much time reverse engineering and/or reimplementing the thing.
  3. an unhappy user(s) experiencing the problem by way of app crashes.
2 Likes

Swift went with the trend and encourages a restrictive style where libraries keep things hidden from users and forbid overriding. That gives library authors more freedom to change internals without causing trouble for clients during updates — with the drawback to cause trouble all the time :joy:.
Especially with open source frameworks, it can be quite ridiculous when you need to change a very tiny detail of a class and have to fork the whole thing because subclassing or write access is disallowed, and it's even more frustrating with closed source.

Some will surely say subclassing is bad anyways, and that we need minimal disclosure to protect code from its users — but I don't consider that standpoint to be eternal truth and would prefer defaults that leave more freedom:
Instead of "you cannot", my choice would be "you can, but at your own risk". That would be way more honest, because it is futile to assume a library author would know all needs of all their users.

However, Swift claims to be opinionated, and when there are opinions, there is always disagreement...

4 Likes

The problem with this is that it's not at your own risk. If people install an update and your app breaks:

  • the user reaction is "wow this update is terrible, it broke my app", rather than "wow this app is terrible, it was using unstable APIs"
  • more importantly: the user's app is still broken, regardless of who is blamed for it
  • even worse than that, if your app is popular enough that breaking it is unthinkable, it's now impacting every other app because the system has to work around your app, or even just cancel making the (presumably desirable) change.

I understand where you're coming from, and if this was a purely personal decision I would completely agree, but unfortunately one developer's choices can impact 2 billion people. These are design decisions made from long bitter experience, not nebulous theoretical benefits.

14 Likes

Note that I wouldn't take anything away that is currently possible.
I'd just add an option, so the the choices for library authors would be "you can't", "you are encouraged to do" and "I just don't know" — and I'm pretty sure that a huge number of cases actually belong into the last category.

The status quo is that overly opaque APIs impacts two billion people all the time, but you just don't notice it regularly.

Oh, and let's not forget: Even with all those hard restrictions, updates break apps anyways quite often.

2 Likes

But that's not a problem with the concept of opaque APIs, it's a problem with the way Apple works.

As for Font, the docs say this:

The system resolves a font’s value at the time it uses the font in a given environment because Font is a late-binding token.

It seems to me that many of the attributes you might want to query simply aren't known until the font is actually used. It may just be bad design, but it seems to me that it is intended to be opaque and therefore looking inside it is just asking for trouble. My experience of SwiftUI is limited, but that seems to be a general philosophy. I'm not saying it is a good or bad philosophy but that is what the designers wanted, so you have to live with it or go back to UIKit or AppKit.

2 Likes

So, I want to get those attributes at the time of actual use... and there's a roadblock, see below.

Believe me it is painful. And yes, we are jumping back and forth between UIKit and SwiftUI to work around its bugs limitations.

This is a minimal illustrative example that causes pain due to overly opaqueness:

A custom UIKit based view:

import SwiftUI

class MyUiView: UIView {
    let string = "Hello, World" as NSString
    let defaultFont = UIFont.systemFont(ofSize: 12)
    var font: UIFont? { didSet { setNeedsDisplay() } }

    override func draw(_ rect: CGRect) {
        // time of use
        string.draw(in: bounds, withAttributes: [.font : font ?? defaultFont])
    }
}

struct MyView: UIViewRepresentable {
    func makeUIView(context: Context) -> MyUiView {
        let view = MyUiView()
        view.isOpaque = false
        return view
    }
    func updateUIView(_ uiView: MyUiView, context: Context) {
        uiView.font = context.environment.font?.uiFont
    }
}

it's usage in Swift UI:

struct ContentView: View {
    var body: some View {
        MyView()
            .font(.title2).bold().fontWeight(.light)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

@main struct MyApp: App {
    var body: some Scene {
        WindowGroup { ContentView() }
    }
}

and this conversion:

extension Font {
    var uiFont: UIFont? {
        // now what?! 🤔
        .systemFont(ofSize: 48)
    }
}

This isn't about SwiftUI specifically but about any "overly opaque" API, Apple or otherwise. Be reasonable with what you reveal and what you obscure.

3 Likes

I have had this pain plenty of times in my life. Read @David_Smith's excellent reply to understand why it cannot be any different for the examples you mention.

I have also had this pain also for libraries and software SDKs where the total number of developers using it, worldwide, would be counted in the 10s and not in the millions. When Apple makes an API they seem to be quite thorough and conservative in the API design for the reasons David writes about. Plenty of other entities defaults to the same closed API designs but totally lack the thoroughness in the API design. Often they have so few consumers and a totally different distribution method (than Apple) and in those cases a way more open API design would benefit everybody.

That might be one big problem Swift has today:
It wants to be used everywhere, but its development has an exclusive focus on Apples needs.
See [Second review] SE-0386: `package` access modifier for a recent example, or Any interest in Linux improvements? Has Swift for Linux caught up yet? or Stability of Swift on Windows?.
Looking further, there is a whole bunch of additions for SwiftUI, like [Accepted] SE-0279: Multiple Trailing Closures.

Also, there has been a significant change on how evolution works.
We started with "send in your wishes, we'll try to fulfil them" to "if you seriously want to propose a change, write an implementation first" to "hey, someone from Apple here, we created a feature which you can already use with the —needed-for-next-wwdc flag, let's quickly finish the formal proposal stuff".

6 Likes

Evolution has never been “send in your ideas and we’ll try to implement them for you”, and Apple never said it would be. Apple employees do read these forums, obviously, and we factor what we see here into our decisions about what to work on next, but that’s it, and that’s always been it. In the early days of the project, the Core Team did accept a few proposals in the that hadn’t been implemented yet, and we quickly realized that was a mistake because those proposals were not making any progress towards ever appearing in the language, so we stopped doing it. We never once imagined that it was Apple’s responsibility to finish those proposals because they’d been accepted, and if you thought that was how Evolution worked, well, I don’t know where you got that idea.

This does mean that your ability to affect the language without doing implementation work is mostly limited to making suggestions on other people’s proposals. That should not be surprising.

7 Likes

Unfortunately this leads to a situation when I have to reverse engineer SwiftUI internals – which is obviously very bad and can break any time. The alternative - to have my own parallel infrastructure analogues to what SwiftUI is already doing (just to have the getters!) – a massive amount of boilerplate, hard to maintain, quite error prone and not future proof code. In addition on the use side I have to remember using my wrappers like ".myfont", "mytracking", etc instead of standard modifiers, something very easy to forget. Just to support "font" (arbitrary ways to set it along with its parameters) one of my projects that decides to go the second way has about 600 lines of the mentioned boilerplate, and I suspect there are a few bugs in there. And then there's color, images and other whatnots.

1 Like

as a non-Apple swift user this is disappointing to hear, but i recognize this as a legitimate explanation and i appreciate the honest response.

regarding the “implementation work” aspect: to say that contributing to the compiler is challenging right now would be an understatement. even for someone who has a C++ background (an uncommon skillset in this community) just compiling the compiler itself is not easy.

i checked the Getting started guide to see if anything has changed since the last time i tried, and it seems you still need to have 70 GB of free disk space. i get this does not sound like a lot if you are building on a corporate-provisioned cloudtop/device, but for the majority of the time (65%?) since i started using swift (in 2016 or so) i was unable to build the toolchain on my personal machine because i did not have 70 GB of free disk space, or time to clear up 70 GB of disk space to build a toolchain. and to have a serious workflow for contributing to the compiler, you probably want to sustain more than one toolchain build per machine at a time.

to use sccache in docker also requires manual installation, although thankfully i don’t see any references to distcc anymore.

aside: i’ve never tried to contribute to the swift corelibs, but it sounds like there are similar obstacles in that domain as well.

obviously, contributing to any open source code base is going to require some amount of effort from the contributor. but i don’t know of many open source projects that have as many barriers to development as the swift compiler does. healthy open source projects prioritize onboarding and invest effort in making the project an attractive pursuit for community members looking for something to do. in the absence of that, the list of developers with the determination to implement things in the compiler instead of complaining to Apple employees is going to be short. this should not be surprising either.

9 Likes

Notwithstanding a rather discouraging statement above...

If you think you have a great idea for Swift please speak up even if you don't have time/skills implementing it. If it is a great idea indeed - someone (from Apple or otherwise) will pick it up and we will see it in Swift one day.

2 Likes

If I understand you correctly, you assume that the implementation will always be like you have found it to be.

If SwiftUI was my project, I would certainly have a long term goal of an implementation that do not depend on UIKit either at all or at least in some or major parts.

If the parts you use were to be part of the API it would put severe limits on the future development of SwiftUI.

1 Like

I am not implying that the getter for, say, font or colour should return UIFont or UIColor. Just that it should reveal enough information so I can fully create the font or colour from a Font/Color. And it's not just about the Font value itself, as you can apply things like "bold()" on the view itself – there should be public way to get that from the environment.

I know for sure the current opaqueness of those getters severely impact the current development of my app (and, I believe many other apps of many developers). Just look at this insanity:

Lots of boilerplate
import SwiftUI

// MARK: MyFont.Variant

extension MyFont {
    public enum Variant {
        case largeTitle
        case title
        case title2
        case title3
        case headline
        case subheadline
        case body
        case callout
        case footnote
        case caption
        case caption2
        
        case customNameSize
        case customNameSizeStyle
        case customFixedSize
        case systemSizeWeightDesign
        case systemStyleDesign
    }
}

// MARK: MyFont.Design

extension MyFont {
    public enum Design: Hashable {
        case `default`
        case serif
        case rounded
        case monospaced
        
        public var toUIKit: UIFontDescriptor.SystemDesign {
            switch self {
            case .default: return .default
            case .serif: return .serif
            case .rounded: return .rounded
            case .monospaced: return .monospaced
            }
        }
        
        public var toSwiftUI: SwiftUI.Font.Design {
            switch self {
            case .default: return .default
            case .serif: return .serif
            case .rounded: return .rounded
            case .monospaced: return .monospaced
            }
        }
    }
}

// MARK: MyFont.TextStyle

extension MyFont {
    public enum TextStyle: CaseIterable, Hashable {
        case largeTitle
        case title
        case title2
        case title3
        case headline
        case subheadline
        case body
        case callout
        case footnote
        case caption
        case caption2
        
        public var toUIKit: UIFont.TextStyle {
            switch self {
            case .largeTitle: return .largeTitle
            case .title: return .title1
            case .title2: return .title2
            case .title3: return .title3
            case .headline: return .headline
            case .subheadline: return .subheadline
            case .body: return .body
            case .callout: return .callout
            case .footnote: return .footnote
            case .caption: return .caption1
            case .caption2: return .caption2
            }
        }
        
        public var toSwiftUI: SwiftUI.Font.TextStyle {
            switch self {
            case .largeTitle: return .largeTitle
            case .title: return .title
            case .title2: return .title2
            case .title3: return .title3
            case .headline: return .headline
            case .subheadline: return .subheadline
            case .body: return .body
            case .callout: return .callout
            case .footnote: return .footnote
            case .caption: return .caption
            case .caption2: return .caption2
            }
        }
    }
}

// MARK: MyFont.Weight

extension MyFont {
    public enum Weight: Hashable, Sendable {
        case ultraLight
        case thin
        case light
        case regular
        case medium
        case semibold
        case bold
        case heavy
        case black
        
        public var toUIKit: UIFont.Weight {
            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
            }
        }
        
        public var toSwiftUI: SwiftUI.Font.Weight {
            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
            }
        }
    }
}

// MARK: MyFont.Leading

extension MyFont {
    public enum Leading: Hashable {
        case standard
        case tight
        case loose
        
        public var toSwiftUI: SwiftUI.Font.Leading {
            switch self {
            case .standard: return .standard
            case .tight: return .tight
            case .loose: return .loose
            }
        }
    }
}

// MARK: MyFont static members

extension MyFont {
    public static let largeTitle = Self(variant: .largeTitle)
    public static let title = Self(variant: .title)
    public static let title2 = Self(variant: .title2)
    public static let title3 = Self(variant: .title3)
    public static let headline = Self(variant: .headline)
    public static let subheadline = Self(variant: .subheadline)
    public static let body = Self(variant: .body)
    public static let callout = Self(variant: .callout)
    public static let footnote = Self(variant: .footnote)
    public static let caption = Self(variant: .caption)
    public static let caption2 = Self(variant: .caption2)
    
    public static func custom(_ name: String, size: CGFloat) -> Self {
        Self(variant: .customNameSize, name: name, size: size)
    }
    public static func custom(_ name: String, size: CGFloat, relativeTo style: TextStyle) -> Self {
        Self(variant: .customNameSizeStyle, name: name, size: size, style: style)
    }
    public static func custom(_ name: String, fixedSize: CGFloat) -> Self {
        Self(variant: .customFixedSize, name: name, fixedSize: fixedSize)
    }
    public static func system(size: CGFloat, weight: Weight = .regular, design: Design = .default) -> Self {
        Self(variant: .systemSizeWeightDesign, size: size, weight: weight, design: design)
    }
    public static func system(_ style: TextStyle, design: Design = .default) -> Self {
        Self(variant: .systemStyleDesign, style: style, design: design)
    }
}

// MARK: MyFont convertors

extension MyFont {
    public func italic() -> Self {
        var new = self
        new._italic = true
        return new
    }
    
    public func smallCaps() -> Self {
        var new = self
        new._smallCaps = true
        return new
    }
    
    public func lowercaseSmallCaps() -> Self {
        var new = self
        new._lowercaseSmallCaps = true
        return new
    }
    
    public func uppercaseSmallCaps() -> Self {
        var new = self
        new._uppercaseSmallCaps = true
        return new
    }
    
    public func monospacedDigit() -> Self {
        var new = self
        new._monospacedDigit = true
        return new
    }
    
    public func weight(_ weight: Weight) -> Self {
        var new = self
        new._weight = weight
        return new
    }
    
    public func bold() -> Self {
        var new = self
        new._bold = true
        return new
    }
    
    public func monospaced() -> Self {
        var new = self
        new._monospaced = true
        return new
    }
    
    public func leading(_ leading: Leading) -> Self {
        var new = self
        new._leading = leading
        return new
    }
}

// MARK: MyFont to SwiftUI.Font converion

extension MyFont {
    var toUIKit: UIFont {
        var font: UIFont
        switch variant {
        case .largeTitle:               font = .preferredFont(forTextStyle: .largeTitle)
        case .title:                    font = .preferredFont(forTextStyle: .title1)
        case .title2:                   font = .preferredFont(forTextStyle: .title2)
        case .title3:                   font = .preferredFont(forTextStyle: .title3)
        case .headline:                 font = .preferredFont(forTextStyle: .headline)
        case .subheadline:              font = .preferredFont(forTextStyle: .subheadline)
        case .body:                     font = .preferredFont(forTextStyle: .body)
        case .callout:                  font = .preferredFont(forTextStyle: .callout)
        case .footnote:                 font = .preferredFont(forTextStyle: .footnote)
        case .caption:                  font = .preferredFont(forTextStyle: .caption1)
        case .caption2:                 font = .preferredFont(forTextStyle: .caption2)
            
        case .customNameSize:           fatalError("TODO")
        case .customNameSizeStyle:      fatalError("TODO")
        case .customFixedSize:          fatalError("TODO")
        case .systemSizeWeightDesign:
            let descriptor = UIFont.systemFont(ofSize: size!, weight: weight!.toUIKit).fontDescriptor
                .withDesign(design!.toUIKit)!
            font = .init(descriptor: descriptor, size: size!)

        case .systemStyleDesign:
            var descriptor = UIFont.preferredFont(forTextStyle: (style ?? .headline).toUIKit).fontDescriptor
            if let design = design?.toUIKit {
                descriptor = descriptor.withDesign(design) ?? descriptor
            }
            font = .init(descriptor: descriptor, size: size ?? UIFont.systemFontSize)
        }
        
        if _italic {
            font = .init(descriptor: font.fontDescriptor.withSymbolicTraits(.traitItalic)!, size: size!)
        }
        if _smallCaps {
            fatalError("TODO")
        }
        if _lowercaseSmallCaps {
            fatalError("TODO")
        }
        if _uppercaseSmallCaps {
            fatalError("TODO")
        }
        if _monospacedDigit {
            fatalError("TODO")
        }
        if let _weight {
            fatalError("TODO")
        }
        if _bold {
            font = .init(descriptor: font.fontDescriptor.withSymbolicTraits(.traitBold)!, size: size!)
        }
        if _monospaced {
            font = .init(descriptor: font.fontDescriptor.withSymbolicTraits(.traitItalic)!, size: size!)
        }
        if let _leading {
            fatalError("TODO")
        }
        return font
    }
    
    var toSwiftUI: SwiftUI.Font {
        var font: SwiftUI.Font
        switch variant {
        case .largeTitle:               font = .largeTitle
        case .title:                    font = .title
        case .title2:                   font = .title2
        case .title3:                   font = .title3
        case .headline:                 font = .headline
        case .subheadline:              font = .subheadline
        case .body:                     font = .body
        case .callout:                  font = .callout
        case .footnote:                 font = .footnote
        case .caption:                  font = .caption
        case .caption2:                 font = .caption2
            
        case .customNameSize:           font = .custom(name!, size: size!)
        case .customNameSizeStyle:      font = .custom(name!, size: size!, relativeTo: style!.toSwiftUI)
        case .customFixedSize:          font = .custom(name!, fixedSize: fixedSize!)
        case .systemSizeWeightDesign:   font = .system(size: size!, weight: weight!.toSwiftUI, design: design!.toSwiftUI)
        case .systemStyleDesign:        font = .system(style!.toSwiftUI, design: design?.toSwiftUI, weight: weight?.toSwiftUI)
        }
        
        if _italic {
            font = font.italic()
        }
        if _smallCaps {
            font = font.smallCaps()
        }
        if _lowercaseSmallCaps {
            font = font.lowercaseSmallCaps()
        }
        if _uppercaseSmallCaps {
            font = font.uppercaseSmallCaps()
        }
        if _monospacedDigit {
            font = font.monospacedDigit()
        }
        if let _weight {
            font = font.weight(_weight.toSwiftUI)
        }
        if _bold {
            font = font.bold()
        }
        if _monospaced {
            font = font.monospaced()
        }
        if let _leading {
            font = font.leading(_leading.toSwiftUI)
        }
        return font
    }
}

// MARK: MyFont

public struct MyFont: Hashable {
    public let variant: Variant
    public var name: String?
    public var size: CGFloat?
    public var fixedSize: CGFloat?
    public var style: TextStyle?
    public var weight: Weight? // used with system(size:weight:design:)
    public var design: Design?
    
    public var _italic: Bool = false
    public var _smallCaps: Bool = false
    public var _lowercaseSmallCaps: Bool = false
    public var _uppercaseSmallCaps: Bool = false
    public var _monospacedDigit: Bool = false
    public var _bold: Bool = false
    public var _monospaced: Bool = false
    public var _leading: Leading?
    public var _weight: Weight? // used with font.weight(...)
}

// MARK: View's font

private struct FontKey: EnvironmentKey {
    static let defaultValue = MyFont.body
}

extension EnvironmentValues {
    var myFont: MyFont {
        get { self[FontKey.self] }
        set { self[FontKey.self] = newValue }
    }
}

extension View {
    public func myFont(_ font: MyFont) -> some View {
        environment(\.myFont, font)
    }
}

// MARK: View's monospacedDigit

private struct MonospacedDigitKey: EnvironmentKey {
    static let defaultValue = false
}

extension EnvironmentValues {
    var myMonospacedDigit: Bool {
        get { self[MonospacedDigitKey.self] }
        set { self[MonospacedDigitKey.self] = newValue }
    }
}

extension View {
    public func myMonospacedDigit() -> some View {
        environment(\.myMonospacedDigit, true)
    }
}

// MARK: View's monospaced

private struct MonospacedKey: EnvironmentKey {
    static let defaultValue = false
}

extension EnvironmentValues {
    var myMonospaced: Bool {
        get { self[MonospacedKey.self] }
        set { self[MonospacedKey.self] = newValue }
    }
}

extension View {
    public func myMonospaced(isActive: Bool = true) -> some View {
        environment(\.myMonospaced, isActive)
    }
}

// MARK: View's fontWeight

private struct FontWeightKey: EnvironmentKey {
    static let defaultValue: MyFont.Weight? = MyFont.Weight.regular
}

extension EnvironmentValues {
    var myFontWeight: MyFont.Weight? {
        get { self[FontWeightKey.self] }
        set { self[FontWeightKey.self] = newValue }
    }
}

extension View {
    public func myFontWeight(_ weight: MyFont.Weight?) -> some View {
        environment(\.myFontWeight, weight)
    }
}

// MARK: View's bold

private struct BoldKey: EnvironmentKey {
    static let defaultValue: Bool = false
}

extension EnvironmentValues {
    var myBold: Bool {
        get { self[BoldKey.self] }
        set { self[BoldKey.self] = newValue }
    }
}

extension View {
    public func myBold(_ isActive: Bool = true) -> some View {
        environment(\.myBold, isActive)
    }
}

// MARK: View's italic

private struct ItalicKey: EnvironmentKey {
    static let defaultValue: Bool = false
}
extension EnvironmentValues {
    var myItalic: Bool {
        get { self[ItalicKey.self] }
        set { self[ItalicKey.self] = newValue }
    }
}
extension View {
    public func myItalic(_ isActive: Bool = true) -> some View {
        environment(\.myItalic, isActive)
    }
}

// MARK: View's kerning

private struct KerningKey: EnvironmentKey {
    static let defaultValue: CGFloat = 0
}

extension EnvironmentValues {
    var myKerning: CGFloat {
        get { self[KerningKey.self] }
        set { self[KerningKey.self] = newValue }
    }
}

extension View {
    public func myKerning(_ value: CGFloat) -> some View {
        environment(\.myKerning, value)
    }
}

// MARK: View's tracking

private struct TrackingKey: EnvironmentKey {
    static let defaultValue: CGFloat = 0
}

extension EnvironmentValues {
    var myTracking: CGFloat {
        get { self[TrackingKey.self] }
        set { self[TrackingKey.self] = newValue }
    }
}

extension View {
    public func myTracking(_ value: CGFloat) -> some View {
        environment(\.myTracking, value)
    }
}

// MARK: View's baselineOffset

private struct BaselineOffsetKey: EnvironmentKey {
    static let defaultValue: CGFloat = 0
}

extension EnvironmentValues {
    var myBaselineOffset: CGFloat {
        get { self[BaselineOffsetKey.self] }
        set { self[BaselineOffsetKey.self] = newValue }
    }
}

extension View {
    public func myBaselineOffset(_ value: CGFloat) -> some View {
        environment(\.myBaselineOffset, value)
    }
}

// MARK: custom view

class MyUiView: UIView {
    let string = "Hello, World" as NSString
    let defaultFont = UIFont.systemFont(ofSize: 12)
    var font: UIFont? { didSet { setNeedsDisplay() } }

    override func draw(_ rect: CGRect) {
        string.draw(in: bounds, withAttributes: [.font : font ?? defaultFont])
    }
}

struct MyView: UIViewRepresentable {
    func makeUIView(context: Context) -> MyUiView {
        let view = MyUiView()
        view.isOpaque = false
        return view
    }
    func updateUIView(_ uiView: MyUiView, context: Context) {
        uiView.font = context.environment.myFont.toUIKit
    }
}

// MARK: example app

struct ContentView: View {
    var body: some View {
        MyView()
            .myFont(.system(size: 12))
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

@main struct iOSApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

600+ lines of code (and counting) to replicate what SwiftUI is doing to have the equivalent of its getters... I will not show the other version (much shorter but obviously much more dangerous) that reverse engineers SwiftUI internals to to Font internals.


To illustrate my point using another example: when I am writing MyView using any technology of my choice (perhaps it's Qt, or it might be drawing to video memory directly, or anything else in the future) and that view can be "coloured" it would be super handy if I was able to get the current foreground colour information from the environment and be able to use that information to construct the colour appropriate for the technology I am using.

MyView()
    .foregroundColor(....)

First obstacle would be to know the relevant environment key – it is not public. Next obstacle would be to get the required information from the Color object. All these guys have relevant getters: UIColor, NSColor, CGColor, CIColor. But not SwiftUI.Color, and that's the case of pretty much everything in SwiftUI :slightly_frowning_face:

Bigger deal, but not a show stopper - in the next version when recompiling the app compiler will complain about removed API's and I will adjust the code accordingly. We have this mechanism in place when we first deprecate API's and then remove them, so this is not unheard of.