【问题标题】:Swift Public protocols with Internal functions and properties具有内部函数和属性的 Swift 公共协议
【发布时间】:2025-12-01 08:30:01
【问题描述】:

我想知道在使用协议时,当我希望一些函数是公开的,而一些函数对我来说是内部的时,最佳实践是什么。

我正在 Swift 3 中写一个 AudioManager strong> 将 AVPlayer 包装为框架。

我希望将一些方法公开,例如使用 AudioManager 的 ViewController 可以访问某些方法,但某些方法不会暴露在框架之外
-> 即具有访问修饰符 internal 而不是 public

我在写具有协议驱动设计的框架,几乎每个部分都应该有一个协议。
因此,协议与框架内的协议进行对话。
例如主类 - AudioManager - 有一个 AudioPlayer,并且应该能够在其上调用一些 internal 函数,
例如pause(reason:) 但该方法应该是 internal 并且不能暴露在框架之外。

这是一个示例。

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()
    }
}

我知道我可以通过将方法 pauseBecauseOfRouteChange 更改为如下所示来使其工作:

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

但我想知道是否有更优雅的解决方案?
类似于标记AudioPlayerProtocol 精炼InternalAudioPlayerProtocol...

或者各位程序员是怎么做的?
如果框架不暴露供内部使用的方法和变量,框架会更漂亮!

谢谢!

【问题讨论】:

    标签: ios swift swift3 access-modifiers swift-protocols


    【解决方案1】:

    不,至少在考虑协议时没有更优雅的解决方案,原因如下:

    想象一个场景,有人使用您的框架想要为AudioPlayerProtocol 编写扩展,那么如果pause(reason:) 方法是内部的,那么如何实现它?

    你可以通过子类化来实现它,这段代码实际上会编译:

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

    对于协议,情况并非如此,因为如果具有公共访问级别的人想要使用您的混合公共/内部协议,您根本无法保证内部功能的实现。

    【讨论】:

      【解决方案2】:

      如果你把你的协议分成内部和公共,然后让公共实现类委托给一个内部实现呢?像这样

      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)
          }
      }
      

      【讨论】:

        【解决方案3】:

        这是一个古老的话题,但实际上可以做的恰恰相反。 代替 publicProtocol 扩展 internalProtocol 有 internalProtocol 扩展 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
            }
        }
        

        然后在管理器中

        public class AudioManager: AudioManagerProtocol {
            public let audioPlayer: AudioPlayerProtocol
            private let intAudioPlayer: InternalAudioPlayerProtocol
        
            init() {
                intAudioPlayer = AudioPlayer()
                audioPlayer = intAudioPlayer
                ...
            }
            ...
            private func pauseBecauseOfRouteChange() {
                intAudioPlayer.pause(reason: .routeChange)
            }
        }
        

        【讨论】: