Infer associated type on protocol conformance

I'm attempting to generalise UIViewRepresentable and NSViewRepresentable in my codebase into a single protocol called ViewRepresentable.

The idea being that a struct can conform to ViewRepresentable, and implicitly conform to UIViewRepresentable on iOS, and NSViewRepresentable on macOS. Although this sounds specific to SwiftUI, it is in fact a general problem.

My approach is like so:

#if os(iOS) || os(watchOS) || os(tvOS)
import UIKit

public protocol ViewRepresentable: UIViewRepresentable {
	associatedtype ViewType
	
	@MainActor
	func makeView(context: Context) -> ViewType
	
	@MainActor
	func updateView(_ view: ViewType, context: Context)
}

extension ViewRepresentable {
	@MainActor
	public func makeUIView(context: Context) -> UIViewType {
		makeView(context: context)
	}
	
	@MainActor
	public func updateUIView(_ uiView: UIViewType, context: Context) {
		updateView(uiView, context: context)
	}
}
#elseif os(macOS)
import AppKit

public protocol ViewRepresentable: NSViewRepresentable {
	associatedtype ViewType
	typealias NSViewType = ViewType
	
	@MainActor
	func makeView(context: Context) -> ViewType
	
	@MainActor
	func updateView(_ view: ViewType, context: Context)
}

extension ViewRepresentable {
	@MainActor
	public func makeNSView(context: Context) -> NSViewType {
		makeView(context: context)
	}
	
	@MainActor
	public func updateNSView(_ nsView: NSViewType, context: Context) {
		updateView(nsView, context: context)
	}
}
#endif

I want the struct that conforms to ViewRepresentable to not have to care if they are conforming to UIViewRepresentable or NSViewRepresentable, so they shouldn't have to specify the typealias for NSViewType or UIViewType, instead I want these to be inferred from them simply specifying the value of the ViewType typealias.

For example:

public struct SignInWithApple: ViewRepresentable {
	public typealias ViewType = ASAuthorizationAppleIDButton

	public func makeView(context: Context) -> ASAuthorizationAppleIDButton {
		ASAuthorizationAppleIDButton()
	}
	
	public func updateView(_ view: ASAuthorizationAppleIDButton, context: Context) {
		// . . .
	}
}

At the moment, this example above fails to compile, because either NSViewType or UIViewType has not been specified by the struct SignInWithApple.

My initial approach was to specify the typealias on the protocol or the extension, so something like this (I've removed the code that hasn't changed):

public protocol ViewRepresentable: UIViewRepresentable {
	typealias UIViewType = ViewType

	/// . . . etc
}

public protocol ViewRepresentable: NSViewRepresentable {
	typealias NSViewType = ViewType

	/// . . . etc
}

This gets things compiling, and solves my problem, but I get warnings:

Typealias overriding associated type 'NSViewType' from protocol 'NSViewRepresentable' is better expressed as same-type constraint on the protocol

Typealias overriding associated type 'UIViewType' from protocol 'UIViewRepresentable' is better expressed as same-type constraint on the protocol

I used the fix it, and it made sense:

public protocol ViewRepresentable: UIViewRepresentable where UIViewType == ViewType {
	/// . . . etc
}

public protocol ViewRepresentable: NSViewRepresentable where NSViewType == ViewType {
	/// . . . etc
}

The warning goes away, but the build fails because it expects the SignInWithApple doesn't conform to ViewRepresentable, because it doesn't specify the typealias NSViewType or UIViewType.

What am I doing wrong here? I can get it to build, with warnings, but the fix it for the warning introduces build errors. Is there a warning free fix I'm missing?

Let me restate what I understand to be your question in a simplified way. Given:

protocol P { associatedtype A }
protocol Q: P where B == A { associatedtype B }

You are surprised that defining the following doesn't compile:

struct S: Q { typealias B = Int }
// Error: type 'S' does not conform to protocol 'Q'
// Error: type 'S' does not conform to protocol 'P'

...and nor (for that matter) does this:

struct S: Q { typealias A = Int }
// Error: type 'S' does not conform to protocol 'Q'

You'd like a way for a user to write a struct such as S that conforms to P via a more refined protocol such as Q without having to define the associated type P.A; rather, you'd like to allow P.A to be inferred from the associated type Q.B.

To do this, you can simply restate the associate type requirement A in your more refined protocol Q as an associated type with a default rather than a type alias:

protocol Q: P {
  associatedtype B
  associatedtype A = B
}
struct S: Q { typealias B = Int } // This compiles.

Note that this means something a bit different from what you're attempting to express with a typealias inside the protocol definition: in my version, the end user can actively choose to make the associated types B and A alias different concrete types, as the equivalence is just a default.

However, based on what you've described, I don't see anything that makes that unworkable, and in fact it may be an advantage (if, for example, they want an NS... type to be the associated type on macOS and a UI... type to be the associated type on other platforms).

1 Like

Oh that makes much more sense. I was attempting to specify the type of A as a typelias, but specifying associated type makes sense.

Thanks.

So the final code becomes:

#if os(iOS) || os(watchOS) || os(tvOS)
import UIKit

public protocol ViewRepresentable: UIViewRepresentable where UIViewType == ViewType {
	associatedtype ViewType
	associatedtype UIViewType = ViewType
	
	@MainActor
	func makeView(context: Context) -> ViewType
	
	@MainActor
	func updateView(_ view: ViewType, context: Context)
}

extension ViewRepresentable {
	@MainActor
	public func makeUIView(context: Context) -> UIViewType {
		makeView(context: context)
	}
	
	@MainActor
	public func updateUIView(_ uiView: UIViewType, context: Context) {
		updateView(uiView, context: context)
	}
}
#elseif os(macOS)
import AppKit

public protocol ViewRepresentable: NSViewRepresentable where NSViewType == ViewType {
	associatedtype ViewType
	associatedtype NSViewType = ViewType
	
	@MainActor
	func makeView(context: Context) -> ViewType
	
	@MainActor
	func updateView(_ view: ViewType, context: Context)
}

extension ViewRepresentable {

	@MainActor
	public func makeNSView(context: Context) -> NSViewType {
		makeView(context: context)
	}
	
	@MainActor
	public func updateNSView(_ nsView: NSViewType, context: Context) {
		updateView(nsView, context: context)
	}
}
#endif

By including where UIViewType == ViewType and where NSViewType == ViewType, I can prevent the user from specifying a different value for NSViewType/UIViewType than ViewType.

Thanks again.