SD449
SD449

Reputation: 107

SwiftUI InsettableShape with .strokeBorder

Can someone please explain why my self made "myRectangle" that I have conformed to InsettableShape doesn't work with .strokeBorder but the built in Rectangle() does?

Here is myRectangle code;

struct myRectangle: InsettableShape {
    var insetAmount: CGFloat = 0
    
    func path(in rect: CGRect) -> Path {
    var path = Path()
        
        path.move(to: CGPoint(x: rect.midX * 1.2, y: rect.maxY))
        path.addLine(to: CGPoint(x: rect.midX * 1.2, y: rect.midY * 0.6))
        path.addLine(to: CGPoint(x: rect.midX * 0.8, y: rect.midY * 0.6))
        path.addLine(to: CGPoint(x: rect.midX * 0.8, y: rect.maxY))
        
        return path
    }
    
    func inset(by amount: CGFloat) -> some InsettableShape {
        var rectangle = self
        rectangle.insetAmount -= amount
        return rectangle
    }
}

then modifying it with the .strokeBorder;

struct ColorCyclingRectangle: View {
    var amount = 0.0
    var steps = 100

    var body: some View {
        ZStack {
            ForEach(0..<steps) { value in
                myRectangle()
                    .inset(by: CGFloat(value))
                    .strokeBorder(self.color(for: value, brightness: 1), lineWidth: 2)
            }
        
        }
    .drawingGroup()
}

    func color(for value: Int, brightness: Double) -> Color {
        var targetHue = Double(value) / Double(self.steps) + self.amount
        
        if targetHue > 1 {
            targetHue -= 1
        }
        
        return Color(hue: targetHue, saturation: 1, brightness: brightness)
        
    }
}

I am expecting the rainbow effect but just get a gradient line.

Expected style

What I am getting

I have tried modifying my path to - insetAmount but I just get a weird shape and the rainbow border is straight, not square

Am I misunderstanding somthing or does this modifyer only work on the default shapes?

Upvotes: 0

Views: 1658

Answers (1)

Waleed Al-Balooshi
Waleed Al-Balooshi

Reputation: 6406

Though this is an older questions I will make an attempt to answer it in hopes the OP is still interested or that it may help others.

You can most definitely use the strokeBorder() modifier on custom types that conform to the InsettableShape protocol noting that it is your responsibility to write the code that insets - reduces size - of your custom shape accordingly.

The first item I noticed in your code is that you are hardcoding some values within your path(in rect: CGRect) method which is why your output is not a square. Two options for this are:

  1. Add width and height variables to myRectangle and use them in your path(in rect: CGRect) to create the shape
  2. Create the shape using the entire size of rect and utilize the .frame() modifier on the container where you are creating your custom shape or on the shape directly to control its dimensions.

I am not sure which is preferable, but I will use option 2 as it seems this is how the built-in shapes work - in relation to width and height at least - and it makes it easier to comprehend the insetting code as follows

struct myRectangle: InsettableShape {
  var insetAmount: CGFloat = 0
  
  func path(in rect: CGRect) -> Path {
    var path = Path()
    path.move(to: CGPoint(x: rect.minX + insetAmount, y: rect.maxY - insetAmount))
    path.addLine(to: CGPoint(x: rect.maxX - insetAmount, y: rect.maxY - insetAmount))
    path.addLine(to: CGPoint(x: rect.maxX - insetAmount, y: rect.minY + insetAmount))
    path.addLine(to: CGPoint(x: rect.minX + insetAmount, y: rect.minY + insetAmount))
    path.addLine(to: CGPoint(x: rect.minX + insetAmount, y: rect.maxY - insetAmount))
    
    return path
  }
  
  func inset(by amount: CGFloat) -> some InsettableShape {
    var rectangle = self
    rectangle.insetAmount += amount
    return rectangle
  }
}

In the above you will note:

  1. I removed the 1.2, 0.8 and 0.6 and instead utilized minX, maxX, minY and maxY to create the shape utilizing the entirety of rect size
  2. To inset a rectangle you need to bring each of the edge points inwards by the inset amount taking account that SwiftUI measures coordinates from top-left, so you can say top-left would be (0,0)
  3. I have used the insetAmount variable within the code in path(in rect: CGRect) that creates the path. For example path.move(to: CGPoint(x: rect.minX + insetAmount, y: rect.maxY - insetAmount)) is the bottom-left point. To inset this point you need to move the X to the right rect.minX + insetAmount and Y upwards rect.maxY - insetAmount - we are subtracting from Y to move upwards per my point 2.

In your ColorCyclingRectangle custom View I only added a .frame(width: 300, height: 300) modifier to the ZStack container that you are creating your custom shape in so that I can control its size.

struct ColorCyclingRectangle: View {
  var amount = 0.0
  var steps = 100

  var body: some View {
    ZStack {
      ForEach(0..<steps) { value in
        myRectangle()
          .inset(by: CGFloat(value))
          .strokeBorder(self.color(for: value, brightness: 1), lineWidth: 2)
      }
    }
    .drawingGroup()
    .frame(width: 300, height: 300)
  }

  func color(for value: Int, brightness: Double) -> Color {
    var targetHue = Double(value) / Double(self.steps) + self.amount
    
    if targetHue > 1 {
      targetHue -= 1
    }
    
    return Color(hue: targetHue, saturation: 1, brightness: brightness)
  }
}

With this I was able to get the rainbow effect.

Hopefully this resolves your issue.

Upvotes: 3

Related Questions