Düğüm merkezlerini bir kez hesaplayarak, kartları özel bir SwiftUI Layout ile yerleştirerek ve bağlantı çizgilerini Canvas ile arkalarında çizerek bir Mac uygulamasında dallanmalı bir eleme tablosu diyagramı oluşturun. İki eksenli kaydırma görünümü, pencere içeriğinden küçük olduğunda diyagramın okunabilir kalmasını sağlar.

Bu eğitimde Node 1 gibi nötr etiketler içeren, yedi düğümlü sentetik bir diyagram oluşturuluyor. Dallar, sağdaki bir kökten sola doğru uzanıyor. Amaç, yerleşim ve çizimin aynı geometri tarafından yönlendirildiği, yeniden boyutlandırılabilir bir macOS penceresinde birbirine bağlı, eşit aralıklı kartlar göstermektir.

Kartları ve çizgileri hizalı tutan bir yerleşim yapısı seçin

Bu uygulama için macOS 13 veya üzerini hedefleyin. Apple, Layout özelliğinin macOS 13’ten itibaren kullanılabildiğini belgeliyor. Gerekli iki yöntemi, kapsayıcının boyutunu bildirir ve alt görünümlerini yerleştirir. Bu özellik, kartları hesaplanan konumlara yerleştirmek için uygundur. Layout

Düğüm kartlarını sıradan SwiftUI görünümleri olarak tutun. Bağlantı çizgileri için yalnızca arka planda bir Canvas kullanın. Apple, bir tuval içinde çizilen tek tek öğelerin, tuval sembolleri olarak sağlanan görünümler de dahil olmak üzere, kendi etkileşim veya erişilebilirlik özelliklerine sahip olmadığını açıklıyor. Kartları ayrı tutmak, ileride sıradan denetimler eklemeyi kolaylaştırır. Canvas

Uygulamanın üç parçası vardır: dalları tanımlayan bir ağaç, düğüm merkezlerini ve kenarları oluşturan bir geometri hesabı ve kartlarla bağlantı çizimini birleştiren bir görünüm. Her iki çizim katmanı da aynı geometri değerini alır. Böylece ağaç değiştiğinde kartlar güncellenirken bağımsız olarak tutulan bir bağlantı dizisinin geride kalması önlenir.

Boyutlandırma ilkesi bilerek sabittir: pencere değiştikçe kartlar aynı boyutta kalır ve çevrelerindeki görüntü alanı kaydırılır. Apple, ScrollView özelliğinin yatay, dikey veya her iki yönde kaydırmayı desteklediğini belgeliyor; yakınlaştırma sağlamaz. ScrollView

Yaprak aralıklarına göre dal konumlarını hesaplayın

Çocukları olmayan düğümler olan yapraklardan başlayın. Her yaprağa sırayla bir dikey yuva atayın. Ardından her ebeveyni, ilk ve son çocuğunun tam ortasına yerleştirin. Yatay konum derinliğe bağlıdır: kök en sağ sütunda bulunur ve her ek derinlik bir sütun sola ilerler.

Bu örnekte 132’ye 44 punto boyutunda kartlar, 64 puntoluk yatay aralık ve 28 puntoluk dikey aralık kullanın. Bunlar örnek tasarım değerleridir. Üç sütun ve dört yaprakla, dış kenar boşlukları hariç içerik 524’e 260 punto olur.

Miktar Hesaplama Sonuç
Sütun adımı Kart genişliği + yatay aralık 196 punto
Yaprak adımı Kart yüksekliği + dikey aralık 72 punto
İçerik genişliği 3 × 132 + 2 × 64 524 punto
İçerik yüksekliği 4 × 44 + 3 × 28 260 punto

Böylece dört yaprağın merkezlerinin dikey koordinatları 22, 94, 166 ve 238 olur. Doğrudan ebeveynleri 58 ve 202’de, kök ise 130’da bulunur. Bu koordinatlar kullanışlı bir tanılama ölçütü sağlar: ebeveyn, en dıştaki çocuk merkezlerinin ortasında olmalı ve her kart bildirilen içerik dikdörtgenine sığmalıdır.

Aşağıdaki algoritma, eşit olmayan dal derinliklerini de kabul eder. Sığ bir yaprak, otomatik olarak en sol sütuna taşınmak yerine kendi derinlik sütununda kalır. Bu, örneğe özgü bilinçli bir kuraldır. Koordinat formülünü değiştirmeden önce, diyagramınıza derinlik hizalamasının mı yoksa yaprak hizalamasının mı uygun olduğuna karar verin.

