Untuk menampilkan bilah kemajuan gradien SwiftUI tanpa memampatkan warnanya, pertahankan lebar gradien sebesar lebar seluruh trek dan animasikan mask yang rata kiri. Contoh macOS ini menerapkan perilaku tersebut dalam ProgressViewStyle kustom.

Tugasnya adalah membuat indikator kemajuan desktop yang dapat diubah ukurannya untuk aktivitas sintetis. Pada penyelesaian 25%, indikator harus menampilkan seperempat pertama dari urutan warna tetap. Warna berikutnya harus muncul seiring kemajuan, sementara warna yang sudah terlihat tetap pada posisinya.

Mengapa warna memampat saat bilah kemajuan memendek?

Apple mendokumentasikan bahwa LinearGradient memetakan titik awal dan akhirnya ke persegi panjang pembatas dari bentuk yang diisinya. Akibatnya, memberikan lebar gradien yang sebanding dengan kemajuan juga mengubah ruang tempat warna-warnanya didistribusikan. Gradien selebar seperempat tetap memuat seluruh urutan warnanya. LinearGradient — Dokumentasi Developer Apple

Untuk efek penyingkapan, pisahkan dua dimensi: lebar gradien adalah lebar seluruh trek, sedangkan lebar yang terlihat adalah lebar trek dikalikan fraksi yang telah diselesaikan. Ini adalah rancangan yang diturunkan dari perilaku gradien yang didokumentasikan, bukan API bilah kemajuan tersendiri.

Bayangkan trek sepanjang 320 poin dengan titik warna biru, ungu, dan oranye pada posisi ternormalisasi 0, 0,5, dan 1. Titik tengah ungu tetap berada di 160 poin selama penyingkapan:

Kemajuan Lebar gradien penuh Lebar mask Hasil yang terlihat
0% 320 poin 0 poin Hanya trek
25% 320 poin 80 poin Seperempat pertama gradien
50% 320 poin 160 poin Gradien hingga titik tengah ungunya
100% 320 poin 320 poin Seluruh urutan warna

Ini memberi Anda cara diagnosis yang berguna: pada 25%, titik warna oranye di ujung harus tetap tersembunyi. Jika sudah muncul di tepi yang bergerak, periksa apakah lebar yang bergantung pada kemajuan diterapkan pada gradien.

Buat demo gradien kemajuan macOS yang lengkap

Gunakan target app SwiftUI macOS dengan target deployment macOS 12 atau lebih baru. Overload mask(alignment:_:) yang memperhitungkan alignment dan digunakan di bawah ini tersedia mulai macOS 12. Apple menjelaskannya sebagai penerapan kanal alfa dari tampilan lain pada tampilan yang dimask. mask(alignment:_:) — Dokumentasi Developer Apple

Ganti file titik masuk app yang dihasilkan dengan program lengkap ini. File tampilan konten yang dihasilkan boleh tetap tidak digunakan. Pastikan hanya ada satu deklarasi @main dalam target, sesuai kontrak titik masuk app dari Apple. App — Dokumentasi Developer Apple

import SwiftUI

@main
struct GradientRevealDemoApp: App {
    var body: some Scene {
        WindowGroup {
            GradientRevealDemo()
        }
    }
}

struct GradientRevealDemo: View {
    @State private var progress = 0.25
    @State private var isIndeterminate = false

    var body: some View {
        VStack(alignment: .leading, spacing: 20) {
            ProgressView(
                value: isIndeterminate ? nil : progress,
                total: 1.0
            ) {
                Text("Sample activity")
            } currentValueLabel: {
                if isIndeterminate {
                    Text("In progress")
                } else {
                    Text("\(Int((progress * 100).rounded()))%")
                }
            }
            .progressViewStyle(GradientRevealStyle())

            Slider(value: $progress, in: 0...1, step: 0.01) {
                Text("Sample completion")
            }
            .disabled(isIndeterminate)

            HStack {
                Button("Reset") { progress = 0 }
                Button("25%") { progress = 0.25 }
                Button("50%") { progress = 0.5 }
                Button("Complete") { progress = 1 }
            }
            .disabled(isIndeterminate)

            Toggle("Indeterminate", isOn: $isIndeterminate)
        }
        .padding(24)
        .frame(minWidth: 360, idealWidth: 520, minHeight: 240)
    }
}

