Bina editor Mac dengan menyimpan teks yang diterima dalam paparan induk dan mengedit draf berasingan dalam helaian. Hanya Commit menyalin draf kembali; Cancel menutup helaian. Tutorial ini melaksanakan aliran tersebut untuk nota teks biasa yang ringkas dalam aplikasi anda sendiri.

Tentukan tindakan commit dan cancel

Tugas pembaca mudah: buka nota sedia ada, semak semula beberapa baris, kemudian sama ada terima keseluruhan semakan itu atau biarkan kandungan asal tidak berubah. Contoh ini menggunakan teks sintetik dan satu sesi pengeditan. Teks yang diterima disimpan dalam memori; penambahan storan fail ialah keputusan aplikasi yang berasingan.

TextEditor Apple memaparkan teks berbilang baris yang boleh ditatal dan menerima binding kepada nilai yang sedang diedit. Oleh itu, binding kepada nilai yang diterima membolehkan suntingan mengubah nilai itu serta-merta. Untuk melaksanakan penerimaan tertangguh, ikat editor kepada draf. Lihat dokumentasi TextEditor Apple.

Reka bentuk ini mempunyai tiga bahagian: teks yang diterima dan dimiliki oleh induk, petikan yang dibuat apabila pengeditan bermula, serta keadaan draf boleh ubah yang dimiliki oleh helaian. String dalam Swift mempunyai semantik nilai, jadi perubahan pada draf tidak mengubah rentetan yang diterima. Storan copy-on-write ialah pengoptimuman pelaksanaan; tingkah laku nilai bebas tetap sama. Lihat String.

Peristiwa Tingkah laku draf Teks yang diterima
Buka editor Bermula daripada teks yang diterima sekarang Tidak berubah
Taip atau buang teks Berubah dalam helaian Tidak berubah
Pilih Commit Dihantar kepada induk, kemudian helaian ditutup Digantikan dengan draf
Pilih Cancel Helaian ditutup tanpa menghantar draf kembali Tidak berubah
Buka semula editor Memulakan sesi baharu Membekalkan teks permulaan baharu

Jadual ini juga merupakan kontrak pelaksanaan. Menutup helaian tidak dengan sendirinya bermaksud penerimaan. Dengan menjelaskan perbezaan itu, pembatalan dapat berfungsi tanpa operasi undur.

Cipta contoh macOS yang lengkap

Cipta projek aplikasi macOS dalam Xcode menggunakan SwiftUI, dan tetapkan sasaran penggunaan kepada macOS 12 atau lebih baharu. Gantikan pengisytiharan aplikasi dan paparan kandungan yang dijana dengan fail sumber tunggal berikut, kemudian buang pengisytiharan aplikasi lama supaya sasaran itu mengandungi tepat satu titik masuk @main. Contoh ini menggunakan pengeditan teks biasa, bukannya pemula teks beratribut yang lebih baharu.

Keperluan minimum macOS 12 berpunca daripada DismissAction helaian; TextEditor teks biasa tersedia mulai macOS 11. Apple mendokumenkan sempadan ketersediaan ini dalam DismissAction dan TextEditor.

import SwiftUI
import Foundation

@main
struct DraftEditorApp: App {
    var body: some Scene {
        WindowGroup {
            NoteView()
        }
    }
}

private struct EditSession: Identifiable {
    let id = UUID()
    let initialText: String
}

private struct NoteView: View {
    @State private var acceptedText = """
    Sample note
    Arrange the blue tiles beside the green tiles.
    """
    @State private var editSession: EditSession?

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            Text("Accepted note")
                .font(.headline)

            ScrollView {
                Text(acceptedText.isEmpty ? "(Empty note)" : acceptedText)
                    .frame(maxWidth: .infinity, alignment: .leading)
            }

            Button("Edit Note") {
                editSession = EditSession(initialText: acceptedText)
            }
        }
        .padding(20)
        .frame(minWidth: 460, minHeight: 300)
        .sheet(item: $editSession) { session in
            DraftEditor(initialText: session.initialText) { newText in
                acceptedText = newText
            }
            .id(session.id)
        }
    }
}

private struct DraftEditor: View {
    @Environment(\.dismiss) private var dismiss
    @State private var draft: String

    private let initialText: String
    private let onCommit: (String) -> Void

    init(
        initialText: String,
        onCommit: @escaping (String) -> Void
    ) {
        self.initialText = initialText
        self.onCommit = onCommit
        _draft = State(initialValue: initialText)
    }

    private var hasChanges: Bool {
        draft != initialText
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            Text("Edit note")
                .font(.headline)

            TextEditor(text: $draft)
                .font(.body)
                .accessibilityLabel("Note draft")
                .frame(minHeight: 220)

            Text(hasChanges ? "Draft changed" : "No changes")
                .font(.caption)
                .foregroundColor(.secondary)

            HStack {
                Spacer()

                Button("Cancel") {
                    dismiss()
                }
                .keyboardShortcut(.cancelAction)

                Button("Commit") {
                    onCommit(draft)
                    dismiss()
                }
                .keyboardShortcut(.return, modifiers: .command)
            }
        }
        .padding(20)
        .frame(minWidth: 480, minHeight: 340)
    }
}

