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!
A protocol can require any conforming type to provide an instance property or type property with a particular name and type. The protocol doesn't specify whether the property should be a stored property or a computed property—it only specifies the required property name and type.
fileprivate — Can only be accessed within the same file. internal — This is the default access control and can be accessed within the same module freely. public — Class can be accessed across different modules (app module and 3rd party library) but cannot be subclassed and its content cannot be overridden.
The swift internal is default access and allows use of a function or property from any source file within the defining module but not from outside the module. So all function and properties within your app that not marked with a compiler keyword is by default marked as internal.
Protocols are used to define a “blueprint of methods, properties, and other requirements that suit a particular task or piece of functionality.” Swift checks for protocol conformity issues at compile-time, allowing developers to discover some fatal bugs in the code even before running the program.
It's an old topic but what one can do is actually the opposite. Instead of publicProtocol extending internalProtocol have 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 manager
public class AudioManager: AudioManagerProtocol {
public let audioPlayer: AudioPlayerProtocol
private let intAudioPlayer: InternalAudioPlayerProtocol
init() {
intAudioPlayer = AudioPlayer()
audioPlayer = intAudioPlayer
...
}
...
private func pauseBecauseOfRouteChange() {
intAudioPlayer.pause(reason: .routeChange)
}
}
How about if you split your protocol into internal and public and then let the public implementation class delegate to an internal implementation. Like so
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)
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With