Search code examples
iosswiftuisf-symbols

How can I get 3 combined `SF Symbol` images to rotate around a specific point?


I have three (3) SF Symbols grouped and arranged in a specific relationship to each other. Now I want the entire group to rotate, as one item, around a specific point.

enter image description here

I have spent hours with trial & error (mostly error) trying to adjust combinations of padding and offsets and anchor points.

Context:

This rotatable view will be used as part of an iOS 17 Annotation using MapKit to mark a specific GPS location indicating the direction of travel (hence the rotating bit and the desired anchor point).

IF there is a better way to accomplish my ultimate goal … I'm open to all ideas. :-)

Here is my code:

import SwiftUI

struct StarArrowView: View {
   
   var bumpColor: Color
   @State var rotaAtAnchor = false
   var body: some View {
      ZStack {
         Divider()
         Divider().offset(y: 25)
         Divider().offset(y: -25)
         Divider().rotationEffect(.degrees(90))
         Divider().rotationEffect(.degrees(90)).offset(x: 25)
         Divider().rotationEffect(.degrees(90)).offset(x: -25)
         Group {
            HStack(spacing: 0) {
               Image(systemName: "star.fill")
                  .symbolRenderingMode(.palette)
                  .foregroundStyle(bumpColor)
                  .font(.system(size: 30, weight: .light))
                  .padding(0)
                  .offset(x: 8, y: 6)
               Image(systemName: "line.diagonal.arrow")
                  .foregroundStyle(bumpColor)
                  .font(.system(size: 40, weight: .light))
                  .padding(0)
                  .offset(x: -10, y: -15)
               Text("➘")
                  .symbolRenderingMode(.palette)
                  .foregroundStyle(bumpColor, bumpColor)
                  .font(.system(size: 30, weight: .ultraLight))
                  .rotationEffect(.degrees(190), anchor: .topLeading)
                  .padding(0)
                  .offset(x: 0, y: 0)
            }
            .padding(.vertical, -20)
            .padding(.horizontal, -5)
            .rotationEffect(.degrees(45))
            .offset(x: 15, y: 22)
            
            
            .rotationEffect(.degrees(rotaAtAnchor ? 0 : 180), anchor: .trailing) //Anchor Position
            .animation(Animation.spring ().repeatForever(autoreverses: true))
            .onAppear() {
               self.rotaAtAnchor.toggle()
            }
         }
      }
   }
}
   


#Preview {
   StarArrowView(bumpColor: .heatmap10)
}


Solution

  • The tricky part about this problem is getting the anchor point right. One way would be to use a UnitPoint with fractional position. Alternatively, in order to use one of the edges of the group as the anchor point, the arrow pointing up needs to be moved out of the bounds of the group. This can be done using an overlay.

    Here is an attempt to get it working. Some notes:

    • When you're working on building the groups and finding anchor points, it can help to put a colored background behind the individual items so that the frame becomes visible.
    • A ZStack is used to combine the symbols. I would suggest, this works better than an HStack because there is some overlap involved.
    • Padding is used in preference to offsets wherever possible. This way, the items are shown at their true position in the layout.
    • The arrow pointing up is applied as an overlay to the group with .topTrailing alignment. It is then moved over the edge of the group with an x-offset.
    • To make sure the arrow pointing up is positioned exactly half-way over the trailing edge, it is given a fixed width (any size greater than the natural width is fine), then the x-offset is half of this width.
    ZStack {
        Divider()
        Divider().offset(y: 25)
        Divider().offset(y: -25)
        Divider().rotationEffect(.degrees(90))
        Divider().rotationEffect(.degrees(90)).offset(x: 25)
        Divider().rotationEffect(.degrees(90)).offset(x: -25)
    
        ZStack(alignment: .leading) {
            Image(systemName: "star.fill")
                .symbolRenderingMode(.palette)
                .foregroundStyle(bumpColor)
                .font(.system(size: 30, weight: .light))
                .rotationEffect(.degrees(45))
            Image(systemName: "line.diagonal.arrow")
                .foregroundStyle(bumpColor)
                .font(.system(size: 40, weight: .light))
                .rotationEffect(.degrees(45))
                .padding(.leading, 30)
        }
        .padding(.trailing, 10)
        .overlay(alignment: .topTrailing) {
            Text("➘")
                .symbolRenderingMode(.palette)
                .foregroundStyle(bumpColor, bumpColor)
                .font(.system(size: 30, weight: .ultraLight))
                .rotationEffect(.degrees(235))
                .frame(width: 30)
                .offset(x: 15)
        }
        .rotationEffect(.degrees(rotaAtAnchor ? 180 : 0), anchor: .trailing)
    
        // Fine adjustment of the position of the entire group
        .padding(.leading, 30)
        .padding(.top, 20)
    
        .animation(Animation.spring ().repeatForever(autoreverses: true), value: rotaAtAnchor)
        .onAppear() {
           self.rotaAtAnchor.toggle()
        }
    }
    

    Animation