title: Example description: <Annotation> anchors any SwiftUI view tree at a map coordinate — chips, badges, photos, custom shapes — whenever the pin needs to look like something other than a stock MapKit <Marker> glyph. Supports a KeywordPoint/Point anchor, a tag for <Map selection>, and an optional title label. iOS 17+; selection requires iOS 18+. <Map> also gains annotationTitles / annotationSubtitles for hiding the MapKit-rendered text labels globally.


import {
  Annotation, Button, Circle, HStack, Map, Marker, Navigation, NavigationStack,
  Picker, RoundedRectangle, Script, ScrollView, Spacer, Text, useEffect, useObservable, useState,
  VStack, ZStack,
} from "scripting"
import type { KeywordPoint, MapAnnotationLabelVisibility, MapSelectionValue } from "scripting"

// Demo center: People's Square, Shanghai
const initialRegion = {
  center: { latitude: 31.2304, longitude: 121.4737 },
  span: { latitudeDelta: 0.05, longitudeDelta: 0.05 },
}

// Three landmarks around the demo center.
const points = [
  { id: "bund", name: "Bund", coord: { latitude: 31.2397, longitude: 121.4906 } },
  { id: "lujia", name: "Lujiazui", coord: { latitude: 31.2397, longitude: 121.5000 } },
  { id: "xtd", name: "Xintiandi", coord: { latitude: 31.2218, longitude: 121.4760 } },
] as const

type PointId = (typeof points)[number]["id"]

function CustomPinDemo() {
  const position = useObservable<MapCameraPosition>(MapCameraPosition.region(initialRegion))
  const selection = useObservable<MapSelectionValue | null>(null)

  const tappedId =
    selection.value != null && selection.value.type === "marker"
      ? (selection.value.tag as PointId)
      : null
  const tapped = points.find(p => p.id === tappedId)

  return <VStack alignment={"leading"} spacing={8}>
    <Text font={"headline"}>{`1. Custom-content \`<Annotation>\``}</Text>
    <Text font={"caption"} foregroundStyle={"secondaryLabel"}>
      {`Each pin is a custom SwiftUI subtree (rounded chip with letter). Tapping
      a chip writes its \`tag\` into \`<Map selection>\`; we use that to grow the
      selected chip and tint it red.`}
    </Text>
    {tapped != null
      ? <Text font={"caption"} foregroundStyle={"systemBlue"}>
        Selected: {tapped.name}
      </Text>
      : <Text font={"caption2"} foregroundStyle={"tertiaryLabel"}>
        Tap a chip to select it.
      </Text>}
    <Map
      cameraPosition={position}
      selection={selection}
      frame={{ height: 280 }}
      clipShape={{ type: "rect", cornerRadius: 12 }}
    >
      {points.map(p => {
        const isSelected = p.id === tappedId
        return <Annotation
          coordinate={p.coord}
          title={p.name}
          anchor="bottom"
          tag={p.id}
        >
          <ZStack>
            <RoundedRectangle
              cornerRadius={10}
              fill={isSelected ? "systemRed" : "systemBlue"}
              frame={{ width: isSelected ? 32 : 24, height: isSelected ? 32 : 24 }}
            />
            <Text
              font={isSelected ? "headline" : "caption"}
              foregroundStyle={"white"}
            >
              {p.name.charAt(0)}
            </Text>
          </ZStack>
        </Annotation>
      })}
    </Map>
  </VStack>
}

function AnchorPickerDemo() {
  const position = useObservable<MapCameraPosition>(MapCameraPosition.region(initialRegion))
  const [anchor, setAnchor] = useState<KeywordPoint>("center")

  const anchorOptions: KeywordPoint[] = [
    "center", "top", "bottom", "leading", "trailing",
    "topLeading", "topTrailing", "bottomLeading", "bottomTrailing",
  ]

  return <VStack alignment={"leading"} spacing={8}>
    <Text font={"headline"}>2. `anchor` placement</Text>
    <Text font={"caption"} foregroundStyle={"secondaryLabel"}>
      The same content view, anchored at different `KeywordPoint`s relative to
      the same coordinate. `"bottom"` is the classic pin-style anchor (pin
      tip sits on the spot); `"center"` puts the visual center on the spot.
    </Text>
    <Picker title="anchor" value={anchor} onChanged={(v: any) => setAnchor(v as KeywordPoint)}>
      {anchorOptions.map(opt => (
        <Text tag={opt}>{opt}</Text>
      ))}
    </Picker>
    <Map
      cameraPosition={position}
      frame={{ height: 240 }}
      clipShape={{ type: "rect", cornerRadius: 12 }}
    >
      <Annotation
        coordinate={initialRegion.center}
        title=""
        anchor={anchor}
      >
        <ZStack>
          <Circle fill="systemOrange" frame={{ width: 28, height: 28 }} />
          <Text font="caption2" foregroundStyle={"white"}>●</Text>
        </ZStack>
      </Annotation>
      {/* Reference pin marks the exact coordinate the annotation is anchored to. */}
      <Marker title="" coordinate={initialRegion.center} tint="systemRed" />
    </Map>
    <Text font={"caption2"} foregroundStyle={"tertiaryLabel"}>
      The red Marker pin marks the actual coordinate. Compare its position to
      the orange annotation as you cycle through anchors.
    </Text>
  </VStack>
}

