För att hålla överlägg justerade mot ett SwiftUI-diagram som anpassats med aspect fit mäter du själva det anpassade diagrammet och placerar markörer i dess lokala koordinatsystem. Lägg till mätöverlägget före eventuell yttre ram som skapar tomt utrymme.

I den här handledningen bygger vi en liten macOS-app med ett syntetiskt diagram och två numrerade markörer. Målet är att hålla varje markör centrerad över den detalj i diagrammet som den hör till medan fönstrets form ändras. Implementeringen använder normaliserade koordinater, så annoteringarnas positioner är oberoende av fönstrets mått.

Varför glider överlägg bort från ett diagram som anpassats med aspect fit?

Aspect fit bevarar diagrammets proportioner när det anpassas till det tillgängliga utrymmet. Beroende på behållarens proportioner kan det lämna tomt utrymme ovanför och nedanför diagrammet eller på sidorna. Apple illustrerar detta beteende i Anpassa bilder till tillgängligt utrymme.

Överläggen glider när illustrationen och dess markörer använder olika rektanglar. Illustrationen kan till exempel uppta en centrerad rektangel som är 600 gånger 375 punkter i en behållare som är 600 gånger 700 punkter. Om en markör positioneras med en andel av behållarens höjd räknas det omgivande tomma utrymmet med.

Modifierarnas ordning avgör vilken rektangel du mäter. Apple dokumenterar att vymodifierare omsluter vyn de tar emot och att en ram placerar sitt underordnade element i en annan rektangel. Ett överlägg som läggs till efter en expanderande ram mäter därför en annan layoutgräns än ett som läggs till direkt på den anpassade illustrationen. Se Apples dokumentation om View och dokumentation om flexibel ram.

För den här uppgiften använder du följande ordning:

  1. Rita flexibel grafik.
  2. Ange dess uttryckliga bildförhållande med .fit.
  3. Lägg till ett överlägg som innehåller GeometryReader.
  4. Placera markörerna med hjälp av läsarens storlek.
  5. Lägg till den yttre ramen och omgivande utfyllnad.

Apple rekommenderar ett överlägg när den modifierade vyn ska styra layouten. Det gör diagrammet till en lämplig layoutägare, med annoteringar ovanpå. En syskonplacering i en ZStack har ett annat layoutbeteende och kräver större omsorg kring vilken vy som bestämmer storleken. Se overlay(alignment:content:).

Bygg ett komplett macOS-exempel

Skapa ett macOS-app-projekt i Xcode med gränssnittet SwiftUI. Ange macOS 12 eller senare som lägsta distributionsversion, eftersom den closure-baserade överläggs-API som används här är tillgänglig från macOS 12. Ersätt den genererade appens startpunktsfil med följande kompletta program och behåll endast en @main-appdeklaration i målet. API:ets tillgänglighet dokumenteras på overlay(alignment:content:).

Den syntetiska illustrationen motsvarar ett designutrymme på 800 gånger 500, vilket ger ett bildförhållande på 1,6. Markörernas koordinater är andelar av det utrymmet: horisontellt är noll den vänstra kanten, vertikalt är noll den övre kanten, och ett motsvarar den motsatta kanten. Inga tillgångsfiler eller externa paket behövs.

import SwiftUI

@main
struct DiagramAlignmentApp: App {
    var body: some Scene {
        WindowGroup {
            DiagramExample()
                .frame(minWidth: 360, minHeight: 260)
        }
    }
}

struct DiagramMarker: Identifiable {
    let id: Int
    let label: String
    let x: CGFloat
    let y: CGFloat
}

struct DiagramExample: View {
    let markers = [
        DiagramMarker(id: 1, label: "1", x: 0.25, y: 0.40),
        DiagramMarker(id: 2, label: "2", x: 0.75, y: 0.60)
    ]

    var body: some View {
        Rectangle()
            .fill(Color.white)
            .overlay {
                DiagramArtwork()
                    .stroke(Color.gray, lineWidth: 2)
            }
            .aspectRatio(800.0 / 500.0, contentMode: .fit)
            .overlay {
                GeometryReader { geometry in
                    ForEach(markers) { marker in
                        Circle()
                            .fill(Color.blue)
                            .frame(width: 28, height: 28)
                            .overlay {
                                Text(marker.label)
                                    .font(.caption.bold())
                                    .foregroundColor(.white)
                            }
                            .position(
                                x: marker.x * geometry.size.width,
                                y: marker.y * geometry.size.height
                            )
                    }
                }
            }
            .border(Color.gray)
            .frame(
                maxWidth: .infinity,
                maxHeight: .infinity,
                alignment: .center
            )
            .padding(24)
            .background(Color.gray.opacity(0.12))
    }
}

