Mac uygulamanızda sürüklenen bir şeklin hareketini hareket boyunca önizleyin ve son konumu tek, kısa bir geri alma grubunda kaydedin. Modeli ve geri alma yöneticisini tek bir sahibin denetiminde tutun, her grup açılışını bir kapanışla eşleştirin ve yinelemenin de çalışması için ters hareketi kaydedin.

Bu öğretici, sentetik ve bellekte tutulan tek bir daire içeren küçük bir SwiftUI tuvali oluşturur. Amaç, kaç hareket güncellemesi gelirse gelsin tamamlanan tek bir sürüklemenin tam olarak bir geri alma gerektirmesini sağlamaktır. Ayrıca aynı sınırların AppKit görünümünde nasıl korunacağını açıklar.

Sürükleme boyunca geri alma grubunu açık tutmamak neden gerekir

Sürükleme, zamana yayılan bir etkileşimdir. Geri alma grubu, geri alınabilir işlemleri toplar. Bu sınırların aynı yaşam süresine sahip olması gerekmez. Apple, groupsByEvent varsayılanının true olduğunu ve işlemleri olay döngüsü geçişleri çevresinde gruplandırdığını belgeliyor; ancak bu davranış, sürüklemenin tamamını kapsayan bir işlem tanımlamaz. Açık gruplar mevcut grupların içine de yuvalanabilir. Apple’ın otomatik olay gruplandırması ve geri alma gruplarını başlatma belgelerine bakın.

İlk SwiftUI hareket geri çağrısında grup açıp onEnded içinde kapatmak iptal durumunda bir boşluk bırakır. Apple, onEnded işlevinin yalnızca hareket başarıyla sona erdiğinde çalıştığını açıkça belirtiyor. Buna karşılık, updating üzerinden tutulan durum hareket bittiğinde veya iptal edildiğinde sıfırlanır. Eşleşmeyen bir endUndoGrouping() özel duruma yol açar; bu nedenle bir geri çağrının grup açıp açmadığını tahmin etmek de uygun değildir. onEnded(_:), updating(_:body:) ve endUndoGrouping() belgelerine bakın.

Bu tuval için aşağıdaki tasarımı kullanın. Bu, belgelenen sınırlardan çıkarılmış bir uygulama tercihidir:

Etkileşim aşaması Görüntü durumu Kaydedilmiş model Geri alma gruplandırması
Sürüklemeden önce Kaydedilmiş merkez Başlangıç merkezi Kapalı
Hareket güncellemeleri Başlangıç merkezi artı geçici öteleme Değişmez Kapalı
Başarılı tamamlanma Son merkez Bir kez güncellenir Eşzamanlı açılır ve kapanır
İptal Önizleme sıfırlanır Değişmez Hiç açılmaz
Geri alma veya yineleme Merkez geri yüklenir Geri alınabilir ayarlayıcı üzerinden güncellenir UndoManager tarafından yönetilir

Bu yaklaşım, her önizleme karesini kalıcı bir düzenleme saymadan dairenin sürekli hareket etmesini sağlar. Tamamlanan bir sürükleme, adı olan tek bir model işlemi hâline gelir.

Geri alma geçmişinin sahibini belirleyin

Örnek, her tuvale kendi modelini ve özel geri alma yöneticisini verir. Bu sahip tüm kayıtları denetlediği için başlangıçta groupsByEvent = false değerini bir kez ayarlar ve kaydedilen her hareketi açıkça gruplandırır. Apple, otomatik gruplandırma devre dışı bırakıldığında geri almadan önce grupların açıkça kapatılmasını zorunlu tutar. Örnek, kaydetme yönteminden dönmeden önce grubu kapatır. groupsByEvent belgesine bakın.

Mevcut bir belge düzenleyicisi, belgenin geri alma yöneticisine zaten sahip olabilir. SwiftUI, bağlama duyarlı yöneticiyi @Environment(\.undoManager) üzerinden sunar. Apple, bağlam geri almayı desteklemiyorsa değerin nil olabileceğini belgeliyor. Böyle bir mimaride belgenin yöneticisini ve yerleşik gruplandırma politikasını kullanmaya devam edin; bu örneğin özel yönetici yapılandırmasını paylaşılan bir yöneticiye kopyalamayın. EnvironmentValues.undoManager belgesine bakın.

Otomatik gruplandırmanın etkin olduğu paylaşılan bir yöneticiyle, tamamlanma sırasında tek bir ters hareket kaydetmek yine de her hareket için ayrı geçmiş girdileri oluşmasını önler. Açık bir alt grup hareketi bir araya getirebilir, ancak onu çevreleyen gruptan bağımsız kılmaz. Başka düzenlemeler de bu grubu paylaşıyorsa birlikte geri alınabilirler. Bağımsız bir üst düzey işlem garantisi, alt görünümün grupları kapatmasını değil, paylaşılan geçmişin sahibinin eşgüdümünü gerektirir.

