SFSafeSymbols / SFSafeSymbols

Safely access Apple's SF Symbols using static typing

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

make `SFSymbol` struct

ddddxxx opened this issue · comments

I believe SFSymbol is naturally a String backed struct, not enum.

public struct SFSymbol: RawRepresentable {
    
    public let rawValue: String
    
    public init(rawValue: String) {
        self.rawValue = rawValue
    }
}

When new symbols were added (like #53), user can do it on their own:

public extension SFSymbol {
    
    @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
    static let sealFill = SFSymbol(rawValue: "seal.fill")
}

We can also break the massive SFSymbol.swift file into smaller fils (v1, v2, v2.1, deprecated).

I believe all existing functions won't be compromised. I can work on a pr with your permission.

As I mention in #66, initializing from rawValue doesn't currently work. This change would solve that issue in allowing you to initialize with any string as a rawValue, but it would hurt the "safety" of the library by always returning a SFSymbol with any raw value, even if it is not a valid symbol.

The current enum has a massive allCases array with every case, so I guess the same thing could be done here with every static instance. When initializing from a raw value, check if it's valid in the allCases array?

@ddddxxx Thanks for your idea and your offer to help!

I only thought about this quickly, but it seems like this is really a much better approach compared to the enum that is currently used:

  • It would fix #66. As @isvvc mentioned, maybe it would be good to adjust the public raw value initializer so that it checks for the existence of the symbol. Of course that would mean that users wouldn't be able to add their own symbols as an extension to the SFSymbol type as you suggested, but the whole point of the SFSymbol type is that it's safe and if anyone could retrieve a SFSymbol by extending the type, it would have the same unsafe character as when using a String directly for the initializer. What do you think? 🤔 Also, I guess an optional raw value initializer isn't possible, so we would need a fatalError in case of non-existence of the provided rawValue in our known strings. To increase performance, we should still use a private raw value initializer for the built-in symbols which circumvents the existence check.
  • It would stop our SFSymbol file from getting larger and larger...
  • Maybe, it's possible to summarize multiple symbols under one @availability check, also reducing code size & complexity:
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
extension SFSymbol {
static let sealFill = SFSymbol(rawValue: "seal.fill")
static let sealFill2 = SFSymbol(rawValue: "seal.fill2")
...
}

(Of course, for deprecations, a per-symbol-@availability statement would still be needed.)

Unfortunately, I currently don't have the time to tackle this large refactoring, but it would be great, if you could help @ddddxxx! If you change something, please make sure that for new versions of SF Symbols the generator project can be used to generate all relevant files. If you have questions about that project (which is quite hacky), feel free to ask! I also recommend that you first test the desired design on a smaller scale by manually coding it before spending much time on the generator project.

I realized the file size is a real issue. The excessively large SFSymbol.swift file freeze Xcode and my git gui app 🤦‍♂️.

@ddddxxx

I realized the file size is a real issue. The excessively large SFSymbol.swift file freeze Xcode and my git gui app 🤦‍♂️.

On my machine, it still works somehow, but further growing the file would definitely break it, too...

but the whole point of the SFSymbol type is that it's safe

I don't think so. SFSymbol doesn't tell you if a symbol name is safe. It tells you if a symbol name is known. A failable initializer will give user false impression that they can validate future symbols, which they can not.

and if anyone could retrieve a SFSymbol by extending the type, it would have the same unsafe character as when using a String directly for the initializer.

I don't think users will use Image(systemSymbol: .init(rawValue: "???")). They should use predefined symbols. and if one extend the type themself, he takes full responsibility.

I still prefer a transparent wrapper, like NSAttributedString.Key. So users won't expect more than what we provide. Validate symbol name dynamically is impossible. They can check known symbol name with SFSymbol.allCases.contains(.mySymbol).

@ddddxxx Sorry for my late reply!

What you said, makes sense to me – a symbol accessible via the SFSymbol type is known, and the one who made it known takes the responsibility that it's safe.

commented

I like the idea of turning SFSymbol into a struct, especially because is allows splitting SFSymbol.swift into multiple files, which in turn allows the user to add their own, custom SFSymbol cases.

Regarding rawValue and initializing via rawValue: Apart from user-extensions to SFSymbol, there is no need for an init(rawValue:). Why would anyone want to create an SFSymbol by its internal string representation? Avoiding this string-based initialization is the whole point of SFSafeSymbols.

Again, I don't think there should be a public init(rawValue:). Even more, I don't believe rawValue is the right name for the property. I'd suggest something like systemSymbolName, as it communicates the purpose of the property much clearer than rawValue.

The question remains how users can create their own SFSymbol instances. There has to be a public initializer to make this possible, but we can give it a name which makes explicit that it should only be used with custom, user-defined SFSymbols.

public struct SFSymbol {
    public let systemSymbolName: String

    internal init(systemSymbolName: String) {
        self.systemSymbolName = systemSymbolName
    }

    /// Create an SFSymbol from your custom, user-defined symbol.
    public init(customName: String) {
        self.systemSymbolName = customName
    }
}

Removing the rawValue property and the expectation that lies upon the relation between rawValue and init(rawValue:) would resemble the actual nature of SFSymbol more closely (and would immediately fix #66), and this specific naming (systemSymbolName) is what I would suggest instead.

What do you think @fredpi @ddddxxx?

@knothed Thanks for your ideas, I agree with you. Such a naming & initializer scheme would indeed solve multiple issues and better fit the new modeling as a struct (in contrast to the previous enum implementation where a rawValue is quite common) 👍

@Stevenmagdy As you are working on this over at #72, maybe you also want to comment and / or possibly implement this approach in your PR?

What about RawRepresentable conformance? It gives us default Codable implementation and much more. @knothed @fredpi

commented

I don't believe there is a need for Codable, but if wanted, Codable could be easily implemented without the conformance to RawRepresentable.

In fact, we explicitly do not want SFSymbol to conform to RawRepresentable as this would suggest some relationship between a rawValue and an init(rawValue:), which is in fact not there.

I agree that we should give up the RawRepresentable because of the initializer, but I think we should keep the rawValue property since it's direct and doesn't suggest if the symbol is custom or not (vs. systemSymbolName), and for source compatibility. @knothed @fredpi

public struct SFSymbol: Equatable, Hashable {
    public let rawValue: String

    internal init(systemName: String) {
        self.rawValue = systemName
    }

    public init(customName: String) {
        self.rawValue = customName
    }
}
commented

This could work. What do you think @fredpi?

@Stevenmagdy @knothed @ddddxxx Yes, the rawValue name is probably better, because

[it] doesn't suggest if the symbol is custom or not

Regarding Codable: If it was possible for people to use the Codable conformance with the previous version (which I'm not sure about, that should be checked), it would be best to add this manually for the new version (should be straightforward as there's only the rawValue property to encode / decode). If it wasn't possible with the previous version, I'm indifferent on whether we should add it.