The Resurgence of Classes in Swift: Why We Need virtual, abstract, and protected

Hello.

Over the years, the Swift community has gone through an incredible evolution. In the early days, many of us believed that classes might one day be replaced entirely by structs and protocols. However, as the language has matured, it’s become clear that classes still play a vital role in modern Swift development. From powerful features like @Observable and frameworks like SwiftData, classes remain integral to building robust, flexible applications.

Today, I’d like to propose one more time a way to further empower class-based development in Swift by introducing the keywords virtual, abstract, and protected. These concepts are familiar to developers from other object-oriented languages, and I believe they could add significant value to the Swift language.

Introduction

In the early days of Swift, there was a widespread belief that classes were on their way to being replaced by structs and protocols. This idea gained momentum due to Swift’s emphasis on safety, immutability, and avoiding the traditional pitfalls of reference types—like memory management issues associated with ARC. The notion of using value types (structs) and behavior contracts (protocols) to model most types of behavior seemed like a promising path, and many developers shifted their focus towards a more protocol-oriented design.

However, as the Swift ecosystem has matured, we’ve come to realize that classes are not only still relevant, but are indispensable in many modern Swift applications. Core features of Apple’s platforms, such as @Observable, Core Data, and SwiftData, are built upon classes and rely on class inheritance to function effectively. Classes remain critical when it comes to reference semantics, shared state, and the flexibility that comes with subclassing.

Given this, I propose we revisit some of the core concepts surrounding inheritance and encapsulation by introducing three important object-oriented features that are common in many other languages: virtual, abstract, and protected.

The Case for virtual

In object-oriented programming, the virtual keyword explicitly marks a method as one that can be overridden in a subclass. This would provide a clear signal to developers that a method is intended to be overridden in subclasses, as opposed to simply relying on inheritance by default, which can sometimes lead to misunderstandings or unintended behaviors.

Practical Examples in Apple SDKs:

  • viewWillAppear(_:) in UIViewController (UIKit): This method is commonly overridden to customize behavior just before a view appears on screen. With a virtual keyword, the intention to allow overriding would be explicit, helping to differentiate between methods that are designed to be customized and those that are not.
  • layoutSubviews() in UIView (UIKit): This is another common override, where subclasses need to define their own layout logic. Marking this method as virtual would make it clear that subclassing for layout customization is expected.

The Case for abstract

An abstract class or method is one that cannot be instantiated directly and must be overridden or completed by a subclass. Swift currently lacks a formal way to declare such intent. Abstract classes or methods would provide a powerful tool for API designers to create more explicit and structured inheritance hierarchies.

In Swift today, this concept is often mimicked by creating protocols or by providing default implementations that raise fatal errors. While these are workarounds, they lack the clarity and expressiveness of an abstract keyword.

Practical Examples in Apple SDKs:

  • UITableViewDataSource and UITableViewDelegate (UIKit): These protocols require implementers to define certain methods, such as tableView(_:cellForRowAt:). Although Swift uses protocols here, the concept of abstract methods in classes would allow similar patterns to be applied directly within class hierarchies, creating a clearer distinction between methods that must be implemented by subclasses.
  • NSDocument (AppKit): In macOS development, NSDocument is often subclassed, and methods like write(to:ofType:) need to be customized. These methods could be abstract, signaling that any subclass of NSDocument must provide its own logic for data serialization.

By making certain classes and methods explicitly abstract, we can help developers avoid confusion and provide a clearer intent about which parts of an API are extendable and which are meant to be customized.

The Case for protected

Protected methods and properties can be overridden or accessed by subclasses but not by instances of those subclasses or other external code. Swift currently lacks a formal protected access control level, which would improve encapsulation, especially when designing large frameworks or complex inheritance hierarchies. This access control level exists in languages like Java and C#, offering flexibility without exposing too much of a class’s internal workings.

Practical Examples in Apple SDKs:

  • layoutSubviews() in UIView (UIKit): This method is frequently overridden in subclasses but shouldn’t be called directly by users of those subclasses. If it were marked protected, it would prevent developers from misusing it outside of the context where it was intended to be overridden.
  • draw(_:) in UIView and NSView: Custom drawing logic is often provided by overriding draw(_:), but it’s not meant to be called directly from outside the class. protected would allow subclass customization while preventing misuse by external code.
  • touchesBegan(_:with:) in UIResponder (UIKit): Designed for subclassing to handle touch events, this method would benefit from being marked as protected. It’s part of the event handling chain, but it shouldn’t be called by external code directly.

Why Now?

As the Swift community has grown, so has our understanding of the balance between different programming paradigms. Protocols and structs are immensely powerful tools, but there are many areas—especially in Apple’s own frameworks—where classes are indispensable.

Given the evolution of Swift and the expanding use cases for Swift on different platforms (iOS, macOS, server-side Swift, etc.), now is the time to take another opportunity to enrich Swift’s class model by introducing these core object-oriented features: virtual, abstract, and protected. Provide explicitness, clarity, and control that are sorely needed for designing robust, flexible, and maintainable APIs.