macOS örneğinin tamamını oluşturun

Dağıtım hedefi macOS 13 veya üzeri olan bir Xcode macOS SwiftUI uygulama hedefi kullanın. Oluşturulan uygulama giriş dosyasını aşağıdaki üç kod bloğuyla, tek bir Swift dosyasında ve verilen sırayla değiştirin. Birlikte programın tamamını oluştururlar. Eski uygulama bildirimini başka bir dosyadaysa kaldırın: Apple tam olarak bir uygulama giriş noktası olmasını gerektirir. App

Ağacı ve geometrisini tanımlayın

Her düğümün bir tamsayı tanımlayıcısı ve bir çocuk dizisi vardır. Geometri oluşturucu, ebeveynin merkezini hesaplamadan önce çocukları ziyaret ederek hem düğüm yerleşimlerini hem de kenarları toplar. Yinelenen bir tanımlayıcı, belirsiz bir görünüm koleksiyonu oluşturmak yerine bir önkoşul hatasına yol açar.

import SwiftUI

struct BranchNode {
    let id: Int
    let children: [BranchNode]

    init(_ id: Int, _ children: [BranchNode] = []) {
        self.id = id
        self.children = children
    }

    var depth: Int {
        (children.map { $0.depth + 1 }.max()) ?? 0
    }
}

struct DiagramNode: Identifiable {
    let id: Int
    let center: CGPoint
}

struct DiagramEdge {
    let child: CGPoint
    let parent: CGPoint
}

struct DiagramGeometry {
    static let cardSize = CGSize(width: 132, height: 44)
    static let horizontalGap: CGFloat = 64
    static let verticalGap: CGFloat = 28

    let nodes: [DiagramNode]
    let edges: [DiagramEdge]
    let size: CGSize

    static func make(root: BranchNode) -> DiagramGeometry {
        let maximumDepth = root.depth
        var nextLeaf = 0
        var nodes: [DiagramNode] = []
        var edges: [DiagramEdge] = []
        var identifiers = Set<Int>()

        func visit(_ node: BranchNode, depth: Int) -> CGPoint {
            precondition(
                identifiers.insert(node.id).inserted,
                "Every node must have a unique identifier."
            )

            let x = cardSize.width / 2
                + CGFloat(maximumDepth - depth)
                * (cardSize.width + horizontalGap)
            let childCenters = node.children.map {
                visit($0, depth: depth + 1)
            }

            let y: CGFloat
            if let first = childCenters.first,
               let last = childCenters.last {
                y = (first.y + last.y) / 2
            } else {
                y = cardSize.height / 2
                    + CGFloat(nextLeaf)
                    * (cardSize.height + verticalGap)
                nextLeaf += 1
            }

            let center = CGPoint(x: x, y: y)
            nodes.append(DiagramNode(id: node.id, center: center))
            for child in childCenters {
                edges.append(DiagramEdge(child: child, parent: center))
            }
            return center
        }

        _ = visit(root, depth: 0)
        let size = CGSize(
            width: CGFloat(maximumDepth + 1) * cardSize.width
                + CGFloat(maximumDepth) * horizontalGap,
            height: CGFloat(nextLeaf) * cardSize.height
                + CGFloat(max(0, nextLeaf - 1)) * verticalGap
        )
        return DiagramGeometry(nodes: nodes, edges: edges, size: size)
    }
}

Model her zaman bir kök içerir; bu nedenle çocuğu olmayan bir ağaç bile sıfırdan büyük içerik boyutuna sahip tek bir kart üretir. Bu programda ayrıca ele alınan boş diyagram durumu yoktur. Uygulamanız kök olmamasına izin veriyorsa, bu geometriyi oluşturmadan önce durumu açıkça ele alın.

Özel bir yerleşim değeriyle kartları konumlandırın

Her kartın merkezini bir LayoutValueKey üzerinden iletin. Apple’ın belgelediği yöntemde, layoutValue(key:value:) ile bir değer ayarlanır ve ardından anahtar kullanılarak alt görünüm vekilinden okunur. Böylece konum, paralel dizi indislerine bağlı kalmak yerine ilgili kartla ilişkilendirilir. LayoutValueKey

struct DiagramCenterKey: LayoutValueKey {
    static let defaultValue: CGPoint = .zero
}

struct BranchLayout: Layout {
    let contentSize: CGSize

    func sizeThatFits(
        proposal: ProposedViewSize,
        subviews: Subviews,
        cache: inout ()
    ) -> CGSize {
        contentSize
    }

