Categories
access-modifiers ios swift swift-protocols swift3

Swift Public protocols with Internal functions and properties

I am wondering what the best practice is when I want some functions to be public and some to me internal when working with protocols.

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

I want some methods to be public, so that e.g. a ViewController making use of the AudioManager can access some methods, but some methods would not be exposed outside the framework
-> i.e. having the access modifier internal instead of public.

I am writing the framework with protocol driven design, almost every part should have a protocol.
So protocols are talking to protocols within the framework.
E.g. the main class – AudioManager – has an AudioPlayer, and should be able to call some internal functions on it,
e.g. pause(reason:) but that method should be internal and not exposed outside the framework.

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 I can get it to work by changing the method pauseBecauseOfRouteChange to look like this:

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

But I am wondering if there is a more elegant solution?
Something like marking that the AudioPlayerProtocol refines the InternalAudioPlayerProtocol

Or how do you fellow programmers do it?
The framework is more beautiful if it does not expose methods and variables that are intended for internal use!

Thanks!