How to deliver View content from different objects

Hi,

I'm trying to allow different classes to deliver their own swift ui view content. I've made a number of (naive) attempts which either do not compile, or do not work:

import Foundation
import SwiftUI

class Base
{

    func getUI() -> some View
    {
        return EmptyView()
    }

}

class Parent : Base
{

    override func getUI() -> some View
    {
        return EmptyView()
    }

}
Method does not override any method from its superclass

I guess this is because the opaque return types do not match under the hood. If I remove the override clause I achieve compilation.

class Parent : Base
{

    func getUI() -> some View
    {
        return EmptyView()
    }

}

However at runtime, Base.getUI is called from instances of Parent - the compiler omits Parent.getUI.

I tried to address this via a protocol, which delivers these compilation errors:

protocol ProvidesUI
{
    func getUI() -> some View
}
'some' type cannot be the return type of a protocol requirement; did you mean to add an associated type?

Not sure what associated type would be appropriate.

protocol ProvidesUI
{
    func getUI() -> View
}
Protocol 'View' can only be used as a generic constraint because it has Self or associated type requirements.

What is the idomatic way to structure this, such that different classes can provide their own SwiftUI View content?

Thanks

How could this possibly be the case if the bodies of the functions are the exact same? The concrete return type of Base.getUI and Parent.getUI in your first example is EmptyView.

It looks like the issue here is that methods that use opaque return types cannot be overridden. Do you really need to use method overriding? If so, you can use the AnyView type-eraser instead:

class Base {

    func getUI() -> AnyView {
        return AnyView(EmptyView())
    }

}

class Parent: Base {

    override func getUI() -> AnyView {
        return AnyView(EmptyView())
    }

}
1 Like

This is what the compiler is suggesting you do:

protocol ProvidesUI {
    
    associatedtype Body : View

    func getUI() -> Body
}

class ConformingType: ProvidesUI {
    
    func getUI() -> some View {
        EmptyView()
    }

}
2 Likes

If you're talking about subclasses delivering different types than their superclasses, this violates the Liskov substitution principle. I don't think subclassing is a match for your goals (or probably any of anyone else's :grimacing:).

1 Like

Thanks @Peter-Schorn - the AnyView cast restored simple OO behaviour and works perfectly for my purposes. :+1:

I share your surprise re the opaque return types, but can't identify any other explanation. I believe the method is compiled as an overload, not an override. (ios - Swift: method overloads that only differ in return type - Stack Overflow)

Once you eliminate the impossible, whatever remains ...

It should not be surprising—this is the raison d’être of opaque types!

Two functions with return type some T have distinct return types: it is exactly this that makes the type opaque. It’s not that the types don’t match “under the hood”; the underlying type can be exactly the same as you demonstrate. Opaque types, as the term suggests, make the underlying type opaque, so that the compiler only looks “over the hood,” if you will.

There is not currently a way to spell “I want an opaque return type that is the same as the opaque return type of some other function.” In diagnostic messages, we currently disambiguate by writing “some T (result of f)” and “some T (result of g)”—conceivably, in the future, we’ll be able to actually spell out “result of f” in code, but that is not today.

2 Likes

I don't know what "this" refers to. Please identify what reason you're referring to here.

Two functions with return type some T do not necessarily have distinct return types. This is not an accurate statement. What makes a type opaque is the fact that it is returned from a function that declares an opaque return type.

What you probably mean to say is that two functions with return type some T could return different underlying types.

The compiler knows the underlying concrete type of any function with an opaque return type, so it should allow you override a function with an opaque return type if the concrete return type is the same.

No, I am not saying what you think I am. Rather, some T is a type of its own (an opaque type) distinct from the underlying type from the perspective of the static type system. Two functions f and g that return some T necessarily have different return types (which Swift diagnostics currently call "result of f" and "result of g") regardless whether the underlying types are the same or different.

The function in question may be defined in another resilient module, the underlying concrete type of which may then change. Whether or not the compiler knows the underlying type, however, is immaterial: some T is a type of its own, and the point of its being an opaque type is that the compiler does not make use of its knowledge of the underlying type.

The behavior I've outlined above is the relevant "this."

2 Likes

Why would opaque types be implemented this way? How does "result of f " and "result of g" differ from the perspective of the caller (or from the perspective? When a function returns some T, you can only call methods on the resulting value that are part of the protocol T. Is there any scenario in which replacing a call to f with a call to g would cause an error?

That's what it means to be opaque. Let me state again: to say that the result type of f is an opaque some T is to say that the static type system makes use of no knowledge about the result type of f other than (1) that it's of some type that conforms to T; and (2) that it's the result type of f.

They differ in that they are the result of two different functions. Since there is no way to utter any constraint that the opaque result types of two different functions both spelled some T are the same and since there is no notion of identity of separately declared functions, all we (as in, the static type system) can know about the result type of g is (1) that it's of some type that conforms to T; and (2) that it's the result type of g, which is not f.

Of course—that is in fact fundamental to the use of opaque types:

protocol T { }
struct S: T { }
func f() -> some T { S() }
func g() -> some T { S() }

var a = f() // The static type of `a` is `some T` (result of `f()`)
a = f()     // OK
a = g()     // Error: cannot assign value of type 'some T' (result of 'g()') to type 'some T' (result of 'f()')

var b = g() // The static type of `b` is `some T` (result of `g()`)
b = g()     // OK
b = f()     // Error: cannot assign value of type 'some T' (result of 'f()') to type 'some T' (result of 'g()')
1 Like
Terms of Service

Privacy Policy

Cookie Policy