Search code examples
objective-cswiftdesign-patternsenumsprotocols

Using Generic swift enum in a protocol


I have case where I often scratch my head around, let's say I have a generic Manager class in a pod that can handle permission, and within the app, I want to be able to extend it to create more meaningful method name, aka to use with enum as parameter, to make its use more clear and less prone to mistake.

But it seems that you can't call private method when you create the extension elsewhere.

I'm sure there would be a more clean way with Generic/AssociatedValue or maybe my pattern is just wrong...

Here's the simplified version of it:

Class in the external pod:

public class FeatureDataManager {

    public static let shared = FeatureDataManager()

    private var permissionManager: PermissionManager!

    private init() {
        self.permissionManager = PermissionManager()
    }

    private getPermission(forFeature feature: String) -> Bool {
        return self.permissionManager.isEnable(feature)
    }
}

and the extension in the app:

extension FeatureDataManager {
    enum FeatureType: String {
        case ads = "ads"
        case showBanner = "banner"
        case showFullScreenPub = "showFullScreenPub"
    }

    public func isPermissionEnable(forFeature feature: FeatureType) {
        // Does not compile, visibility issue
        self.getPermission(forFeature: feature.rawValue)
    }
}

Clarification:

FeatureDataManager is a class in the Pod that is solely used to check for permissions in the form of String value across many app that are using importing it.

I wanted each single app using it, to define an extension that would have their own finite enum of their supported permissions. Let's say App A support Ads, but not App B. So I wanted to have a universal method that when you call featureManager.isPermissionEnable(.Ads), whenever app that is, the auto-complete would just offer the list of the supported permission for that app. Also, the goal of wrapping my string permission value into an enum is to be more bulletproof to mistake and easier refactoring if a name change, just have to change it in a single place.


Solution

  • What you're looking for would be a "protected" level, and that doesn't exist in Swift, and can't exist without creating a new protection level, since it would break compiler optimizations. In your example, since getPermission(forFeature:) is promised never to be called outside this scope, the compiler is free to inline it. So this function may not even exist at the point that your extension wants to call it.

    It would be possible for Swift to add a "protected" level that is "semi-public," but Swift does not have any such feature. You will need to redesign FeatureDataManager to make this possible. From your example, it's not obvious how to do that, because you provide no public interface for permissions at all, so it's not clear what you mean by "I want to be able to extend it to create more meaningful method name." Currently there is no public method name. If there were one, then making a more convenient syntax like you describe would be easy.

    Can you give an example of the calling code that you want this extension to improve?

    For more on why the language is this way, see Access Control and protected. It's not an accident.

    You note that you can do this in the same file, and that's true. Swift allows this for stylistic reasons (many people use extensions inside a single file for code organization reasons). Swift treats all extensions in the same file as being in the main definition. But that does not extend to other files, and certainly not to other modules.


    The generic solution to this looks like:

    public class FeatureDataManager<Feature>
    where Feature: RawRepresentable, Feature.RawValue == String {
    
        private func getPermission(forFeature feature: String) -> Bool { ... }
    
        public func isPermissionEnable(forFeature feature: Feature) {
            self.getPermission(forFeature: feature.rawValue)
        }   
    }
    

    An App would then create a feature set and create a manager for that feature set:

    enum AppFeature: String {
        case ads = "ads"
        case showBanner = "banner"
        case showFullScreenPub = "showFullScreenPub"
    }
    
    let featureDataManager = FeatureDataManager<AppFeature>()
    featureDataManager.isPermissionEnable(forFeature: .ads)
    

    That does prevent the easy creation of a .shared instance. It's arguable whether that's good or bad, but on the assumption that you want it, you would need to wrap it up:

    class AppFeatureDataManager {
        enum Feature: String {
            case ads = "ads"
            case showBanner = "banner"
            case showFullScreenPub = "showFullScreenPub"
        }
    
        static var shared = AppFeatureDataManager()
    
        let manager = FeatureDataManager<Feature>()
    
        public func isPermissionEnable(forFeature feature: Feature) {
            manager.isPermissionEnable(forFeature: feature)
        }
    }
    

    Now that's a bit too much boiler-plate for the app side (especially if there are more methods than isPermissionEnable), so you can remove the boilerplate this way (full code):

    public class FeatureDataManager<Feature>
    where Feature: RawRepresentable, Feature.RawValue == String {
    
        private var permissionManager: PermissionManager
    
        init() {
            self.permissionManager = PermissionManager()
        }
    
        private func getPermission(forFeature feature: String) -> Bool {
            self.permissionManager.isEnable(feature)
        }
    
        public func isPermissionEnable(forFeature feature: Feature) {
            self.getPermission(forFeature: feature.rawValue)
        }
    }
    
    protocol AppFeatureDataManager {
        associatedtype Feature: RawRepresentable where Feature.RawValue == String
        var manager: FeatureDataManager<Feature> { get }
    }
    
    // Here you can write any necessary pass-through functions so the app doesn't have to
    extension AppFeatureDataManager {
        public func isPermissionEnable(forFeature feature: Feature) {
            manager.isPermissionEnable(forFeature: feature)
        }
    }
    
    //
    // Application Developer writes this:
    //
    class MyGreatAppFeatureDataManager {
        enum Feature: String {
            case ads = "ads"
            case showBanner = "banner"
            case showFullScreenPub = "showFullScreenPub"
        }
    
        // This is the only thing that's really required
        let manager = FeatureDataManager<Feature>()
    
        // They're free make this a shared instance or not as they like.
        // That's not something the framework cares about.
        static var shared = MyGreatAppFeatureDataManager()
        private init() {}
    }
    

    All that said, I think this is getting too many layers if FeatureDataManager is really just a front-end for PermissionManager like you've described here. (Maybe your example is highly simplified, so the below doesn't apply.)

    If PermissionManager is public, and the real goal is just to have a nicer front-end to it, I would write it this way:

    protocol FeatureDataManager {
        associatedtype Feature: RawRepresentable where Feature.RawValue == String
        var permissionManager: PermissionManager { get }
    }
    
    extension FeatureDataManager {
        func isPermissionEnable(forFeature feature: Feature) {
            permissionManager.isEnable(feature.rawValue)
        }
    }
    
    //
    // App developer writes this
    //
    class MyGreatAppFeatureDataManager: FeatureDataManager {
        enum Feature: String {
            case ads = "ads"
            case showBanner = "banner"
            case showFullScreenPub = "showFullScreenPub"
        }
    
        // This is the only required boilerplate; the protocol can't do this for you.
        let permissionManager = PermissionManager()
    
        // And the developer can decide to make it a shared instance if they like,
        // but it's not the business of the framework
        static let shared = MyGreatAppFeatureDataManager()
        private init() {}
    }