Swift Public protocols with Internal functions and properties
Asked Answered
P

3

14

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!

Pissed answered 28/9, 2016 at 14:53 Comment(0)
A
3

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)
    }
}
Apraxia answered 19/5, 2019 at 22:36 Comment(0)
B
2

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.

Blim answered 22/4, 2017 at 18:50 Comment(0)
F
2

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)
    }
}
Forborne answered 28/9, 2017 at 22:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.