Swift Public Protocols with Internal Functions and Properties

I am wondering what is best when I want some functions to be publicly accessible and some internal to the protocol.

I am writing an AudioManager in Swift 3 wrapping AVPlayer as a frame.

I want some methods to be public, so for example, a ViewController using AudioManager can access some methods, but some methods will not be displayed outside the box
β†’ i.e. have access modifier internal instead of public .

I am writing a framework with a protocol design, almost every part should have a protocol.
So the protocols speak with the protocols within. the main class - AudioManager - has AudioPlayer and should be able to call some internal functions on it, for example, pause(reason:) , but this method should be internal and not shown out of the box.

Here is an example.

 internal enum PauseReason { case byUser case routeChange } // Compilation error: `Public protocol cannot refine an internal protocol` public protocol AudioPlayerProtocol: InternalAudioPlayerProtocol { func pause() // I want } internal protocol InternalAudioPlayerProtocol { func pause(reason: PauseReason) // Should only be accessible within the framework } public class AudioPlayer: AudioPlayerProtocol { public func pause() { pause(reason: .byUser) } // This would probably not compile because it is inside a public class... internal func pause(reason: PauseReason) { //I want this to be internal // save reason and to stuff with it later on } } public protocol AudioManagerProtocol { var audioPlayer: AudioPlayerProtocol { get } } public class AudioManager: AudioManagerProtocol { public let audioPlayer: AudioPlayerProtocol init() { audioPlayer = AudioPlayer() NotificationCenter.default.addObserver(self, selector: #selector(handleRouteChange(_:)), name: NSNotification.Name.AVAudioSessionRouteChange, object: nil) } func handleRouteChange(_ notification: Notification) { guard let userInfo = notification.userInfo, let reasonRaw = userInfo[AVAudioSessionRouteChangeReasonKey] as? NSNumber, let reason = AVAudioSessionRouteChangeReason(rawValue: reasonRaw.uintValue) else { print("what could not get route change") } switch reason { case .oldDeviceUnavailable: pauseBecauseOfRouteChange() default: break } } } private extension AudioManager { func pauseBecauseOfRouteChange() { audioPlayer.pause(reason: .routeChange) } } // Outside of Audio framework class PlayerViewController: UIViewController { fileprivate let audioManager: AudioManagerProtocol @IBAction didPressPauseButton(_ sender: UIButton) { // I want the `user of the Audio framwwork` (in this case a ViewController) // to only be able to `see` `pause()` and not `pause(reason:)` audioManager.audioPlayer.pause() } } 

I know that I can make it work by changing the pauseBecauseOfRouteChange method to look like this:

 func pauseBecauseOfRouteChange() { guard let internalPlayer = audioPlayer as? InternalAudioPlayerProtocol else { return } internalPlayer.pause(reason: .routeChange) } 

But I wonder if there is a more elegant solution?
Something like labeling that AudioPlayerProtocol clarifies InternalAudioPlayerProtocol ...

Or how do your fellow programmers do it?
A frame is more beautiful if it does not disclose methods and variables intended for internal use!

Thanks!

+10
source share
3 answers

No, there is no more elegant solution for this, at least when considering protocols, and here's why:

Imagine that someone using your infrastructure wants to write an extension for AudioPlayerProtocol , how then can the pause(reason:) method be implemented if it is internal?

You can achieve this by a simple subclass, and this code really compiles:

 public class AudioPlayer: AudioPlayerProtocol { public func pause() { pause(reason: .byUser) } internal func pause(reason: PauseReason) { } } 

This is not the case with protocols, because you simply cannot guarantee the implementation of an internal function if someone with a public access level wants to use your mixed public / internal protocol.

+2
source

How about whether you split your protocol into internal and public, and then allow the public implementation class to delegate the internal implementation. In this way

 internal protocol InternalAudioPlayerProtocol { func pause(reason: PauseReason) } public protocol AudioPlayerProtocol { func pause() } internal class InternalAudioPlayer: InternalAudioPlayerProtocol { internal func pause(reason: PauseReason) { } } public class AudioPlayer: AudioPlayerProtocol { internal var base: InternalAudioPlayerProtocol internal init(base: InternalAudioPlayerProtocol) { self.base = base } public func pause() { base.pause(reason: .byUser) } } public protocol AudioManagerProtocol { var audioPlayer: AudioPlayerProtocol { get } } public class AudioManager: AudioManagerProtocol { internal let base = InternalAudioPlayer() public let audioPlayer: AudioPlayerProtocol public init() { audioPlayer = AudioPlayer(base: base) } internal func handleSomeNotification() { pauseBecauseOfRouteChange() //amongst other things } internal func pauseBecauseOfRouteChange() { base.pause(reason: .routeChange) } } 
+1
source

This is an old topic, but what can be done is actually the opposite. Instead of publicProtocol extending internalProtocol, there is internalProtocol extending publicProtocol.

 public protocol AudioPlayerProtocol { func pause() // I want } internal protocol InternalAudioPlayerProtocol: AudioPlayerProtocol { func pause(reason: PauseReason) // Should only be accessible within the framework } public class AudioPlayer: InternalAudioPlayerProtocol { public func pause() { pause(reason: .byUser) } internal func pause(reason: PauseReason) { //Do stuff } } 

Then in the dispatcher

 public class AudioManager: AudioManagerProtocol { public let audioPlayer: AudioPlayerProtocol private let intAudioPlayer: InternalAudioPlayerProtocol init() { intAudioPlayer = AudioPlayer() audioPlayer = intAudioPlayer ... } ... private func pauseBecauseOfRouteChange() { intAudioPlayer.pause(reason: .routeChange) } } 
0
source

Source: https://habr.com/ru/post/1257402/


All Articles