Swift Public protocols with Internal functions and properties

Sajjon picture Sajjon · Sep 28, 2016 · Viewed 8.4k times · Source

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!

Answer

Wladek Surala picture Wladek Surala · Apr 22, 2017

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

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

You can achieve it by just subclassing and this code actually will compile:

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

    internal func pause(reason: PauseReason) {
    }
}

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