    func placeSubviews(
        in bounds: CGRect,
        proposal: ProposedViewSize,
        subviews: Subviews,
        cache: inout ()
    ) {
        let cardProposal = ProposedViewSize(
            width: DiagramGeometry.cardSize.width,
            height: DiagramGeometry.cardSize.height
        )
        for subview in subviews {
            let center = subview[DiagramCenterKey.self]
            subview.place(
                at: CGPoint(
                    x: bounds.minX + center.x,
                    y: bounds.minY + center.y
                ),
                anchor: .center,
                proposal: cardProposal
            )
        }
    }
}

İçeriği esnek olmadığı için yerleşim, her öneri için aynı sonlu boyutu döndürür. Apple, esnek olmayan bir yerleşimin öneriyi yok sayabileceğini açıkça gösteriyor. SwiftUI, bir yerleşim geçişinde boyutlandırma yöntemini farklı önerilerle birkaç kez çağırabilir; bu nedenle bu yöntem yalnızca geometriyi döndürür ve modelde güncelleme yapmaz. sizeThatFits(proposal:subviews:cache:)

Yerleştirme, her yerel merkeze bounds.minX ve bounds.minY ekler. Sağlanan sınırlar, üst görünümün koordinat sisteminde ifade edilir; bu nedenle sıfır başlangıç noktasını varsaymak, kapsayıcı hareket ettiğinde yerleşimi bozar. Apple ayrıca boyutlandırma ve yerleştirme hesaplamalarının aralıklar ve boyutlar konusunda tutarlı olmasını gerektirir. placeSubviews(in:proposal:subviews:cache:)

.center kullanıldığında, sağlanan nokta her kartın ortasını temsil eder. Alt görünüm yerleştirme API’sinin gerektirdiği üzere, her vekil yerleştirme yöntemi içinde tam bir kez yerleştirilir. Sonraki bloktaki açık kart çerçeveleri, görüntülenen kartların geometri boyutlarıyla eşleşmesini sağlar. place(at:anchor:proposal:)

Bağlantıları çizin ve pencereyi gösterin

Her bağlantı, çocuk kartın sağ kenarından başlar ve ebeveyn kartın sol kenarında biter. İki ara nokta, aralığın ortasından geçen dik açılı bir yol oluşturur. Path.addLines(_:), nokta dizisinden birbirine bağlı düz çizgi parçaları oluşturur. addLines(_:)

struct BranchDiagram: View {
    let geometry: DiagramGeometry

    var body: some View {
        BranchLayout(contentSize: geometry.size) {
            ForEach(geometry.nodes) { node in
                Text("Node \(node.id)")
                    .font(.body)
                    .frame(
                        width: DiagramGeometry.cardSize.width,
                        height: DiagramGeometry.cardSize.height
                    )
                    .background(
                        RoundedRectangle(cornerRadius: 8)
                            .fill(Color.gray.opacity(0.12))
                    )
                    .overlay {
                        RoundedRectangle(cornerRadius: 8)
                            .stroke(Color.secondary, lineWidth: 1)
                    }
                    .layoutValue(
                        key: DiagramCenterKey.self,
                        value: node.center
                    )
            }
        }
        .background {
            Canvas { context, _ in
                var path = Path()
                for edge in geometry.edges {
                    let start = CGPoint(
                        x: edge.child.x
                            + DiagramGeometry.cardSize.width / 2,
                        y: edge.child.y
                    )
                    let end = CGPoint(
                        x: edge.parent.x
                            - DiagramGeometry.cardSize.width / 2,
                        y: edge.parent.y
                    )
                    let middleX = (start.x + end.x) / 2
                    path.addLines([
                        start,
                        CGPoint(x: middleX, y: start.y),
                        CGPoint(x: middleX, y: end.y),
                        end
                    ])
                }
                context.stroke(
                    path,
                    with: .color(.secondary),
                    lineWidth: 1.5
                )
            }
        }
    }
}

@main
struct BranchDiagramApp: App {
    let geometry = DiagramGeometry.make(
        root: BranchNode(1, [
            BranchNode(2, [BranchNode(4), BranchNode(5)]),
            BranchNode(3, [BranchNode(6), BranchNode(7)])
        ])
    )

    var body: some Scene {
        WindowGroup {
            ScrollView([.horizontal, .vertical]) {
                BranchDiagram(geometry: geometry)
                    .padding(24)
            }
            .frame(minWidth: 360, minHeight: 240)
        }
    }
}