function TitleVisibilityDemo() {
  const position = useObservable<MapCameraPosition>(MapCameraPosition.region(initialRegion))
  const [titles, setTitles] = useState<MapAnnotationLabelVisibility>("automatic")

  return <VStack alignment={"leading"} spacing={8}>
    <Text font={"headline"}>{`3. \`<Map annotationTitles>\``}</Text>
    <Text font={"caption"} foregroundStyle={"secondaryLabel"}>
      {`Toggle the global title visibility. \`annotationTitles\` applies to
      \`Marker(item:)\`, \`<Annotation title>\`, and Apple POI labels in the
      same map.`}
    </Text>
    <Picker title="annotationTitles" value={titles} onChanged={(v: any) => setTitles(v as MapAnnotationLabelVisibility)}>
      <Text tag="automatic">automatic</Text>
      <Text tag="visible">visible</Text>
      <Text tag="hidden">hidden</Text>
    </Picker>
    <Map
      cameraPosition={position}
      annotationTitles={titles}
      frame={{ height: 240 }}
      clipShape={{ type: "rect", cornerRadius: 12 }}
    >
      {points.map(p => (
        <Annotation coordinate={p.coord} title={p.name} anchor="bottom">
          <Circle fill="systemGreen" frame={{ width: 20, height: 20 }} />
        </Annotation>
      ))}
    </Map>
  </VStack>
}

function SelectionPopoverDemo() {
  const position = useObservable<MapCameraPosition>(MapCameraPosition.region(initialRegion))
  // selection 走 <Map selection>(Phase 3f),annotation tap 写入 { type:"marker", tag }
  const selection = useObservable<MapSelectionValue | null>(null)
  // popover 用 Observable<boolean> 形式,直接跟 selection 状态同步
  const popoverShown = useObservable(false)

  // selection → popover 双向同步:
  //  - 点中 annotation → 显示 popover
  //  - 点空白 / 关闭 popover → 清空 selection
  useEffect(() => {
    const isMarker =
      selection.value?.type === "marker" && selection.value.tag === "popover-pin"
    popoverShown.setValue(isMarker)
  }, [selection.value])

  return <VStack alignment={"leading"} spacing={8}>
    <Text font={"headline"}>{`4. Selection-driven popover`}</Text>
    <Text font={"caption"} foregroundStyle={"secondaryLabel"}>
      {`SwiftUI 原生模式:annotation content view 上直接挂 \`popover\` modifier。
      tap 写入 selection observable,我们再把它同步到 popover 的 isPresented。`}
    </Text>
    <Map
      cameraPosition={position}
      selection={selection}
      frame={{ height: 280 }}
      clipShape={{ type: "rect", cornerRadius: 12 }}
    >
      <Annotation
        coordinate={initialRegion.center}
        title="People's Square"
        anchor="bottom"
        tag="popover-pin"
      >
        <ZStack
          popover={{
            isPresented: popoverShown,
            arrowEdge: "bottom",
            presentationCompactAdaptation: "popover",
            content: <VStack alignment={"leading"} spacing={8} padding>
              <Text font={"headline"}>People's Square</Text>
              <Text font={"caption"} foregroundStyle={"secondaryLabel"}>
                The popover anchors automatically to the annotation it sits on.
                Tap the map background to dismiss — selection clears and the
                popover hides via the observable.
              </Text>
              <HStack>
                <Spacer />
                <Button title="Close" action={() => selection.setValue(null)} />
              </HStack>
            </VStack>,
          }}
        >
          <Circle fill="systemPurple" frame={{ width: 28, height: 28 }} />
          <Text font="caption2" foregroundStyle={"white"}>i</Text>
        </ZStack>
      </Annotation>
    </Map>
    <Text font={"caption2"} foregroundStyle={"tertiaryLabel"}>
      {`Apple's \`itemDetailSelectionAccessory\` is only for \`Marker(item:)\`. For
      \`<Annotation>\` you roll the card yourself with the same view modifier
      you'd use on any other view.`}
    </Text>
  </VStack>
}

function Example() {
  const dismiss = Navigation.useDismiss()

  return <NavigationStack>
    <ScrollView
      navigationTitle="Map Annotation"
      toolbar={{
        cancellationAction: <Button
          title="Close"
          action={dismiss}
        />
      }}
    >
      <VStack
        navigationTitle={"Annotation"}
        navigationBarTitleDisplayMode={"inline"}
        spacing={24}
        padding
      >
        <Text font={"caption"} foregroundStyle={"secondaryLabel"}>
          {`Custom-content map annotations. Use whenever a pin's visual needs to
          be something other than a stock MapKit marker glyph. Coexists with
          \`<Marker>\` in the same \`<Map>\` and shares the \`<Map selection>\` /
            \`tag\` selection mechanism.`}
        </Text>

        <CustomPinDemo />
        <AnchorPickerDemo />
        <TitleVisibilityDemo />
        <SelectionPopoverDemo />
      </VStack>
    </ScrollView>
  </NavigationStack>
}

async function run() {
  await Navigation.present({ element: <Example /> })
  Script.exit()
}

run()