struct DiagramArtwork: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()

        path.addRect(CGRect(
            x: rect.minX + 0.10 * rect.width,
            y: rect.minY + 0.20 * rect.height,
            width: 0.30 * rect.width,
            height: 0.40 * rect.height
        ))

        path.addRect(CGRect(
            x: rect.minX + 0.60 * rect.width,
            y: rect.minY + 0.40 * rect.height,
            width: 0.30 * rect.width,
            height: 0.40 * rect.height
        ))

        path.move(to: CGPoint(
            x: rect.minX + 0.25 * rect.width,
            y: rect.minY + 0.40 * rect.height
        ))
        path.addLine(to: CGPoint(
            x: rect.minX + 0.75 * rect.width,
            y: rect.minY + 0.60 * rect.height
        ))

        return path
    }
}

Kör appen och ändra fönstrets storlek både horisontellt och vertikalt. Markör 1 ska ligga kvar i mitten av den första rektangeln, och markör 2 i mitten av den andra. Den tunna kanten visar det anpassade diagrammets gräns; det grå området runtom tillhör den yttre layouten.

Därför fungerar den här ordningen på modifierarna

Grundrektangeln tillhandahåller flexibel grafik, och .aspectRatio(800.0 / 500.0, contentMode: .fit) begränsar dess proportioner. Apple definierar argumentet för bildförhållande som bredd delat med höjd och anger att det anpassade resultatet gäller för storleksändringsbart innehåll. Se aspectRatio(_:contentMode:).

Mätöverlägget kommer direkt efter den begränsningen. GeometryReader definierar sitt innehåll utifrån sin egen storlek och sitt eget koordinatsystem, medan geometry.size rapporterar behållarens storlek. Här täcker den behållaren den anpassade rektangeln. Om du multiplicerar normaliserade koordinater med dess bredd och höjd får du därför lokala markörpositioner. Se GeometryReader och GeometryProxy.size.

Den avslutande expanderande ramen centrerar den färdiga kombinationen av diagram och markörer inom det tillgängliga området. Eftersom markörerna redan är kopplade till den inre rektangeln behöver de inte beräkna de yttre marginalerna.

Bevara kontrakten för ritning och positionering

DiagramArtwork följer protokollet Shape, och dess metod path(in:) returnerar en Path uttryckt i den angivna rektangeln. Den returnerar inte en vy och utför inga layoutändringar. Det följer Apples dokumenterade kontrakt för path(in:).

Varje markörs .position(x:y:) placerar dess mittpunkt vid den beräknade punkten. Den placerar inte det övre vänstra hörnet där. Exemplet förankrar avsiktligt mitten av varje numrerad cirkel vid en detalj, i linje med Apples definition av position(x:y:).

Cirklarna förblir 28 punkter breda när fönstret ändrar storlek, medan deras mittpunkter följer illustrationen. Det är ett medvetet presentationsval för att annoteringarna ska vara läsbara. Om markörernas mått motsvarar avstånd i teckningen beräknar du även de måtten utifrån den anpassade storleken.

Kontrollera justeringen med ett genomräknat koordinatexempel

En numerisk kontroll hjälper dig att skilja ett koordinatfel från ett stilproblem. Vid centrerad aspect fit kan du låta de positiva källdimensionerna vara sourceWidth och sourceHeight, och de ändliga tillgängliga dimensionerna vara containerWidth och containerHeight. Följande är matematisk pseudokod, inte ett separat Swift-program:

scale = min(containerWidth / sourceWidth,
            containerHeight / sourceHeight)

fittedWidth = sourceWidth * scale
fittedHeight = sourceHeight * scale

originX = (containerWidth - fittedWidth) / 2
originY = (containerHeight - fittedHeight) / 2

containerX = originX + normalizedX * fittedWidth
containerY = originY + normalizedY * fittedHeight

Dessa ekvationer härleder en enhetlig anpassning och centrerad placering utifrån det dokumenterade anpassningsbeteendet. Om vi använder källan på 800 gånger 500 som exempel och placerar markören 1 vid (0.25, 0.40) får vi följande resultat. Behållarens mått avser här det tillgängliga området efter omgivande utfyllnad.