ForEach, tanımlanabilir düğüm değerlerini alır; bu, Apple’ın tanımlanabilir öğeler veya açık bir tanımlayıcı bağımsız değişkeni kullanma gerekliliğiyle uyumludur. Ağacı daha sonra düzenlenebilir hâle getirirseniz tanımlayıcıları mantıksal düğümlere bağlı tutun. ForEach

Tuval doğrudan yerleşimin arka planına eklenir; böylece yerel koordinatları, kartlar için kullanılan geometriyle eşleşir. Diyagram bir kez oluşturulduktan sonra dış kenar boşluğu uygulanır ve her iki katman birlikte hareket eder. Kardeş bağlantıların ortak bölümleri üst üste gelerek köşeli parantez biçimli bir birleşim oluşturur.

WindowGroup, pencerenin görünüm hiyerarşisini ve standart macOS pencere yönetimi davranışını sağlar. Örnek, değişmez sentetik geometriyi uygulama yapısında tutar; görünümleri yeniden konumlandırmak için zamanlayıcılar veya yaşam döngüsü geri çağrıları kurmaz. WindowGroup

Diyagramı doğrulayın ve yaygın sorunları giderin

Bu örneğin aritmetiği, küçük ve deterministik bir Python hesabıyla kontrol edildi: yedi merkez, altı kenar, 524’e 260 puntoluk içerik dikdörtgeni ve bu dikdörtgenin içinde kalan tüm kartlar. SwiftUI programı incelendi ancak araştırma ortamında Swift derleyicisi bulunmadığından derlenemedi.

Uygulamayı Xcode’da oluşturduğunuzda şu gözlemlenebilir sonuçları kontrol edin:

  1. En soldaki sütunda dört, ortadaki sütunda iki, sağdaki sütunda bir kart görünür.
  2. Her bağlantı, ilgili kart kenarının yatay orta noktasına değerek biter.
  3. Her ebeveyn, en dıştaki çocuklarının dikey olarak ortasında yer alır.
  4. Pencere yeniden boyutlandırıldığında kart boyutları korunur ve gerektiğinde kaydırma görünür.
  5. Örnek kökü BranchNode(1) ile değiştirmek, bağlantısız tek bir kart gösterir.

Kartlar ve çizgiler kaymışsa önce değiştiricilerin uygulanma sırasını inceleyin. Yalnızca kartlara uygulanan kenar boşluğu veya ofsetler, bağlantı geometrisinin paylaşmadığı ikinci bir koordinat ayarı ekler. Örnekte gösterildiği gibi, diyagram genelindeki aralıkları BranchDiagram dışında tutun.

Bir kart yerleşimin başlangıç noktasında beklenmedik biçimde görünüyorsa son görünümünde DiagramCenterKey değerinin bulunduğunu kontrol edin. Anahtarın varsayılanı .zero değeridir; bu nedenle atamanın eksik olması kart merkezini başlangıç noktasına yerleştirir. Program tanımlayıcı önkoşulunda durursa, çizimden önce her düğüme benzersiz bir tanımlayıcı verin.

Sabit kart boyutları bu örnekteki kısa sentetik etiketler için uygundur. Daha uzun metinler veya daha büyük yazı tipleri için bir ölçüm aşaması ekleyin: alt görünüm vekillerinden boyutlarını isteyin, bu ölçümlere göre sütun genişliklerini ve alt ağaç aralıklarını hesaplayın ve bağlantı uçlarında da aynı sonuçları kullanın. Boyut önerisi tek başına alt görünümü bu boyutu kabul etmeye zorlamaz. place(at:anchor:proposal:)

Bu algoritma bir ağacı tanımlar: görüntülenen her çocuk tek bir ebeveyne bağlıdır. İsteğe bağlı çapraz bağlantıları yönlendirmez veya bir düğümün birden çok ebeveyn altında paylaşılmasını temsil etmez. Bu gereksinimler farklı bir geometri modeli ve açık bir yönlendirme politikası gerektirir. Benzer şekilde, animasyon için bağlantı geometrisinin kart konumlarıyla birlikte hareket etmesi gerekir; bu örnek statik yerleşim sunar.

İçerik boyutundan kart merkezlerine ve kenar uç noktalarına kadar her şeyi tek bir geometri hesabıyla yönetin. Böylece aralıkları ayarlayabileceğiniz, koordinatları inceleyebileceğiniz ve SwiftUI yerleşimiyle çizim arasındaki tutarlılığı koruyarak diyagramı genişletebileceğiniz somut bir noktanız olur.

Kaynaklar