skip to Main Content

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:

enter image description here

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


  1. Not a solution, I’m afraid, but an explanation.

    The problem, as you’ve discovered, is that the result of

    self.environment(.someKey, someValue)
    

    is not CustomView. The function doesn’t modify the existing view, it creates a new one.

    Let’s say .someKey is an Int, then you can test this out as follows:

    extension CustomView {
        func doSomething() -> some View {
            let newView = self.environment(.someKey, 23)
            print(type(of: newView))
            return newView
        }
    }
    

    and you’ll see that the actual returned type is

    ModifiedContent<CustomView, _EnvironmentKeyWritingModifier<Int>>
    

    This means that if you also declare doSomethingElse() in an extension on CustomView you won’t be able to apply it to the result of doSomething().

    Nor can you force cast the result of doSomething() to CustomView as they are just different types, so it will crash.

    Login or Signup to reply.
  2. How do you know environment returns CustomView? You don’t – what type it returns is an implementation detail of SwiftUI. Your whole premise falls apart when you see that environment returns something like ModifiedContent<CustomView, _EnvironmentKeyWritingModifier<T>> (as shown in Ashley Mills’ answer), doesn’t it?

    Let’s inline doSomething:

    CustomView()
        .environment(.someKey, someValue)
        .doSomethingElse()
    

    You are not calling doSomethingElse on CustomView() here – you are calling it on the return value of environment, and environment doesn’t even return CustomView, so why should doSomethingElse 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 the doSomething and doSomethingElse to also the wrapper types. But IMO this is really convoluted for what it’s worth.

    protocol ViewChain: View {
        associatedtype ChainRoot
    }
    
    struct WrappedViewChain<Body: View, ChainRoot: View>: ViewChain {
        let wrapped: Body
        let rootType: ChainRoot.Type
        
        var body: some View {
            wrapped.body
        }
    }
    
    extension CustomView: ViewChain {
        typealias ChainRoot = CustomView
    }
    
    extension ViewChain where ChainRoot == CustomView {
        func doSomething() -> WrappedViewChain<some View, CustomView> {
            WrappedViewChain(wrapped: environment(.someKey, someValue), rootType: CustomView.self)
        }
        
        func doSomethingElse() -> WrappedViewChain<some View, CustomView> {
            WrappedViewChain(wrapped: environment(.someOtherKey, someOtherValue), rootType: CustomView.self)
        }
    }
    

    This essentially made it so that doSomething() and doSomethingElse "remembers" the root of the chain in the second type parameter of WrappedViewChain.

    Note that this still won’t work if you make it "forget" the root somewhere along the way, but Text behaves like this too.

    CustomView()
        .frame(width: 100) // forgets the root
        .doSomething() // now this doesn't work
    
    CustomView()
        .doSomething() // OK
        .frame(width: 100) // forgets the root
        .doSomethingElse() // now this doesn't work
    
    Login or Signup to reply.
  3. 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:

    struct CustomView: View {
        @State var myBackground = Color.clear
        
        var body: some View {
            Text("Hello World!")
                .background(myBackground)
        }
    }
    
    extension CustomView {
        func customBackground(_ newBackground: Color) -> some CustomView {
            self.myBackground = newBackground
            return self
        }
    
        func clearBackground() -> some CustomView {
            self.myBackground = .clear
            return self
        }
    }
    
    

    And use it as needed:

    struct AnotherView: View {
    
        var body: some View {
          VStack {
            CustomView()
              .customBackground(.blue)
              .clearBackground()
              .customBackground(.red)
               // ... and so on
    
            Text("Hi everyone!")
              .customBackground(.blue) // <--- this will fail compiling
          }
        }
    
    }
    

    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

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search