@MainActor
struct GradientRevealStyle: ProgressViewStyle {
    func makeBody(configuration: Configuration) -> some View {
        let fraction = configuration.fractionCompleted.flatMap {
            $0.isFinite ? min(max($0, 0), 1) : nil
        }

        VStack(alignment: .leading, spacing: 8) {
            configuration.label

            if let fraction = fraction {
                GeometryReader { geometry in
                    ZStack(alignment: .leading) {
                        Rectangle()
                            .fill(Color.gray.opacity(0.2))

                        LinearGradient(
                            stops: [
                                .init(color: .blue, location: 0),
                                .init(color: .purple, location: 0.5),
                                .init(color: .orange, location: 1)
                            ],
                            startPoint: .leading,
                            endPoint: .trailing
                        )
                        .frame(width: geometry.size.width, height: 14)
                        .mask(alignment: .leading) {
                            Rectangle()
                                .frame(
                                    width: geometry.size.width
                                        * CGFloat(fraction),
                                    height: 14
                                )
                                .animation(
                                    .easeInOut(duration: 0.25),
                                    value: fraction
                                )
                        }
                    }
                    .frame(width: geometry.size.width, height: 14)
                    .clipShape(Capsule())
                }
                .frame(height: 14)
            } else {
                ProgressView()
                    .progressViewStyle(.circular)
            }

            configuration.currentValueLabel
        }
    }
}

Style kustom ini mengimplementasikan makeBody(configuration:), titik ekstensi yang didokumentasikan untuk tampilan progress view. Style ini merender kedua label yang diberikan dan membaca fraksi penyelesaian dari konfigurasi. Menempatkan pengaturan tampilan dalam protokol ini mempertahankan antarmuka ProgressView milik pemanggil. ProgressViewStyle — Dokumentasi Developer Apple

Contoh dimulai pada 25%. Gunakan tombol untuk titik pemeriksaan yang dapat diulang, seret slider untuk memeriksa nilai di antaranya, dan ubah ukuran jendela untuk memeriksa tata letak. Kontrol ini mengubah state lokal sintetis; nilainya tidak mewakili pekerjaan yang diukur. Kode sumber telah diperiksa dan perhitungan lebar diverifikasi dengan Python, tetapi program SwiftUI tidak dikompilasi atau dijalankan di lingkungan riset.

Bagaimana tata letak menjaga gradien tetap stabil

GeometryReader mengukur ruang yang dialokasikan untuk indikator. Apple menjelaskannya sebagai kontainer yang kontennya bergantung pada ukuran dan ruang koordinatnya sendiri, dengan ukuran pilihan yang fleksibel. Menerapkan .frame(height: 14) di luarnya memberi kontainer fleksibel ini tinggi eksplisit di dalam tumpukan vertikal. GeometryReader — Dokumentasi Developer Apple

Di dalam kontainer itu, gradien menerima lebar hasil pengukuran secara penuh. Hanya persegi panjang di dalam mask yang menerima lebar hasil perkalian. Saat kemajuan berubah, bingkai gradien tetap konstan untuk ukuran jendela tertentu.

Alignment .leading pada mask sangat penting: alignment ini menempatkan mask yang lebih kecil pada tepi awal gradien. Parameter alignment milik sebuah frame menempatkan konten di dalam frame tersebut; parameter itu tidak menggantikan alignment antara mask dan tampilan yang dimask. Apple mendokumentasikan keduanya sebagai tanggung jawab alignment yang terpisah. mask(alignment:_:) — Dokumentasi Developer Apple, frame(width:height:alignment:) — Dokumentasi Developer Apple

Persegi panjang opak menampilkan gradien di bagian mana pun persegi panjang itu berada. Corak warnanya tidak menentukan warna yang ditampilkan karena masking menggunakan alfa. Menggantinya dengan persegi panjang transparan akan menyembunyikan gradien. Letakkan trek abu-abu di luar mask agar tetap terlihat saat kemajuan nol.

Capsule terakhir memangkas gabungan trek dan gradien yang ditampilkan agar mengikuti siluet luar yang sama. Apple mendokumentasikan clipping sebagai mempertahankan bagian yang tertutup oleh bentuk pemangkas. Contoh ini sengaja mempertahankan tepi yang bergerak lurus hingga mencapai ujung capsule yang membulat; mask penyingkapan berbentuk capsule akan menghasilkan tepi bergerak yang membulat. clipShape(_:style:) — Dokumentasi Developer Apple

Mengubah ukuran jendela akan mengubah lebar trek penuh, sehingga gradien dihitung ulang berdasarkan lebar baru tersebut. “Warna stabil” di sini berarti posisi warna relatif terhadap trek tetap selama pembaruan kemajuan, bukan koordinat layar tetap saat ukuran jendela berubah.