Tam SwiftUI örneğini oluşturun

Dağıtım hedefi macOS 14 veya üzeri olan bir macOS SwiftUI uygulama projesi oluşturun. Oluşturulan uygulama bildirimiyle içerik görünümünü aşağıdaki iki dosyayla değiştirin. Yalnızca bir @main bildirimi bırakın. Örnek, sahipliği açıkça göstermek için yerel Undo ve Redo düğmelerini kullanır; uygulama menüsü ve yanıtlayıcı zinciri tümleştirmesi ayrı bir tümleştirme adımıdır.

Model, geri alınabilir bir ayarlayıcı kullanır: mevcut konumu yakalar, onu geri yükleyen bir kapanış kaydeder, ardından yeni konumu uygular. Bu kapanış geri alma sırasında çalıştığında aynı ayarlayıcı ters işlemi yineleme olarak kaydeder. Apple, Geri Alma İşlemlerini Kaydetme belgesinde bu karşılıklı kayıt düzenini gösteriyor. Güncel belgeler ayrıca UndoManager ve kayıt işleyicisinin ana aktöre yalıtıldığını belirtiyor; bu nedenle örnek modelde @MainActor kullanılır. UndoManager belgesine bakın.

Aşağıdaki eksiksiz modeli ShapeModel.swift dosyasına koyun:

import Foundation
import Combine

@MainActor
final class ShapeModel: ObservableObject {
    @Published private(set) var center = CGPoint(x: 120, y: 100)
    @Published private(set) var canUndo = false
    @Published private(set) var canRedo = false

    private let history: UndoManager

    init() {
        let manager = UndoManager()
        manager.groupsByEvent = false
        history = manager
    }

    func commitDrag(translation: CGSize) {
        let destination = CGPoint(
            x: center.x + translation.width,
            y: center.y + translation.height
        )
        guard destination != center else { return }

        // This owner expects no caller-owned group.
        precondition(history.groupingLevel == 0)
        history.beginUndoGrouping()
        defer {
            history.endUndoGrouping()
            refreshAvailability()
        }

        replaceCenter(with: destination)
    }

    private func replaceCenter(with destination: CGPoint) {
        let previous = center
        guard previous != destination else { return }

        history.registerUndo(withTarget: self) { target in
            target.replaceCenter(with: previous)
        }
        history.setActionName("Move Shape")
        center = destination
    }

    func undo() {
        precondition(history.groupingLevel == 0)
        guard history.canUndo else { return }
        history.undo()
        refreshAvailability()
    }

    func redo() {
        precondition(history.groupingLevel == 0)
        guard history.canRedo else { return }
        history.redo()
        refreshAvailability()
    }

    private func refreshAvailability() {
        canUndo = history.canUndo
        canRedo = history.canRedo
    }
}

defer kasıtlı olarak beginUndoGrouping() ile aynı eşzamanlı kapsamda yer alır. Swift, erken dönüş veya fırlatılan hata dâhil, kapsamdan çıkılırken ertelenen eylemleri yürütür. Bu yapı, ayrı ayrı çağrılan hareket geri çağrıları boyunca uzatılamaz. Bu eşleştirme, normal denetim akışı çıkışlarında modelin grubunun dengeli kalmasını sağlar. Denetim Akışı — Ertelenen Eylemler belgesine bakın.

Geri alma kapanışı self değerini yakalamak yerine target parametresi üzerinde çalışır. Apple, yöneticinin hedefe sahip olmayan bir başvuru tuttuğunu belgeliyor ve güçlü başvuru döngülerini önlemek için sağlanan parametrenin kullanılmasını öneriyor. Tuval, kullanımdayken modeli elde tutar; model de geçmişini yönetir. registerUndo(withTarget:handler:) belgesine bakın.

Uygulamayı ve tuvali ShapeUndoApp.swift dosyasına koyun:

import SwiftUI

@main
@MainActor
struct ShapeUndoApp: App {
    var body: some Scene {
        WindowGroup {
            ShapeCanvas()
        }
    }
}

@MainActor
struct ShapeCanvas: View {
    @StateObject private var model = ShapeModel()
    @GestureState private var previewTranslation: CGSize? = nil

    private var drag: some Gesture {
        DragGesture(
            minimumDistance: 1,
            coordinateSpace: .named("board")
        )
        .updating($previewTranslation) { value, state, _ in
            state = value.translation
        }
        .onEnded { value in
            model.commitDrag(translation: value.translation)
        }
    }

