1

I have a map pin view that represents an action.

Action 1

Action 2

This is the code

VStack {
                                ActionView(action: place.actions.first!)
                                    .frame(width: 35, height: 35)
                                Text(self.displayPlaceName ? place.name : "")
                            }.overlay(
                                Image(systemName: "arrowtriangle.left.fill")
                                    .rotationEffect(Angle(degrees: 270))
                                    .foregroundColor(place.actions.first!.color())
                                    .offset(y: 10)
                            )

The pin can represent only one action.

If there are two actions I would like to see half of both pin. something like that.

Expected result

Thanks, Nicolas

n savoini
  • 422
  • 1
  • 3
  • 13
  • Have you tried putting two of your `VStack{ActionView...}` , into a `HStack` with no spacing and the appropriate offset position? Or simply two of your `VStack{ActionView...}` at the same location, with the top one and covering half of the other, again using the appropriate offset. – workingdog support Ukraine Jul 03 '23 at 23:54
  • I have not try this way, I'll give it a chance, thanks. – n savoini Jul 04 '23 at 01:01

1 Answers1

3

One way to hide half of a view is by applying a gradient mask. If we put two stops in the gradient at the same location with different colors, we get an instant change (a step) in the gradient rather than a smooth transition. Some code:

struct ContentView: View {
  var body: some View {
    Callout(borderColor: .brown) {
      Text("")
    }
    .mask {
      Rectangle()
        .fill(.linearGradient(
          stops: [
            // The specific color doesn't matter here, only its alpha,
            // which is 1 for .red and 0 for .clear.
            .init(color: .red, location: 0),
            .init(color: .red, location: 0.5),
            .init(color: .clear, location: 0.5),
            .init(color: .clear, location: 1.0),
          ],
          startPoint: .leading,
          endPoint: .trailing
        ))
    }
  }
}

Result:

the left half of a callout containing a soup bowl emoji

We can swap the gradient colors to show the other half of the masked content. So, we can use a ZStack containing two callouts, with opposite halves masked:

struct ContentView: View {
  var body: some View {
    ZStack {
      Callout(borderColor: .brown) {
        Text("")
      }
      .mask {
        Rectangle()
          .fill(.linearGradient(
            stops: [
              // The specific color doesn't matter here, only its alpha,
              // which is 1 for .red and 0 for .clear.
              .init(color: .red, location: 0),
              .init(color: .red, location: 0.5),
              .init(color: .clear, location: 0.5),
              .init(color: .clear, location: 1.0),
            ],
            startPoint: .leading,
            endPoint: .trailing
          ))
      }

      Callout(borderColor: .green) {
        Text("")
      }
      .mask {
        Rectangle()
          .fill(.linearGradient(
            stops: [
              .init(color: .clear, location: 0),
              .init(color: .clear, location: 0.5),
              .init(color: .red, location: 0.5),
              .init(color: .red, location: 1.0),
            ],
            startPoint: .leading,
            endPoint: .trailing
          ))
      }
    }
  }
}

Result:

The left half shows a brown-framed callout of a soup bowl emoji. The right half shows a green-framed callout of a camera emoji.

The result is a sharp cut between the two callouts. You can fade one into the other by replacing the step in the gradients with an interpolation:

struct ContentView: View {
  var body: some View {
    ZStack {
      Callout(borderColor: .brown) {
        Text("")
      }
      .mask {
        Rectangle()
          .fill(.linearGradient(
            stops: [
              .init(color: .red, location: 0),
              .init(color: .red, location: 0.4),   // <<< CHANGED
              .init(color: .clear, location: 0.6), // <<< CHANGED
              .init(color: .clear, location: 1.0),
            ],
            startPoint: .leading,
            endPoint: .trailing
          ))
      }

      Callout(borderColor: .green) {
        Text("")
      }
      .mask {
        Rectangle()
          .fill(.linearGradient(
            stops: [
              .init(color: .clear, location: 0),
              .init(color: .clear, location: 0.4), // <<< CHANGED
              .init(color: .red, location: 0.6),   // <<< CHANGED
              .init(color: .red, location: 1.0),
            ],
            startPoint: .leading,
            endPoint: .trailing
          ))
      }
    }
  }
}

Result:

same as prior result except the left callout fades into the right callout over several pixel columns

Here's the source for my Callout view in case you want to play with it:

struct CalloutBorderShape: Shape {
  let arrowHeight: CGFloat

  func path(in rect: CGRect) -> Path {
    let rect = rect.divided(atDistance: arrowHeight, from: .maxYEdge).remainder
    let lineWidth: CGFloat = 3
    var path = Path()
    path.addRoundedRect(
      in: rect.insetBy(dx: 0.5 * lineWidth, dy: 0.5 * lineWidth),
      cornerSize: .init(width: 2 * lineWidth, height: 2 * lineWidth)
    )
    path = path.strokedPath(.init(lineWidth: lineWidth))
    path.move(to: .init(x: rect.midX, y: rect.maxY + arrowHeight))
    path.addLine(to: .init(x: rect.midX - arrowHeight, y: rect.maxY))
    path.addLine(to: .init(x: rect.midX + arrowHeight, y: rect.maxY))
    path.closeSubpath()
    return path
  }
}

struct Callout<Content: View>: View {
  let borderColor: Color

  @ViewBuilder
  let content: Content

  var body: some View {
    content
      .padding(EdgeInsets(
        top: paddingSize,
        leading: paddingSize,
        bottom: paddingSize + arrowHeight,
        trailing: paddingSize
      ))
      .overlay {
        CalloutBorderShape(arrowHeight: arrowHeight)
          .fill(borderColor)
      }
  }

  private let paddingSize: CGFloat = 10
  private let arrowHeight: CGFloat = 6
}
rob mayoff
  • 375,296
  • 67
  • 796
  • 848