What I want to achieve:
CustomView()
.doSomething() // ← should only be available on CustomView
.doSomethingElse() // ← should only be available on CustomView
AnyOtherView()
.doSomething() // ← should not compile
Pretty much like SwiftUI’s Text implementation has that exact functionality:
What I tried
struct CustomView: View {
...
}
extension CustomView {
func doSomething() -> some CustomView {
self.environment(.someKey, someValue)
}
func doSomethingElse() -> some CustomView {
self.environment(.someOtherKey, someOtherValue)
}
}
I get the following error: "An ‘opaque’ type must specify only ‘Any’, ‘AnyObject’, protocols, and/or a base class".
What I also tried:
extension CustomView {
func doSomething() -> CustomView {
self.environment(.someKey, someValue)
}
func doSomethingElse() -> CustomView {
self.environment(.someOtherKey, someOtherValue)
}
}
I get the following error: Cannot convert return expression of type ‘some View’ to return type ‘CustomView’.
Xcode provides the following fix:
extension CustomView {
func doSomething -> CustomView {
self.environment(.someKey, someValue) as! CustomView
}
}
But force casting does not really look like a great solution.
How can I fix this?
I only want to extend CustomView
. I don’t want to extend View
and return some View
because that would expose functionality to all views (which is not what I want).
If I extend CustomView and simply return some View, then I cannot use both functions at the same time.
Edit: Why do I want to achieve that?
I am building up a Swift Package to provide CustomView
to multiple projects.
And to make CustomView
easy to use I wanted to make it configurable with view modifiers instead of a simple initializer.
I could use my provided CustomView
like that:
CustomView(value1: someValue, value2: someOtherValue)
… but I wanted to make it more SwiftUI-Like in the way of optional view modifiers like that:
CustomView()
.value1(someValue)
.value2(someOtherValue)
That would look nice if I needed other view modifiers on that view like tint(...)
or fixedSize()
, etc. Much like you would configure Text
, which you customize with view modifiers instead of the initializer, since customizing is optional.
3
Answers
Not a solution, I’m afraid, but an explanation.
The problem, as you’ve discovered, is that the result of
is not
CustomView
. The function doesn’t modify the existing view, it creates a new one.Let’s say
.someKey
is anInt
, then you can test this out as follows:and you’ll see that the actual returned type is
This means that if you also declare
doSomethingElse()
in an extension onCustomView
you won’t be able to apply it to the result ofdoSomething()
.Nor can you force cast the result of
doSomething()
toCustomView
as they are just different types, so it will crash.How do you know
environment
returnsCustomView
? You don’t – what type it returns is an implementation detail of SwiftUI. Your whole premise falls apart when you see thatenvironment
returns something likeModifiedContent<CustomView, _EnvironmentKeyWritingModifier<T>>
(as shown in Ashley Mills’ answer), doesn’t it?Let’s inline
doSomething
:You are not calling
doSomethingElse
onCustomView()
here – you are calling it on the return value ofenvironment
, andenvironment
doesn’t even returnCustomView
, so why shoulddoSomethingElse
be available?One way to rephrase what you want is to keep track of the "root" of the call chain – a way to say "this method should be available on call chains that has
CustomView
as its root"You can sort of do this by returning from
doSomething
a wrapper that also has the root’s type, and adding thedoSomething
anddoSomethingElse
to also the wrapper types. But IMO this is really convoluted for what it’s worth.This essentially made it so that
doSomething()
anddoSomethingElse
"remembers" the root of the chain in the second type parameter ofWrappedViewChain
.Note that this still won’t work if you make it "forget" the root somewhere along the way, but
Text
behaves like this too.As others have pointed out, you cannot pipe any modifier in your functions that have a return type other than your
CustomView
With that said, you can do something like:
And use it as needed:
But you cannot use any other modifier that erases your type. To reach your desired behavior, all the changes done in your functions/modifiers must be done only on properties accessible by your struct directly and have a return value that you guarantees is your view’s type