iosswiftswiftuiopaque-types

SwiftUI: restrict extension to custom view struct / return custom view type


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.


Solution

  • 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