Implement a public method that rely on a private property in Swift using protocols

Hello! It's my first post here. Hope I'm doing everything right.
Here is the question:
I'm building an API that uses ResultBuilder with structs as components and chaining methods as modifiers.
Sometimes different components have the same modifier, e.g.:

var resultBuilderContent = {
    Component1()
       .modifier(x: 1)
    Component2()
       .modifier(x: 2)
}

I'd like to implement 'modifier' method in a protocol to avoid duplicating the code. But the method relies on internal properties and if I implement it like this:

protocol SameModifierProtocol{
    var x: Int { get set }
}
extension SameModifierProtocol{
    public func modifier(x: Int)->SameModifierProtocol{
        var s = self
        s.x = x
        return s
    }
}
public struct Component: SameModifierProtocol{
    var x: Int = 0
    public init(){}
}
// in another module
let c = Component().modifier(x: 1)

I get the error: "'modifier' is inaccessible due to 'internal' protection level".

If I try to differentiate access levels between two protocols like this:

protocol SameModifierProtocol{
    var x: Int { get set }
}

public protocol ReceivingSameModifier{
}
extension ReceivingSameModifier where Self: SameModifierProtocol{
    public func modifier(x: Int)->ReceivingSameModifier{
        var s = self
        s.x = x
        return s
    }
}

I get the following error: "Cannot declare a public instance method in an extension with internal requirements".

This is where I stuck. What are my options?

I could easily implement this method in a superclass and then subclass my Components from it. But I wanted to mimick SwiftUI and used structs, and here's the rub.

That seems a typical case where SPI can help. Try

public protocol SameModifierProtocol {
    @_spi(SameModifier) var x: Int { get set }
}

extension SameModifierProtocol {
    public func modifier(x: Int) -> Self {
        var s = self
        s.x = x
        return s
    }
}

public struct Component: SameModifierProtocol {
    @_spi(SameModifier) public var x: Int = 0
    public init() {}
}

And you can get

Should mention that SPI isn’t an officially adopted feature, nor is guaranteed to be stable. But it’s useful for hiding implementation details, and callers can’t see the detail unless they specify the SPI name on import declarations.

2 Likes

@stevapple wow! Thanks a lot! I didn't know about this feature!

1 Like

The standard solution is as follows:

/// The publically visible capabilities.
public protocol SameModifierProtocol {
  func modifier(x: Int) -> SameModifierProtocol
}
/// The internal requirements on which the default implementation relies.
internal protocol SynthesizedSameModifierProtocolConformance:
  SameModifierProtocol {
    var x: Int { get set }
}
/// The default implementation.
extension SynthesizedSameModifierProtocolConformance {
    public func modifier(x: Int) -> SameModifierProtocol{
        var s = self
        s.x = x
        return s
    }
}

/// Conforms to the public protocol
/// and requests the default implementation from the internal one.
/// Clients can only see the public protocol.
public struct Component: SynthesizedSameModifierProtocolConformance {
    internal var x: Int = 0
    public init() {}
}
let c = Component().modifier(x: 1) // ✓
5 Likes

This solution indeed works! Thank you!