Swift has always been about clarity and safety, and introducing these keywords would continue to push the language forward in those areas. We would retain all of the language’s current strengths while embracing the full power of classes in areas where they shine.

In Summary

  • virtual: Gives developers an explicit way to mark methods that are intended to be overridden in subclasses, avoiding confusion and making inheritance more predictable.
  • abstract: Clearly defines classes and methods that cannot be instantiated or used directly, but must be implemented by subclasses.
  • protected: Offers better encapsulation, allowing methods and properties to be overridden or accessed by subclasses but not by external code.

This post aimed to present a comprehensive argument that connects Swift’s historical background with practical examples from Apple’s SDKs. Its main goal is to highlight the enduring relevance of classes in contemporary software development and propose improvements to Swift’s inheritance model by incorporating these features.

Thank you for taking the time to consider these ideas. I truly believe that virtual, abstract, and protected can enhance Swift’s class model while maintaining the language’s principles of clarity and safety. I’d love to hear your thoughts, feedback, and any additional ideas you might have on how we can improve Swift together.

Let’s continue to push the language forward, balancing the strengths of protocols, structs, and classes in the Swift ecosystem!

Warm regards,
-Van

8 Likes

Without offering an opinion one way or another (I refuse to open this can of worms again :joy:), the longstanding debate about protected basically comes down to what each person views access control modifiers as accomplishing.

Library developers tend to view access control as primarily a tool for controlling their "API surface area", that is, the set of things that clients can depend on and that therefore must be supported indefinitely into the future.

App developers very often view access control as primarily an organizational tool for their code (which makes sense, since apps don't have public APIs).

Swift in general has leaned towards the library view of the world, and protected, from that viewpoint, is exactly the same as public.

This is perhaps the most heavily discussed area of Swift, historically.

18 Likes

This will improve the ergonomics significantly when working with classes.

For example:

// The status quo

class B {
   func f () {
      fatalError ("sub classes must override")
   }

   func g () {...}
}

class C1: B {
}

class C2: B {
   func g () {...}
}
// If we had virtual/pure virtual functions

class B {
   // pure virtual function
   virtual func f ()

   virtual func g () {...}
}

class C1: B {
   // Error: pure virtual func f () not implemented
}

class C2 : B {
   override func g () {...}
   // Error: pure virtual func f () not implemented
}
1 Like

Unless your app is a single module, app developers actually care about both. They may only need to organize their app module, but certainly care about proper API surface in their other modules. And protected (typeprivate) is equally applicable to both cases, as even framework developers need to organize their internals.

I'm certainly a fan of a protected equivalent, but I think the Swift version would be more like typeprivate, which can be applied to more than classes.

12 Likes

Without getting into the discussion of why inheritance is such an awful tool, can't you achieve the same with protocols:

protocol B: AnyObject {
  func f()
  func g()
}
extension B {
  func g {
    // ...
  }
}

class C1: B {
  // Error: need to implement f()
}

class C2: B {
  func g() {
    // ...
  }
  // Error: need to implement f()
}
8 Likes

UIKit APIs (which represent in your examples the majority of cases) are mostly bridged from ObjC and more likely won’t end up benefiting from these modifier at all. At least, this is a speculation from the outside perspective.

Plus you have to account that if these kind of access modifiers was needed, they more likely has been added a long time ago, so it is also hard to justify a design of a framework that is a black box for many of us.

IMO the missing point here that Swift isn’t trying to encourage hierarchies of classes and complex relationships within them. Instead, it offers — and continues to expand this toolkit — to approach problems in a different way.

For instance, virtual is the most useful to declare an equivalent of protocol in C++. Mixing it with some implementations to get an abstract class in most cases is more likely a sign that type will benefit from decomposition. There are other designs that can allow customization without the need of inheritance and override — e.g. SwiftUI took another approach and there is no need for inheritance, even for layout.

I’d rather see this not as a limitation, but as a possibility to explore different ways to achieve desired behavior, and quite often they are better than inheritance. Yes, we have some systems that already work in that way, but there is no sign (apart from technical details) that these systems are willing to adopt any of these — I would say — complications.

C++ has offered not the best model of OOP (Alan Kay particularly disagree with this [1]). And many modern languages actually tend to abandon this legacy and either avoid OOP at all or support only small subset of what is available in C++.


And a few more comments on concrete cases:

If we ignore that this is an ObjC code we are dealing with, Swift already has a tools to indicate this — final and open. You can define method as final to disallow overrides whatsoever, or define it as open in a library to indicate that it can be customized.

If we snap out of inheritance in design, the layout customization is much better done as a separate type that then passed to the view. This allows to avoid inheritance, better reuse layouts, and offload views API — exactly what is done in SwiftUI btw.


  1. “I made up the term object-oriented, and I can tell you I did not have C++ in mind.” ↩︎

3 Likes

No. A base class can hold state, but a protocol to replace the base class can't.

I have experienced this: the lack of virtual/pure virtual functions in Swift really makes it painful to port C++ based real-life code to Swift.

3 Likes

Honestly, I think the big thing missing from protocol conformance on classes is an easy way to provide default conformances that can be overridden by subclasses.

Right now, a default implementation of a protocol requirement on a base class can't be overridden by a subclass, so you either need to implement that default separately on each base class (made a bit easier with a macro), or define a defaultF() in an extension and call that in each subclass.

As far as I know, the compiler has all the machinery it needs to add a default method to the vtable of the base class, we just don't have a spelling for it. Thankfully, we already have a perfectly good contextual keyword that we can use here: dynamic. Right now it's only allowed on @objc declarations to make them go through Objective-C's dynamic dispatch (message lookup via selector, etc), but having it make a default implementation of a requirement always go through dynamic lookup is a natural extension.

As for the virtual base class problem, that's already solvable (assuming you don't need multiple inheritance) by having a base class with the stored members you need and then making a protocol that requires conformance to that base class. The pure virtual methods are requirements of the protocol, and the rest are on the base class.