    var body: some View {
        let translation = previewTranslation ?? .zero
        let isDragging = previewTranslation != nil

        VStack(spacing: 16) {
            ZStack(alignment: .topLeading) {
                Color.clear

                Circle()
                    .fill(Color.blue)
                    .frame(width: 44, height: 44)
                    .position(
                        x: model.center.x + translation.width,
                        y: model.center.y + translation.height
                    )
                    .gesture(drag)
            }
            .frame(width: 420, height: 300)
            .background(Color.gray.opacity(0.12))
            .coordinateSpace(name: "board")
            .clipped()

            HStack {
                Button("Undo") {
                    guard previewTranslation == nil else { return }
                    model.undo()
                }
                .disabled(isDragging || !model.canUndo)

                Button("Redo") {
                    guard previewTranslation == nil else { return }
                    model.redo()
                }
                .disabled(isDragging || !model.canRedo)
            }
        }
        .padding(20)
    }
}

Hareket, adlandırılmış tek bir tuval koordinat alanı kullanır; böylece öteleme, hareket eden daireye göre değil tahtaya göre ölçülür. Apple, translation değerini hareketin başlangıcından itibaren toplam yer değiştirme olarak tanımlar. Bunu önizlemeye atayın; hareket sırasında modele tekrar tekrar eklemeyin. DragGesture ve translation belgelerine bakın.

İsteğe bağlı hareket durumu, önizlemenin etkin olup olmadığını da gösterir. Önizleme sırasında yerel geçmiş düğmelerinin ikisi de devre dışı bırakılır. Başarılı tamamlanmada son hareket değeri, kaydedilecek ötelemeyi sağlar. İptalde hareket durumu sıfırlanır ve kaydedilmiş merkez değişmeden kalır; temizlenmesi gereken açık bir grup yoktur.

Bu örnekte bilerek tek bir yazıcı vardır. Sürükleme sırasında başka hiçbir komut daireyi değiştirmez. Klavye hareketi, silme veya başka düzenleme komutları eklerseniz, sürükleme sırasında her komutun kullanılamaz mı olacağını yoksa önce önizlemeyi mi iptal edeceğini belirleyin. Tüm komutları bu politikaya yönlendirin; yalnızca bu iki düğmeyi devre dışı bırakmak ek komut yollarını yönetemez.

Tek sürüklemeyi, tek geri almayı ve çalışan yinelemeyi doğrulayın

Hesaplamayı incelemek için örnek koordinatlar kullanın. (120, 100) noktasından başlayan hareketin kümülatif (10, 5), (25, 12) ve (40, 20) ötelemeleri, (130, 105), (145, 112) ve (160, 120) merkezlerinde önizlemeler oluşturur. Kaydedilmiş merkez, hareket tamamlanana kadar (120, 100) olarak kalır ve ardından bir kez (160, 120) olur.

Geri alma, kayıtlı ayarlayıcıyı (120, 100) ile çağırır. Ayarlayıcı bu konumu geri yüklemeden önce (160, 120) değerini yakalar ve yineleme için kaydeder. Geri alma geri çağrısında kaydı bastırmayın: yinelemeyi koruyan, bu karşılıklı kayıttır. Apple bu davranışı Geri Alma İşlemlerini Kaydetme belgesinde açıklıyor.

Kod, geri alınabilir ayarlayıcının içinde ek bir açık grup oluşturmaz. Geçmiş yeniden oynatılırken gruplandırmayı UndoManager denetler; Apple, redo() değerinin yeniden oynatılan işlemleri geri alma yığınına tek bir grup olarak kaydettiğini belgeliyor. Eylem adı, sentetik Move Shape etiketi kullanılarak setActionName üzerinden atanır. redo() ve setActionName(_:) belgelerine bakın.

Mac uygulamasında şu davranışları kontrol edin:

  1. Birkaç kez yön değiştirerek tek bir sürüklemeyi tamamlayın. Tek bir Undo, şekli başlangıç konumuna döndürmelidir.
  2. Redo seçin. Şekil, tamamlanan sürüklemenin son konumuna dönmelidir.
  3. Birbirinden ayrı iki sürüklemeyi tamamlayın. İki geri alma işlemi, önceki iki konumu ters sırayla geri yüklemelidir.
  4. Tanınan bir sürüklemeyi başlangıç noktasında bitirin. Değişiklik yok denetimi geçmiş girdisi oluşturmamalıdır.
  5. İçeren uygulamada iptal veya görünüm kaldırma yolunu deneyin. Kaydedilmiş bir hareket olmadan önizleme kaybolmalıdır.
  6. Bir hareketi geri alın, ardından farklı bir hareketi tamamlayın. Önceki yineleme dalı kaybolmalıdır; Apple, yeni bir geri alma kaydının yineleme yığınını temizlediğini belgeliyor.