Ini ialah contoh aplikasi lengkap yang merangkumi teks awal, keadaan persembahan, editor dan titik masuknya. Pengkompil Swift dan SDK macOS tidak tersedia dalam persekitaran penyelidikan, jadi contoh ini disemak berdasarkan dokumentasi API yang diperoleh dan bukannya dikompil atau dijalankan di sini.

Sebab induk memiliki nilai yang diterima

Induk memiliki acceptedText kerana ia memaparkan hasil yang diterima dan membekalkan nilai permulaan bagi sesi pengeditan seterusnya. Anak memiliki draft kerana hanya antara muka pengeditan perlu mengubahnya. Ini selaras dengan panduan Apple supaya keadaan dijadikan peribadi dan diletakkan pada aras hierarki paparan yang sesuai. Lihat State.

Induk tidak menghantar $acceptedText kepada editor. Sebaliknya, ia menghantar nilai rentetan dan closure. Closure itu mengandungi satu-satunya tugasan yang menerima semakan: acceptedText = newText. Dalam pelaksanaan ini, tindakan menaip mahupun membatalkan tidak boleh mencapai tugasan tersebut.

Sebab setiap pembukaan mendapat pengecam sesi

EditSession pilihan menyediakan keadaan persembahan dan teks awal. Apple mendokumenkan bahawa sheet(item:onDismiss:content:) mempersembahkan kandungan apabila item terikatnya bukan nil, lalu menghantar item itu kepada closure kandungan. Lihat sheet(item:onDismiss:content:).

Setiap kali Edit Note ditekan, pengecam baharu dicipta. Menggunakan pengecam itu pada editor menjadikan jangka hayat keadaan yang dimaksudkan jelas: satu draf milik satu sesi pengeditan. Pemula membekalkan nilai keadaan awal; ia bukan mekanisme untuk menyegerakkan draf sedia ada secara berterusan dengan perubahan induk yang berlaku kemudian.

Untuk tugas ini, tingkah laku petikan itu memang dikehendaki. Nota yang sedang disunting tidak sepatutnya tiba-tiba ditetapkan semula hanya kerana bahagian lain paparan induk dikemas kini. Apabila editor dibuka semula, draf baharu bermula daripada teks yang paling terkini diterima.

Tetapkan tingkah laku penutupan dan papan kekunci dengan sengaja

Baca @Environment(\.dismiss) di dalam editor yang dipersembahkan, seperti dalam contoh. Apple menjelaskan bahawa tindakan itu terpakai pada persekitaran tempat ia diisytiharkan. Membacanya dalam paparan akar yang mempersembahkan editor boleh menyasarkan tetingkap paparan itu dan bukannya helaian. Lihat DismissAction.

Butang Cancel hanya menutup helaian. Draf boleh dibuang kerana ia tidak pernah mengubah nilai yang diterima. Butang Commit terlebih dahulu memanggil closure penerimaan, kemudian menutup helaian. Jangan pindahkan penerimaan ke dalam onDismiss: panggilan balik itu menerangkan penutupan, yang turut berlaku apabila seseorang membatalkan.

Contoh ini menetapkan Escape kepada Cancel menggunakan .cancelAction, pintasan pembatalan standard yang didokumenkan Apple. Ia menetapkan Command-Return kepada Commit menggunakan padanan kekunci dan pengubah yang dinyatakan secara jelas. Ini ialah pintasan kawalan dalam antara muka aplikasi. Lihat cancelAction dan keyboardShortcut(_:modifiers:).

Return biasa kekal tersedia untuk pengeditan berbilang baris kerana contoh ini tidak menetapkan .defaultAction kepada butang commit. Apple mentakrifkan .defaultAction sebagai Return tanpa pengubah. Command-Return ialah pilihan editorial untuk antara muka berbilang baris ini, bukannya dakwaan bahawa setiap editor Mac patut menggunakannya. Lihat defaultAction.

Pastikan pintasan unik dalam antara muka yang berkaitan. Apple mendokumenkan bahawa apabila beberapa kawalan mempunyai pintasan yang sama, kawalan sepadan pertama yang ditemui semasa penyelesaian akan digunakan. Uji pintasan semasa titik sisipan berada di dalam editor, selain mengklik butang yang kelihatan. Lihat keyboardShortcut(_:modifiers:).

Sahkan sempadan penerimaan dengan urutan ujian ringkas