Tillgänglig behållare Anpassat diagram Diagrammets ursprungspunkt i behållaren Markörens mittpunkt i behållaren
1000 × 700 1000 × 625 (0, 37.5) (250, 287.5)
600 × 700 600 × 375 (0, 162.5) (150, 312.5)
1000 × 400 640 × 400 (180, 0) (340, 160)

I mittraden ger en multiplikation av hela behållarens höjd med 0.40 resultatet 280, vilket missar den korrekta vertikala positionen 312.5. I det lokala överlägget i exemplet är den vertikala koordinaten helt enkelt 0.40 × 375 = 150; den omslutande layouten står för marginalen på 162.5 punkter.

Tabellens beräkningar kontrollerades med en liten deterministisk Python-beräkning. SwiftUI-programmet granskades men kompilerades inte i den här miljön; kontrollera återgivningen i Xcode på den aktuella Mac-datorn.

Anpassa mönstret till ett befintligt diagram

För ett bildbaserat diagram gör du Image storleksändringsbar innan du lägger till modifierarna för aspect fit och överlägg. Apples guide för bildanpassning förklarar att en bild annars återges i sin ursprungliga storlek och att enbart .resizable() kan skala axlarna oberoende av varandra. Bevara bildens faktiska bildförhållande och lägg sedan till marköröverlägget före den yttre ramen. Se Anpassa bilder till tillgängligt utrymme.

När annoteringar utgår från källkoordinater normaliserar du dem mot samma fullständiga bildgränser: dividera källans horisontella koordinat med källans bredd och den vertikala koordinaten med källans höjd. Transparanta marginaler i en bild ingår fortfarande i dessa gränser. Om koordinaterna har mätts mot ett beskuret område omvandlar du dem till hela bildens koordinatsystem innan du använder det här mönstret.

Behåll bildspecifika modifierare på Image innan allmänna vymodifierare omsluter den. Behåll på samma sätt var body: some View för den sammansatta vyn och returtypen Path för formens ritmetod. Apple dokumenterar anpassad vykomposition och hur modifierare omsluter vyer i View.

Beräkningen förutsätter en enhetlig anpassning utan ytterligare beskärning, rotation eller separat panorering och zoomning. Om du inför sådana operationer tillämpar du en gemensam omvandling på illustrationen och annoteringarna eller utökar koordinatmappningen uttryckligen. Om du byter till .fill ändras anpassningsvillkoren och innehåll kan återges utanför ramen; Apple dokumenterar beskärning som en separat åtgärd i Anpassa bilder till tillgängligt utrymme.

Felsök kvarvarande justeringsproblem

Kontrollera följande villkor i ordning:

  1. Mätgräns: Låt marköröverlägget ligga före den yttre expanderande ramen. Dess kant ska följa illustrationen, inte det omgivande tomma utrymmet.
  2. Koordinatgränser: Bekräfta att normaliserade koordinater använder hela källrektangeln och det avsedda origo i det övre vänstra hörnet. I det här exemplet går avsedda värden inom gränserna från noll till ett; avvisa oväntade värden i stället för att tyst begränsa dem.
  3. Förankring: Kom ihåg att .position(x:y:) förankrar markörens mittpunkt. Om ett hörn på en etikett ska nudda en detalj behövs en uttrycklig extra förskjutning eller en annan förankringsstrategi.
  4. Små layouter: En markör kan ligga korrekt centrerad samtidigt som den överlappar en annan markör. Välj en separat regel för att dölja eller flytta om etiketter vid små storlekar.
  5. Beteende vid kanter: En centrerad markör vid en kant på illustrationen sticker utanför den kanten. Avgör om det är acceptabelt innan du lägger till beskärning.

Beräkna positionerna utifrån den aktuella geometrin i stället för att spara den första uppmätta storleken i .onAppear. Apple definierar återanropet utifrån vyns framträdande, vars tidpunkt beror på vytypen; det är inget återanrop när fönstret ändrar storlek. Om du läser geometrin direkt kopplas positioneringsberäkningen i det här exemplet till den aktuella layouten. Se onAppear(perform:).

För ett anpassat diagram i ett Mac-fönster som kan ändra storlek är en gemensam rektangel en tillförlitlig utgångspunkt: låt illustrationen fastställa sina anpassade gränser, mät dem och beräkna varje annoterings mittpunkt utifrån dem. Lägg till omgivande layout först när det sambandet är etablerat.

Källor