Function in generic class extension not called

Hello,

I have a generic class and am missing something. I have the following class:

struct TitleContainer<TitleContent, SubtitleContent> {
	let titleContent: TitleContent
	let subtitleContent: SubtitleContent?
	
	init(_ titleContent: TitleContent, _ subtitleContent: SubtitleContent?) {
		self.titleContent = titleContent
		self.subtitleContent = subtitleContent
	}
	
	var description: String {
		"title: “\(titleContent)”\(subtitleContent != nil ? "subtitle: “\(subtitleContent!)”" : "")"
	}
	
	func apply(_ popupItem: LNPopupItem, popupBar: LNPopupBar) {
		fatalError("Unsupported type")
	}
}

extension TitleContainer where TitleContent == String, SubtitleContent == String {
	func apply(_ popupItem: LNPopupItem, popupBar: LNPopupBar) {
		popupItem.title = titleContent
		popupItem.subtitle = subtitleContent
	}
}

@available(iOS 15, *)
extension TitleContainer where TitleContent == AttributedString, SubtitleContent == AttributedString {
	func apply(_ popupItem: LNPopupItem, popupBar: LNPopupBar) {
		popupItem.attributedTitle = titleContent.swiftUIToUIKit
		popupItem.attributedSubtitle = subtitleContent?.swiftUIToUIKit
	}
}

extension TitleContainer where TitleContent: View, SubtitleContent: View {
	func apply(_ popupItem: LNPopupItem, popupBar: LNPopupBar) {
		let subtitleView: AnyView
		if let subtitleContent {
			subtitleView = AnyView(subtitleContent)
		} else {
			subtitleView = AnyView(EmptyView())
		}
		let titleView = TitleContentView(titleView: AnyView(titleContent), subtitleView: subtitleView, popupBar: popupBar)
		popupItem.setValue(LNPopupBarTitleViewAdapter(rootView: titleView).view, forKey: "swiftuiTitleContentView")
	}
}

I have an outer class that uses the above class in the following way:

struct PopupItem<TitleContent, SubtitleContent, ButtonToolbarContent: ToolbarContent>: Identifiable {
	public
	let id: String
	let titleContainer: TitleContainer<TitleContent, SubtitleContent>
	//...
	func apply(_ popupItem: LNPopupItem, popupBar: LNPopupBar) {
		//...
		titleContainer.apply(popupItem, popupBar: popupBar)
		//...
	}
	
	func lnPopupItem(for popupBar: LNPopupBar) -> LNPopupItem {
		let rv = LNPopupItem()
		apply(rv, popupBar: popupBar)
		return rv
	}
}

At runtime, it always goes to the fatalError variant above.

Leaving only the String variant of the apply method, the compiler complaints that

Referencing instance method 'apply(_:popupBar:)' on 'TitleContainer' requires the types 'SubtitleContent' and 'String' be equivalent

But if I put a breakpoint on the fatalError line, in the debugger I see:

(lldb) po type(of: self)
LNPopupUI.TitleContainer<Swift.String, Swift.String>
(lldb) po type(of: self)
LNPopupUI.PopupItem<Swift.String, Swift.String, SwiftUI.TupleToolbarContent<SwiftUI.ToolbarItem<(), SwiftUI.Button<SwiftUI.Text>>>>

So types are correct at runtime, but the compiler is confused.

Any assistance will be appreciated!

Your problem reduces to the following. Static dispatch does not work like dynamic dispatch. You are missing the constrained overloads of PopupItem. Under your design, you do not need the "fatal error" version , because your overloads all have to be explicit.

struct TitleContainer<TitleContent> { }

extension TitleContainer<String> {
  func apply() { }
}

struct PopupItem<TitleContent> {
  let titleContainer: TitleContainer<TitleContent>
}

extension PopupItem<String> {
  func apply() {
    titleContainer.apply()
  }
}