Gunakan semakan berikut dalam aplikasi Mac yang sedang berjalan. Semakan ini menguji tingkah laku yang membezakan editor draf daripada editor langsung, termasuk kes rentetan kosong yang mudah terlepas pandang.

  1. Buka Edit Note, gantikan baris kedua dengan Arrange the green tiles in a row., kemudian klik Cancel. Nota yang diterima sepatutnya mengekalkan baris kedua asalnya.
  2. Buka semula editor. Teks asal yang diterima sepatutnya tertera, tanpa semakan yang dibatalkan.
  3. Buat semakan yang sama dan klik Commit. Nota yang diterima kini sepatutnya memaparkan baris yang disemak.
  4. Buka semula, tambah pemisah baris, kemudian gunakan Command-Return. Draf berbilang baris yang lengkap sepatutnya menjadi teks yang diterima.
  5. Buka semula, buat perubahan, kemudian uji Escape semasa mengedit. Pastikan pembatalan membiarkan teks yang diterima tidak berubah.
  6. Buka semula, buang semua teks, kemudian klik Commit. Induk sepatutnya memaparkan (Empty note). Apabila dibuka semula, editor sepatutnya kosong.

Teks kosong sah dalam contoh ini. Pelaksanaan ini tidak memangkas ruang kosong, membuang pemisah baris atau melumpuhkan penerimaan untuk rentetan kosong. Jika jenis nota anda memerlukan kandungan, tambah peraturan pengesahan yang jelas dan gunakannya dalam tindakan commit serta pada keadaan butang yang didayakan.

Perbandingan hasChanges hanya mengawal label status. Membiarkan Commit didayakan apabila tiada perubahan menjadikan penerimaan satu tugasan nilai yang sama dan tidak memudaratkan. Jika kemudian anda melumpuhkannya apabila tiada perubahan, pastikan Cancel kekal tersedia supaya helaian sentiasa mempunyai jalan keluar yang jelas.

Ketahui bila SwiftUI sudah memadai dan bila AppKit membantu

Untuk tugas teks biasa ini, TextEditor sudah menyediakan permukaan pengeditan yang diperlukan. Susun atur teks tersuai mahupun jambatan AppKit tidak diperlukan untuk memisahkan perubahan draf daripada teks yang diterima. Pemisahan itu ditentukan oleh reka bentuk keadaan aplikasi.

SwiftUI semasa turut menyokong pemula TextEditor rentetan beratribut yang tersedia mulai macOS 26. Oleh itu, teks kaya tidak semestinya memerlukan penggantian SwiftUI. Namun, jenis draf perlu diubah dan keperluan penggunaan yang lebih baharu perlu dipatuhi. Apple turut mendokumenkan had penukaran atribut pemformatan untuk editor itu. Lihat init(text:selection:).

Jika anda memerlukan tingkah laku pengeditan khusus AppKit atau kawalan terus terhadap susun atur teks, laluan integrasi yang disokong ialah NSTextView yang dihoskan melalui NSViewRepresentable. Apple menerangkan kaedah representable untuk mencipta, mengemas kini dan merungkai paparan AppKit, dengan koordinator untuk menyampaikan perubahan terwakil kembali kepada SwiftUI. Lihat NSViewRepresentable.

Kekalkan kontrak penerimaan yang sama dalam jambatan itu: perubahan pengeditan AppKit mengemas kini draf, dan hanya tindakan commit yang jelas mengemas kini model yang diterima. Sebagai langkah berjaga-jaga dalam pelaksanaan, elakkan daripada menggantikan keseluruhan kandungan paparan teks pada setiap kemas kini SwiftUI; bezakan perubahan masuk sebenar daripada perubahan keluar yang dibuat oleh editor sendiri.

TextKit mempunyai satu lagi sempadan: NSTextView mendedahkan susun atur moden melalui textLayoutManager dan susun atur legasi melalui layoutManager. Apple mendokumenkan bahawa mengakses yang kedua pada macOS 12 atau lebih baharu menukar paparan teks kepada mod keserasian menggunakan NSLayoutManager. Pilih enjin itu dengan sengaja apabila menambah ciri susun atur. Lihat TextKit dan NSTextView.

Akhir sekali, operasi commit dalam contoh ini segerak dan berasaskan memori. Jika anda mengembangkannya untuk menulis dokumen, tentukan sama ada penerimaan bermaksud mengemas kini model atau melengkapkan penulisan itu. Untuk reka bentuk tulis sebelum tutup, biarkan helaian terbuka jika penulisan gagal dan kekalkan draf supaya percubaan boleh dibuat semula. Jika nota yang sama boleh berubah melalui antara muka lain milik aplikasi anda semasa pengeditan, tambahkan semakan semakan sebelum menggantikannya.

Reka bentuk asasnya kekal ringkas: cipta satu draf bagi setiap sesi pengeditan, benarkan editor mengubah draf itu, dan sediakan satu laluan penerimaan yang jelas. Dengan sempadan itu, pembatalan kekal mudah dijangka walaupun permukaan pengeditan berkembang.

Sumber