Pertahankan nilai kemajuan, tipe, dan perilaku siklus hidup

Style menerima fractionCompleted sebagai Double opsional. Apple menetapkan rentang numeriknya dari 0 hingga 1 dan menggunakan nil untuk kemajuan yang tidak diketahui atau kemajuan relatif terhadap interval tanggal. Mengonversi fraksi tersebut ke CGFloat saat menghitung lebar membuat batas numeriknya eksplisit. fractionCompleted — Dokumentasi Developer Apple

Contoh ini membatasi fraksi finite sebagai langkah defensif dan mengarahkan fraksi yang tidak tersedia atau nonfinite ke indikator tak tentu standar. Penanganan nilai nonfinite tersebut merupakan kebijakan eksplisit dalam contoh ini. Dengan begitu, fraksi yang tidak tersedia tidak dianggap sebagai penyelesaian nol. Indikator bagian dalam memilih .circular secara eksplisit agar menggunakan style bawaan.

Di lokasi pemanggilan, berikan jumlah yang telah selesai dan total yang finite. Inisialisator berbasis nilai menerima nil untuk kemajuan tak tentu dan mendokumentasikan bahwa nilai di luar rentang dibatasi ke batasnya. Contoh ini menggunakan total 1 dan membatasi slider pada interval yang sama. init(value:total:label:currentValueLabel:) — Dokumentasi Developer Apple

Tampilan induk memiliki state kemajuan, sedangkan style menentukan hasil render dari konfigurasi. SwiftUI memperbarui tampilan yang bergantung ketika state berubah. Karena itu, contoh ini tidak memerlukan timer, callback tampilan, atau tugas latar belakang untuk mempertahankan state visualnya. State — Dokumentasi Developer Apple

Animasi diterapkan pada persegi panjang penyingkapan. Modifier yang dicakup pada nilai menerapkan animasi ketika fraksi yang dipantau berubah; durasi seperempat detik yang dipilih adalah pilihan presentasi ilustratif. Modifier ini tidak menghitung kemajuan tugas. Hapus modifier tersebut jika pembaruan langsung lebih sesuai untuk antarmuka Anda. animation(_:value:) — Dokumentasi Developer Apple

Ada batasan terdokumentasi untuk kemajuan berbasis timer: menurut Apple, progress view relatif terhadap tanggal tidak mendukung style kustom. Untuk tampilan gradien ini, gunakan inisialisator kemajuan numerik dan biarkan pemilik aktivitas memberikan fraksi yang telah diselesaikan. Jika Anda memerlukan pembaruan otomatis relatif terhadap tanggal dari Apple, gunakan inisialisator tersebut dengan presentasi yang didukungnya. init(timerInterval:countsDown:label:currentValueLabel:) — Dokumentasi Developer Apple

Periksa penyingkapan dengan urutan diagnosis singkat

Gunakan pemeriksaan ini setelah membangun target macOS. Pemeriksaan ini membedakan masalah pemetaan warna dari masalah alignment dan ukuran:

  1. Pilih Reset. Trek abu-abu harus tetap terlihat, tanpa gradien.
  2. Pilih 25%. Hanya warna-warna awal gradien yang seharusnya muncul. Ujung oranye yang terlihat menandakan gradien itu sendiri dipendekkan.
  3. Pilih 50%. Dengan titik warna yang diberikan, batas penyingkapan seharusnya mencapai titik tengah ungu.
  4. Pilih Complete. Gradien seharusnya memenuhi trek hingga warna oranye.
  5. Ubah ukuran jendela saat kemajuan 50%. Bagian yang terlihat seharusnya tetap setengah dari trek yang baru diukur.
  6. Aktifkan Indeterminate. Isian numerik seharusnya digantikan oleh indikator bawaan, sementara label tugas dan nilai saat ini tetap ada.

Jika penyingkapan berkembang keluar dari tengah, periksa .mask(alignment: .leading). Jika indikator mengambil ruang vertikal berlebihan, periksa frame di luar GeometryReader. Jika perubahan kemajuan menggeser semua titik warna, pastikan hanya mask yang menggunakan pengali kemajuan.

Aturan yang dapat digunakan kembali sederhana: ukur gradien untuk seluruh trek, lalu gunakan mask berukuran terpisah untuk menampilkan kemajuan. Memisahkan kedua tanggung jawab itu memudahkan penalaran tentang penyelesaian parsial, perubahan ukuran, dan animasi, sekaligus mempertahankan konfigurasi progress view yang terdokumentasi.

Sumber