Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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!

like image 286
Sajjon Avatar asked Sep 28 '16 14:09

Sajjon


People also ask

Can Swift protocols have properties?

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.

What is difference between internal and public in Swift?

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.

What is internal func in Swift?

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.

What are protocols used for in Swift?

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.


2 Answers

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)
    }
}
like image 30
La pieuvre Avatar answered Oct 21 '22 21:10

La pieuvre


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)
    }
}
like image 97
user3763801 Avatar answered Oct 21 '22 21:10

user3763801