Son davranış, Apple’ın Geri Alma Yöneticisi Mimarisi belgesini izler. Yukarıdaki koordinat hesabı, küçük ve deterministik bir Python hesaplamasıyla kontrol edildi. Bu ortamda Swift derleyicisi veya macOS SDK bulunmadığından Swift kaynağı incelendi ancak derlenip çalıştırılmadı.

Aynı sınırları AppKit tuvaline uygulayın

Mevcut bir NSView için AppKit’in olay sırasını koruyun. Apple’ın güncel başvuru belgesi mouseDragged(with:) değerini sol düğme basılıyken gerçekleşen hareket, mouseUp(with:) değerini ise bu düğmenin bırakılması olarak tanımlıyor. Arşivlenmiş olay kılavuzu, özel bir görünüm için gereken üç yöntemli sıralamayı ve koordinat dönüşümünü açıklıyor. mouseDragged(with:), mouseUp(with:) ve Fare Olaylarını İşleme belgelerine bakın.

Aynı tasarımı bu yöntemlere uyarlayın:

  • mouseDown(with:) içinde sentetik şekil üzerinde isabet testi yapın ve başlangıç merkezini, işaretçinin tuval koordinatlarına dönüştürülmüş konumuyla birlikte kaydedin.
  • mouseDragged(with:) içinde, kaydedilen işaretçi konumundan yer değiştirmeyi hesaplayıp geçici bir önizleme çizin.
  • mouseUp(with:) içinde son yer değiştirmeyi hesaplayın, izleme durumunu temizleyin ve modelin eşzamanlı kaydetme yöntemini bir kez çağırın.
  • Tuval sahibinin iptal veya temizleme yolunda, önizlemeyi atın ve kaydetmeden izleme durumunu temizleyin.

Etkileşim için tek bir olay sahibi kullanın. SwiftUI, AppKit tuvalini gömüyorsa hareketi ve kaydı SwiftUI hareketinin mi yoksa AppKit fare işleyicilerinin mi yöneteceğini seçin. Her ikisinin de aynı hareketi kaydetmesi yinelenen işlemler oluşturur. Ters çevrilmiş koordinat sistemi veya tuval dönüşümü dâhil olmak üzere çizim koordinatlarını tutarlı kullanın.

Apple’ın arşivlenmiş kılavuzu, üç yöntemli izleme yaklaşımı sırasında klavye komutlarının da gönderilebileceğini açıklar. Bu nedenle her türlü menü tümleştirmesi, yerel düğmelerle aynı etkin sürükleme politikasına uymalıdır. Belge geçmişini belge sahibinde tutun ve menü eylemlerinin o sahibin yöneticisini hedeflediğinden emin olun.

Başkasının grubunu kapatmadan gruplandırma hatalarını giderin

Bir sürükleme çok sayıda geri alma adımı oluşturuyorsa, model ayarlayıcılarının hareket güncellemeleri sırasında kayıt yapıp yapmadığını kontrol edin. Bu kayıtları son kaydetme aşamasına taşıyın. Birkaç sürükleme birlikte geri alınıyorsa, yöneticinin otomatik gruplandırma politikasını ve sahibi tarafından oluşturulmuş çevreleyen grupları inceleyin. Geri almadan sonra yineleme kullanılamıyorsa, geri yükleme yolunun ters işlemi kaydedip kaydetmediğini inceleyin.

Gruplandırma özel durum oluşturuyorsa, kapatma çağrılarının sayısını değiştirmeden önce sahipliği inceleyin. Apple, groupingLevel == 0 değerinin açık grup bulunmadığı anlamına geldiğini ve birden fazla grup açık kaldığında undo() değerinin özel durum oluşturduğunu belgeliyor. Grup düzeyi sıfıra ulaşana dek kapatan bir döngü, başka bir çağırana ait grupları kapatabilir. Düzeyi tanılama amacıyla kullanın ve sahibin sorumluluğunu uygulayın. groupingLevel ve undo() belgelerine bakın.

Bu bellek içi tuval için yararlı sınır, kaydedilen harekettir: önizlemeyi serbestçe güncelleyin, bir kez kaydedin, geri alınabilir bir değişiklik kaydedin ve dönmeden önce grubu kapatın. Böylece iptal, olay teslimi ve geçmiş sahipliği açık kalırken tamamlanan her sürükleme tek bir geri alma adımı oluşturur.

Kaynaklar