Search code examples
swiftgenericsswiftuinsview

Is there a way in Swift to partially match a generic?


That is, if I have a class C that takes two generics A and B, is there a way where I can cast an object to C where I don't care what B is?

My specific use case is that I need to bridge between NSView functionality and the new SwiftUI in a multi-window, but non-document based application. The problem I am having is, given an NSView, I need to obtain the SwiftUI View that it is managing (in my case a View called ContentView).

Note that I do have a solution, which I include below, but it involves the use of Mirror based reflection and I am wondering if there is a better way, most likely involving the use of as? to cast to a partial match of a generic.

The bridging is done using the NSHostingView hence it should seem that one would just do the following:

if let hostingView = NSApplication.shared.keyWindow?.contentView as? NSHostingView<ContentView> {
    // do what I need with 'hostingView.rootView'
}

Unfortunately, NSHostingView.rootView does not return the actual ContentView that I created, it returns a modified version of that view dependant on the modifiers used. (In my case I'm using .environmentObject modifier.) As a result the if statement above never returns true because the type is not NSHostingView<ContentView> but rather NSHostingView<ModifiedContent<ContentView, _bunch_Of_Gobbletygook_Representing_The_Modifiers>>. One way to "solve" the problem is to print out the result of type(of: hostingView) when I create the window, and then change my cast to include the current version of the "gobbledygook", but that is brittle for the following two reasons:

  1. If I change the modifiers, the compiler will not warn me that I need to update the cast, and
  2. Since the "gobbledygook" contains single underscored values, I must assume those are internal details that could change. Hence without my changing any code, an OS update could cause the cast to start failing.

So I have created a solution in the form of the following NSView extension:

extension NSView {
    func originalRootView<RootView: View>() -> RootView? {
        if let hostingView = self as? NSHostingView<RootView> {
            return hostingView.rootView
        }
        let mirror = Mirror(reflecting: self)
        if let rootView = mirror.descendant("_rootView") {
            let mirror2 = Mirror(reflecting: rootView)
            if let content = mirror2.descendant("content") as? RootView {
                return content
            }
        }
        return nil
    }
}

This allows me to handle my needs using the following:

private func currentContentView() -> ContentView? {
    return NSApplication.shared.keyWindow?.contentView?.originalRootView()
}

... sometime later ...

if let contentView = currentContentView() {
    // do what I need with contentView
}

What I would like to know is if there is a way to implement originalRootView without the use of reflection, presumably by allowing a partially specified cast to the ModifiedContent object. For example, something like the following (which does not compile):

extension NSView {
    func originalRootView<RootView: View>() -> RootView? {
        if let hostingView = self as? NSHostingView<RootView> {
            return hostingView.rootView
        }
        if let hostingView = self as? NSHostingView<ModifiedContent<RootView, ANY>> {
            return hostingView.rootView.content
        }
        return nil
    }
}

The problem is what to put for "ANY". I would think some form of Any or AnyObject, but the complier complains about that. Essentially I would like to tell the compiler that I don't care what ANY is so long as the ModifiedContent has RootView as its content type.

Any ideas would be appreciated.


Solution

  • Just to make the results official, the answer is "no" there is no way to partially match a generic as of Swift 5.1.