Constraints on Public Classes with Internal Members
A public class may freely contain internal properties, methods, and initializers. Those members are visible only inside the defining module; external consumers see only the public interface. This is a deliberate design pattern for hiding implementation details while still exposing the type itself.
Confirmed rules
- External instantiation: A
public class with only internal initializers cannot be directly constructed by downstream modules. You must provide at least one public initializer or a public factory method.
- Subclassing:
public classes are not subclassable outside their module. Only open classes permit external subclassing.
- Overriding: Even inside the module, a
public method on a public class cannot be overridden externally unless both the class and the member are marked open.
- ABI visibility:
internal members do not appear in the public .swiftinterface and are not part of the public ABI surface.
- Layout impact: Stored
internal properties still affect the class's memory layout. Adding, removing, or reordering them is a binary-breaking change for resilient (non-@frozen) frameworks unless you bump the major version.
Practical implication
Use internal members freely for implementation detail. If you need to expose a constructor or allow a specific override point, promote only that member to public (or open). Consider @usableFromInline for internal helpers that must be inlinable across modules without becoming public API.
When open Introduces Stability Risk vs public
The open modifier is a contractual commitment to downstream modules. It introduces risk in these scenarios:
- Method override contracts: Any
open method can be overridden. Changing its signature, removing it, or altering its call semantics (e.g., whether it calls super) breaks existing subclasses.
- Call ordering dependencies: External subclasses may rely on the exact sequence of
super calls or side effects in open methods. Refactoring internal logic becomes hazardous.
- Protected internal behavior leakage: If an
open method calls internal helpers, subclasses indirectly depend on those helpers' behavior. Changing the helpers becomes a breaking change.
- Resilient library overhead: In Swift frameworks (resilient by default),
open classes use dynamic dispatch and emit additional metadata, adding runtime cost on hot paths.
- Versioning rigidity: Switching between
public and open is both source- and binary-breaking. The decision must be made at framework design time.
Rule of thumb: Default to public (non-open) for classes. Use open only when you explicitly intend to support external subclassing and are prepared to maintain the override contract across major versions.
Verification Checklist
- Build a two-target Swift package (framework + executable). Confirm the compiler rejects external access to
internal members and external subclassing of public classes.
- Run
swiftc -emit-module-interface on the framework target; verify internal symbols are absent from the .swiftinterface.
- If you plan to evolve internal stored properties, decide whether the class should be
@frozen (no future stored properties allowed) or resilient (major version bump required for layout changes).
One Diagnostic Detail
Are you building a resilient framework (the default for SwiftPM/Xcode frameworks) or a @frozen library? The answer changes whether adding/removing internal stored properties is a binary-breaking change.