Why so many API is internal in native libraries?

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.