(I don't think a good way to deal with the problem exists in Swift.)

1 Like

Overloading is a fiction created at compile time. To understand the behavior you’re seeing, try renaming the first overload to apply1 and the second overload to apply2. At each call site, you’ll have to pick one or the other explicitly at compile time, there’s no way to call “apply1 or apply2”. This is basically how the compiler implements overloading under the hood.

5 Likes

Thank you for your replies!

That makes sense, but still not clear how to solve my issue. Duplicating the outer PopupItem.appy creates duplicate logic and doesn’t solve the issue, just moves it one layer out.

The way this PopupItem instances travel in the code, it is basically type-erased and at the point the outer apply is called, the compiler would not have the type information. Changing that would require a lot of work.

Is there any way to move this from static dispatch to dynamic dispatch without having to resort to is/as?

For example, how is a SwiftUI.AnyView handled internally? It is still able to resolved to a UIKit view hierarchy, even if diff might be slower. I would like to achieve some kind of AnyPopupItem with a lnPopupItem(for:) function exposed, but not sure how. Thanks

Got the following to work:

class TitleContainer<TitleContent, SubtitleContent> {
	let titleContent: TitleContent
	let subtitleContent: SubtitleContent?
	
	init(_ titleContent: TitleContent, _ subtitleContent: SubtitleContent?) {
		self.titleContent = titleContent
		self.subtitleContent = subtitleContent
	}
	
	var description: String {
		"title: “\(titleContent)”\(subtitleContent != nil ? "subtitle: “\(subtitleContent!)”" : "")"
	}
	
	dynamic
	func apply(_ popupItem: LNPopupItem, popupBar: LNPopupBar) {
		fatalError("Unsupported type")
	}
}

class StringTitleContainer: TitleContainer<String, String> {
	override
	func apply(_ popupItem: LNPopupItem, popupBar: LNPopupBar) {
		popupItem.title = titleContent
		popupItem.subtitle = subtitleContent
	}
}

@available(iOS 15, *)
class AttributedStringTitleContainer: TitleContainer<AttributedString, AttributedString> {
	override
	func apply(_ popupItem: LNPopupItem, popupBar: LNPopupBar) {
		popupItem.attributedTitle = titleContent.swiftUIToUIKit
		popupItem.attributedSubtitle = subtitleContent?.swiftUIToUIKit
	}
}

class ViewTitleContainer<TitleContent: View, SubtitleContent: View>: TitleContainer<TitleContent, SubtitleContent> {
	override
	func apply(_ popupItem: LNPopupItem, popupBar: LNPopupBar) {
		let subtitleView: AnyView
		if let subtitleContent {
			subtitleView = AnyView(subtitleContent)
		} else {
			subtitleView = AnyView(EmptyView())
		}
		let titleView = TitleContentView(titleView: AnyView(titleContent), subtitleView: subtitleView, popupBar: popupBar)
		popupItem.setValue(LNPopupBarTitleViewAdapter(rootView: titleView).view, forKey: "swiftuiTitleContentView")
	}
}

I do have the type information at instance creation, and do have unique initializers for each supported type, so I create the correct instance of the title container there, and eventually when the apply is called, it goes to the correct function override with dynamic dispatch.

That's just abusing subclassing in order to avoid the necessary code generation required to make TitleContainer a protocol though.

OK, but given a TitleContainer protocol and several concrete implementations, what is the practical difference? Witness table vs vtable sounds the same to me. Meanwhile, with inheritance, there is less duplication, as I can share common logic in the base class, whereas that is not really possible with a protocol.

I am happy to learn new (to me) design techniques.

I cannot abide switching from value to reference just to avoid code generation. The Swift ecosystem should make this easier.

Unrelated, why use AnyView instead of ViewBuilder?

My use-case is complex. An instance of the above PopupItem is passed into a SwiftUI hierarchy as an environment value, so type is lost there already. The data in the popup item is then put into an ObjC object which reaches UI (the ObjC framework is 10 years old, and the SwiftUI wrapper is a helper to make it pretty for SwiftUI). So the UIHostingController goes from SwiftUI to ObjC as UIViewController and type information cannot really be kept in the ObjC/Swift barrier.

The initializer is exposed as ViewBuilder, of course.

init(id: String, image: Image? = nil, progress: Float? = nil, @ViewBuilder title: () -> TitleContent, @ViewBuilder subtitle: () -> SubtitleContent = { EmptyView() }, @ToolbarContentBuilder buttons: () -> ButtonToolbarContent = { emptyToolbarItem() })

I don’t like the absolutist approach of your first sentence. Programming languages and concepts within them are tools for making software. The software is the end goal, not necessarily the computer science principles of it, when they are at odds. I understand this might not be very popular on the Swift forum. :rofl: Apologies.

If you look at Apple’s own SwiftUI/[UIKit/AppKit] barriers, there is a lot of AnyView usage. At the end, the two environments are fundamentally incompatible.

You can implement something very similar by mixing protocol requirements, protocol requirements with default implementations, and extensions on the protocol to define things that cannot be overridden.

Can you please provide a code sample? Thanks

How about the following:

protocol Container {
    associatedtype Content
    var content: Content { get }
    func apply()
}

struct PopupItem {
  let titleContainer: any Container
}

extension PopupItem {
  func apply() {
    titleContainer.apply()
  }
}

Usage:

struct StringContainer: Container {
  let content = 1
  func apply() { print("\(content)") }
}

struct IntContainer: Container {
  let content = "abc"
  func apply() { print(content) }
}

let item = PopupItem(titleContainer: IntContainer())
item.apply()
2 Likes

FWIW, this is also sometimes known as Double dispatch - Wikipedia or the Visitor pattern - Wikipedia.

2 Likes

SomeProtocol and SomeClass are roughly analogous. The one difference is that you can define a finalMethod() on a type conforming to SomeProtocol without a warning, but you cannot do so on a subclass of SomeClass.

protocol SomeProtocol {
    func abstractMethod() // no default implementation
    func overridableMethod()
}
extension SomeProtocol {
    func overridableMethod() { ... } // default implementation
    func finalMethod() { ... } // no corresponding protocol requirement
}

class SomeClass {
    // not supported for classes:
    // abstract func abstractMethod()

    func overridableMethod() { ... }
    final func finalMethod() { ... }
}
1 Like

A base class can have shared storage with its subclasses that would need to duplicated for each protocol implementation.

Protocol default implementations seem to have warts/gotchas of their own:

Yep, there are definitely pros and cons to both protocols and classes. So there are definitely cases where one or the other is clearly better, and cases where it is tricky to identify which one to use.

That’s also not a bug. It can be explained in terms of the semantics of overloading just like your original example. Although arguably, overloading is the original wart/gotcha which leads to all this confusion.

2 Likes

I didn’t write it is a bug, I understand the technical reason behind it. It’s still a gotcha, as it is unexpected if you don’t sit to consider some technical implementation detail. If everything was using dynamic dispatch, none of these gotchas would exist. :grin: