SwiftUI - fill with AngularGradient or LinearGradient

Viewed 950

I would like to create a View with a shape such as a Rectangle, which is either filled with a linear or an angular gradient, based on a variable.

My attempt looks like this:

struct TempTestView: View {
    @State var circleMode: Bool = true
    let gradient: Gradient = Gradient(colors: [.red, .green])
    var body: some View {
        Rectangle()
            .fill(circleMode ? AngularGradient(gradient: gradient, center: .center) : LinearGradient(gradient: gradient, startPoint: .leading, endPoint: .trailing))
            .opacity(0.5)
            .frame(width: 200, height: 200)


    }
}

struct TempTestView_Previews: PreviewProvider {
    static var previews: some View {
        TempTestView()
    }
}

Swift throws me an error: Cannot convert value of type 'AngularGradient' to expected argument type 'FillStyle'

If I change the code to remove the inline if and replace it with either AngularGradient(gradient: gradient, center: .center) or LinearGradient(gradient: gradient, startPoint: .leading, endPoint: .trailing), swift compiles successfully.

If I try to wrap everything in a function that returns some ShapeStyle, swift complains that AngularGradient and LinearGradient are not of the same type. With a View I'd wrap both in AnyView, but I don't see an AnyStyle to wrap in.

My example is simplified. For animation/transition reasons, there cannot be two separate rectangle, which was why I'm looking for an inline solution like in my example.

Any help would be appreciated.

5 Answers

I had exactly the same problem. I solved it by creating a computed property that returns the pre-formatted rectangle and using it as a view in the body. This solution is not ideal though, but at least it lets you add the view to the body and specify transitions and animations just once:

private var Bkgr:  AnyView {
  if circleMode {
    return AnyView(Rectangle().fill(AngularGradient(gradient: gradient, center: .center)))
  }
  return AnyView(Rectangle().fill(LinearGradient(gradient: gradient, startPoint: .leading, endPoint: .trailing)))
}

Then you can just simply add it in your body as a single view:

var body: some View {
  self.Bkgr
    .opacity(/*..*/)
    .transition(/*...*/)
    .animation(/**/)
    // go crazy just once
}

Opaque return type some view requires you to provide determined code. The result type must be certain, not depending of any variable or condition. This is why you can use condition ? Result1 : Result2 operator only if Result1 and Result2 are the same type. LinearGradient and AngularGradient are not.

So, you can put whole Rectangle in If-else block. Its not a flow instruction in a runtime, its an alternative view container, like VStack. It will make a cortege of two Views which can have different types. Its like Schroedinger View :) Actually it is a both Views type at the same time, so swift just hide one of them according to condition.

Try something like this:

struct TempTestView: View {
    @State var circleMode: Bool = true
    let gradient: Gradient = Gradient(colors: [.red, .green])
    var body: some View 
        Group{
            if circleMode{
                Rectangle()
                    .fill(AngularGradient(gradient: gradient, center: .center))
            }else{
                Rectangle()
                    .fill(LinearGradient(gradient: gradient, startPoint: .leading, endPoint: .trailing))
            }
        }
            .opacity(0.5)
            .frame(width: 200, height: 200)
    }           
}

You could try approaching this situation with a different angle. Try to relate the Angular and the Linear gradient as views. and place them inside a VStack as so:

struct TempTestView: View {
    @State var circleMode: Bool = true
    let gradient: Gradient = Gradient(colors: [.red, .green])
    var body: some View {
        VStack {
            if circleMode {
                AngularGradient(gradient: gradient, center: .center)
            } else {
                LinearGradient(gradient: gradient, startPoint: .leading, endPoint: .trailing)
            }
        }
        .opacity(0.5)
        .frame(width: 200, height: 200)
    }
}

I tried it and it complies.

The body must always return the same kind of view:

struct TempTestView: View {

    @State var circleMode: Bool = true
    let gradient: Gradient = Gradient(colors: [.red, .green])

    var body: some View {
        Group {
            Button(action: {
                self.circleMode.toggle()
            }) {
                Text("Toggle Circle Model!")
            }
            if circleMode {
                Rectangle()
                    .fill(AngularGradient(gradient: gradient, center: .center))
            } else {
                Rectangle()
                    .fill(LinearGradient(gradient: gradient, startPoint: .leading, endPoint: .trailing))
            }
        }
        .opacity(0.5)
        .frame(width: 200, height: 200)
    }
}

Try this code for your desired result.

another option is to use function to return AnyView.

struct TempTestView: View {
    @State var circleMode: Bool = true
    let gradient: Gradient = Gradient(colors: [.red, .green])
    var body: some View {
        VStack{
            myGradient()
                .opacity(0.5)
                .frame(width: 200, height: 200)
            Button(action: {self.circleMode.toggle()}){
                Text("toggle")
            }
        }
    }
    func myGradient() -> AnyView{
        if circleMode {
            return AnyView(Rectangle().fill(AngularGradient(gradient: gradient, center: .center)))
        } else {
            return AnyView(Rectangle().fill(LinearGradient(gradient: gradient, startPoint: .leading, endPoint: .trailing)))
        }
    }
}

Its some cheating way. You hide real signature of your View from SwiftUI with AnyView container. But if works. It's just a code there in a function. You can use flow control (loops and if-else in runtime) to create AnyView.

Related