protocol P: Base {
  func f()
}

class Base {
  var stored: Int = 42
  func g() {
    print("Default implementation of g()")
  }
}

class C1: Base, P {
  // Error: need to implement f()
}

class C2: Base, P {
  override func g() {
    print("Overridden implementation of g()")
  }
  // Error: need to implement f()
}
2 Likes

The thing about that is you then have a requirement that all subclasses inherit from both Base and P, so how do you enforce that?

The compiler enforces it. It literally will not let you conform to P without also subclassing Base.

Yes, but it will let you subclass Base without conforming to P, which isn't desirable.

Yes, but if your API's only take some/any P then it won't matter if there's a random non-P sublcass of Base.

3 Likes

That's assuming that the other uses of Base are written according to its intention. What we're missing is the ability to declare that intention to the compiler, which is essentially what abstract class would provide.

You could also just have P, and use a macro to add the stored properties and the default method implementations.

2 Likes

Instead of an abstract base class, you might want to consider a class that is generic over a protocol conformance. Then the protocol defines the abstract operations, and the implementation -- which can be a struct, so its stored inline in the class instance -- can have its own storage for implementing those operations, too:

protocol P {
  func customizationPoint(_: C<Self>, _: Int)
}

class C<T: P> {
  private var t: T
  var y: Int

  init(t: T) { self.t = t; self.y = 0 }

  func doSomething(_ x: Int) {
    t.customizationPoint(self, x)    
  }
}

struct Impl: P {
  var storage: Bool = false

  func customizationPoint(_ c: C<Self>, _ x: Int) {
    if storage { c.y = x }
  }
}

You can combine this with inheritance:

class D : C<Impl> { ... more stuff here ... }

Or use a typealias to refer to C<Impl>.

This of course generalizes to more complex combinations where your class is customized along multiple orthogonal axes, and in fact if you don't need the subtyping aspect of inheritance, this can replace inheritance entirely as a way of composing classes.

31 Likes

When I started Swift (from version 3.0), having used a lot of C++, I regularly wanted class inheritance and virtual classes/members, but after giving protocols a chance I found that for almost all cases the protocol approach was nicer and more powerful in many ways. It requires a different mental image of how types are related even if the practical upshot is the same.

What I have found over time is that I only ever feel the need to use class inheritance is when I want some generic functions over a set of related types and would otherwise hit the problem of protocols not being able to conform to themselves - I can consider a base-class as a kind of pseudo-protocol to which even "generic" variables of that "protocol" type will "conform". It's horrible.

2 Likes

As someone who came from a Java background, it took me a while to embrace the potential of protocols. There days I generally try to utilise them and avoid classes and inheritance if possible.

However, a lot of the arguments saying “use protocols instead” are missing one key point: class inheritance does exist in Swift, and since it exists, I think it should at least make the best version of that available for times when it is actually used.

Regarding the specific items pitched above, here's my take:

  1. virtual. This one I would vote against, since as noted elsewhere, final and open already exist to address the "please override me" aspect. And I would prefer abstract for pure signature definitions over virtual. Also, I've never liked the term, since it doesn't really communicate clearly what the keywords actually does.
  2. abstract. This, I think we need for classes, along with allowing abstract method/get/set and possibly even constructor definitions to be defined only on abstract classes. These would allow abstract parent classes to define protocol-like method signatures that subclasses must implement.
  3. protected. Also important, as noted by Jon above. I guess typeprivate is clearer in some ways, although fileprivate as a term has always grated on me, purely from an English language standpoint.
6 Likes

I agree with @vns at least on a point.
Coming from the swift approach I designed a UI library on the past year, written both in swift (swiftUI) and in kotlin (JPK).
As I'm all about to use structs/protocols the most I can, I had need to use some classes/protocols for sharing reasons (most been singletons), but only one class/subclass system in all my 100 entities library.
On the kotlin side, I used the same organisation, classes/interfaces — with a little burden to maintain "value type" behavior when I used structs on swift side... other debate. The fact is I never used an other architecture than the one used in swift, even though kotlin offers "virtual", "abstract" and "protected".
And on the side of understandingness for Library users, the intention is very clear. No subclassing to do anywhere, except the only case I mentioned. So you want to push your stuff in that machine: conform to protocol / implement the interface and both behave as existential types.