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
.
Hi @dankinsold,
This forum is focused on discussions about the Swift language itself and the core libraries; Apple has asked than any feedback about its proprietary libraries such as SwiftUI and UIKit be conducted over on the Apple Developer Forums. Thanks!
Thanks, I will keep it in mind
I thought this section of the forum was for community discussions and didn't count on Apple developers' answers
This sounds to me like a more general question rather than something that is necessarily Apple-specific.
A library's public interface represents a sort of contract, guaranteeing certain behaviours which clients can depend on. It is considered good engineering practice to limit the scope of that public interface to only those features which the developers intend to promise as stable.
Of course, that public interface is implemented in some way, but those are considered implementation details and it is good practice to keep those details non-public. This gives library developers the freedom they need to evolve their libraries - to add new features, fix bugs, improve performance, etc., without breaking existing clients.
This is an interesting general question. You illustrate it with SwiftUI examples, but it can apply to any kind of api.
Not making apis public makes life easier:
- Each new public api must be supported in the future - for a very long time if the library aims at good backward compatibility. But sometimes the library author knows very well that the internal api you're drooling for is a temporary implementation that will be removed. And some others don't know yet.
- Each new public api is subject to the Hyrum's Law, so avoiding user frustration takes a good amount of energy.
- There is another public api that does what you want (but you just did not discover it yet) and the author does not want to expose duplicate/redundant apis.
I can feel your pain, it's overly opaque. However this is the way it is and I don't see it's going to change.
I wonder if it is even possible for a public API to be "overly opaque".
It's a bit of a Gandalf argument, but one could say that it is precisely as opaque as its authors intend it to be.
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.
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.
I conditionally distinguish two ways of using libraries.
- Use in the final product, default way.
- 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
.
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.
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):
- get the underlying font settings (all of the parameters)
- construct the corresponding UIFont or CGFont or CTFont
- 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:
- a happy Apple developer (who is now free to mess with the getters as (s)he pleases as they are opaque
- an unhappy third party developer(s) spending way too much time reverse engineering and/or reimplementing the thing.
- an unhappy user(s) experiencing the problem by way of app crashes.
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 .
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...
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.
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.
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.
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.
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.