SwiftUI di rak: Animasi. Bagian 1

gambar

Baru-baru ini saya menemukan artikel baru di mana orang-orang mencoba mereproduksi konsep yang menarik menggunakan SwiftUI. Inilah yang mereka lakukan:

gambar

Saya mempelajari kode mereka dengan penuh minat, tetapi mengalami frustrasi. Tidak, tidak dalam arti bahwa mereka melakukan sesuatu yang salah, tidak sama sekali. Saya hanya tidak belajar dari kode mereka praktis tidak ada yang baru. Implementasi mereka lebih tentang Combine daripada tentang animasi. Dan saya memutuskan untuk membangun lunopark untuk menulis artikel saya tentang animasi di SwiftUI, menerapkan konsep yang sama, tetapi menggunakan 100% kemampuan animasi built-in, walaupun itu tidak terlalu efektif. Belajar - sampai akhir. Untuk bereksperimen - begitu dengan binar :)

Inilah yang saya dapat:


Namun, untuk pengungkapan penuh topik, saya harus berbicara secara rinci tentang dasar-dasar. Teks itu ternyata sangat banyak, dan karena itu, saya memecahnya menjadi dua artikel. Ini adalah bagian pertama dari itu - lebih tepatnya, tutorial tentang animasi secara umum, tidak berhubungan langsung dengan animasi pelangi, yang akan saya bahas secara rinci di artikel selanjutnya.

Pada artikel ini, saya akan berbicara tentang dasar-dasar, yang tanpanya Anda dapat dengan mudah menjadi bingung dalam contoh yang lebih kompleks. Banyak hal yang akan saya bicarakan, dalam satu atau lain bentuk, telah dijelaskan dalam artikel berbahasa Inggris seperti seri ini ( 1 , 2 , 3 , 4) Saya, di sisi lain, tidak terlalu fokus pada menyebutkan cara-cara bekerja seperti pada menggambarkan bagaimana tepatnya ini bekerja. Dan seperti biasa, saya banyak bereksperimen, jadi saya segera berbagi hasil yang paling menarik.

peringatan: di bawah kucing ada banyak gambar dan animasi gif.

TLDR


Proyek ini tersedia di github . Anda dapat melihat hasil saat ini dengan animasi pelangi di TransitionRainbowView (), tapi saya tidak akan terburu-buru di tempat Anda, tetapi saya menunggu artikel berikutnya. Selain itu, saat menyiapkannya, saya menyisir kode sedikit.

Pada artikel ini, kami hanya akan membahas dasar-dasarnya, dan hanya memengaruhi konten folder Basa.

pengantar


Saya akui, saya tidak akan menulis artikel ini sekarang. Saya punya rencana yang menurutnya artikel tentang animasi seharusnya yang ketiga atau bahkan keempat berturut-turut. Namun, saya tidak bisa menolak, saya benar-benar ingin memberikan sudut pandang alternatif.

Saya ingin segera memesan. Saya tidak percaya bahwa ada kesalahan yang dibuat dalam artikel yang disebutkan, atau pendekatan yang digunakan di dalamnya tidak benar. Tidak semuanya. Itu membangun model objek dari proses (animasi), yang, menanggapi sinyal yang diterima, mulai melakukan sesuatu. Namun, bagi saya, artikel ini kemungkinan besar mengungkapkan kerja dengan kerangka kerja Combine. Ya, kerangka kerja ini adalah bagian penting dari SwiftUI, tetapi ini lebih tentang gaya yang bereaksi seperti pada umumnya daripada tentang animasi.

Pilihan saya tentu tidak lebih elegan, lebih cepat dan lebih mudah dirawat. Namun, ini mengungkapkan jauh lebih baik apa yang ada di bawah naungan SwiftUI, dan memang inilah tujuan artikel - untuk mencari tahu dulu.

Seperti yang saya katakan di artikel sebelumnyaoleh SwiftUI, saya mulai terjun ke dunia pengembangan ponsel dengan SwiftUI, mengabaikan UIKit. Ini, tentu saja, memiliki harga, tetapi ada keuntungannya. Saya tidak mencoba untuk tinggal di biara baru sesuai dengan piagam lama. Jujur, saya belum tahu ada charter, jadi saya tidak punya penolakan terhadap yang baru. Itulah mengapa, artikel ini, bagi saya, dapat bernilai tidak hanya bagi pemula, seperti saya, tetapi juga bagi mereka yang mempelajari SwiftUI sudah memiliki latar belakang dalam bentuk pengembangan di UIKit. Menurut saya, banyak orang tidak memiliki tampilan yang segar. Jangan melakukan hal yang sama, mencoba memasukkan alat baru ke dalam gambar lama, tetapi ubah visi Anda sesuai dengan kemungkinan baru.

Kami 1c-toreukan melalui ini dengan "bentuk terkendali". Ini adalah semacam SwiftUI di dunia 1s, yang terjadi lebih dari 10 tahun yang lalu. Faktanya, analoginya cukup akurat, karena formulir yang dikelola hanyalah cara baru untuk menggambar antarmuka. Namun, ia benar-benar mengubah interaksi klien-server aplikasi secara keseluruhan, dan gambaran dunia di benak para pengembang pada khususnya. Ini tidak mudah, saya sendiri tidak mau mempelajarinya selama sekitar 5 tahun, karena Saya berpikir bahwa banyak peluang yang terputus hanya diperlukan untuk saya. Tetapi, seperti yang telah ditunjukkan oleh praktik, pengkodean pada formulir yang dikelola tidak hanya mungkin, tetapi hanya perlu.

Namun, jangan membicarakannya lagi. Saya mendapatkan panduan yang terperinci dan independen yang tidak memiliki referensi, atau tautan lain dengan artikel yang disebutkan atau masa lalu yang ke-1. Selangkah demi selangkah, kami akan menyelami detail, fitur, prinsip, dan batasan. Pergilah.

Bentuk Animasi


Bagaimana animasi bekerja secara umum


Jadi, ide utama animasi adalah transformasi perubahan diskrit tertentu menjadi proses yang berkelanjutan. Misalnya, jari-jari lingkaran adalah 100 unit, menjadi 50 unit. Tanpa animasi, perubahan akan terjadi secara instan, dengan animasi - lancar. Bagaimana itu bekerja? Sangat sederhana. Untuk perubahan yang mulus, kita perlu menginterpolasi beberapa nilai dalam segmen "Itu ... Itu telah menjadi". Dalam hal jari-jari, kita harus menggambar beberapa lingkaran menengah dengan jari-jari 98 unit, 95 unit, 90 unit ... 53 unit dan, akhirnya, 50 unit. SwiftUI dapat melakukan ini dengan mudah dan alami, cukup bungkus kode yang melakukan perubahan ini denganAnimation {...}. Tampaknya ajaib ... Sampai Anda ingin menerapkan sesuatu yang sedikit lebih rumit daripada "halo dunia".

Mari kita beralih ke contoh. Objek paling sederhana dan paling mudah dipahami untuk animasi dianggap sebagai animasi bentuk. Bentuk (Saya masih akan memanggil struktur yang sesuai dengan protokol bentuk bentuk) di SwiftUI adalah struktur dengan parameter yang dapat masuk ke dalam batas-batas ini. Itu itu adalah struktur yang memiliki fungsi tubuh (rect: CGRect) -> Path. Semua runtime yang diperlukan untuk menggambar formulir ini adalah untuk meminta garis besarnya (hasil fungsi adalah objek tipe Path, pada kenyataannya, itu adalah kurva Bezier) untuk ukuran yang diperlukan (ditentukan sebagai parameter fungsi, persegi panjang dari tipe CGRect).

Bentuk adalah struktur yang disimpan. Dengan menginisialisasi itu, Anda menyimpan semua parameter yang Anda butuhkan untuk menggambar garis besarnya. Ukuran pemilihan untuk formulir ini dapat berubah, maka yang diperlukan hanyalah mendapatkan nilai Path baru untuk frame CGRect baru, dan voila.

Mari kita mulai mengkode:

struct CircleView: View{
    var radius: CGFloat
    var body: some View{
        Circle()
            .fill(Color.green)
            .frame(height: self.radius * 2)
            .overlay(
                Text("Habra")
                    .font(.largeTitle)
                    .foregroundColor(.gray)
                )

    }
}
struct CustomCircleView: View{
    var radius: CGFloat
    var body: some View{
        CustomCircle()
            .fill(Color.gray)
            .frame(width: self.radius * 2, height: self.radius * 2)
            .overlay(
                Text("Habr")
                    .font(.largeTitle)
                    .foregroundColor(.green)
                )
    }
}
struct CustomCircleTestView: View {
    @State var radius: CGFloat = 50
    var body: some View {
        VStack{
            CircleView(radius: radius)
               .frame(height: 200)
            Slider(value: self.$radius, in: 42...100)
            Button(action: {
                withAnimation(.linear(duration: 1)){
                    self.radius = 50
                }
            }){
                Text("set default radius")
            }
        }
    }
}


Jadi, kita memiliki lingkaran (Lingkaran ()), jari-jari yang dapat kita ubah menggunakan penggeser. Ini terjadi dengan lancar, seperti slider memberi kita semua nilai perantara. Namun, ketika Anda mengklik tombol "set jari-jari default", perubahan itu juga tidak terjadi secara instan, tetapi sesuai dengan instruksi withAnimation (.linear (durasi: 1)). Linier, tanpa akselerasi, membentang selama 1 detik. Kelas! Kami menguasai animasinya! Kami tidak setuju :)

Tapi bagaimana jika kami ingin menerapkan bentuk kami sendiri dan menghidupkan perubahannya? Apakah sulit melakukan ini? Mari kita periksa.

Saya membuat salinan Lingkaran sebagai berikut:

struct CustomCircle: Shape{
    public func path(in rect: CGRect) -> Path{
        let radius = min(rect.width, rect.height) / 2
        let center = CGPoint(x: rect.width / 2, y: rect.height / 2)
        return Path(){path in
            if rect.width > rect.height{
                path.move(to: CGPoint(x: center.x, y: 0))
                let startAngle = Angle(degrees: 270)
                path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle:  startAngle + Angle(degrees: 360), clockwise: false)
            }else{
                path.move(to: CGPoint(x: 0, y: center.y))
                let startAngle = Angle(degrees: 0)
                path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle:  startAngle + Angle(degrees: 360), clockwise: false)
            }
            path.closeSubpath()
        }
    }
}

Jari-jari lingkaran dihitung setengah lebih kecil dari lebar dan tinggi perbatasan area layar yang dialokasikan untuk kita. Jika lebarnya lebih besar dari ketinggian, kita mulai dari tengah batas atas (Catatan 1), jelaskan lingkaran penuh dalam arah searah jarum jam (Catatan 2), dan tutup garis besar kami di sini. Jika tingginya lebih besar dari lebar, kita mulai dari tengah batas kanan, kami juga menggambarkan lingkaran penuh searah jarum jam dan menutup kontur.

Catatan 1
Apple ( ) . , (0, 0), (x, y), x β€” , y β€” . .. y. y β€” . , . 90 , 180 β€” , 270 β€” .

Catatan 2
1 , β€œ ” β€œ ” . Core Graphics (SwiftUI ):
In a flipped coordinate system (the default for UIView drawing methods in iOS), specifying a clockwise arc results in a counterclockwise arc after the transformation is applied.

Mari kita periksa bagaimana lingkaran baru kita akan merespons perubahan di blok withAnimation:

struct CustomCircleView: View{
    var radius: CGFloat
    var body: some View{
        CustomCircle()
            .fill(Color.gray)
            .frame(width: self.radius * 2, height: self.radius * 2)
            .overlay(
                Text("Habr")
                    .font(.largeTitle)
                    .foregroundColor(.green)
                )
    }
}

struct CustomCircleTestView: View {
    @State var radius: CGFloat = 50
    var body: some View {
        VStack{
                HStack{
                CircleView(radius: radius)
                    .frame(height: 200)
                CustomCircleView(radius: radius)
                    .frame(height: 200)
            }
            Slider(value: self.$radius, in: 42...100)
            Button(action: {
                withAnimation(.linear(duration: 1)){
                    self.radius = 50
                }
            }){
                Text("set default radius")
            }
        }
    }
}


Wow! Kami belajar cara membuat gambar bentuk bebas kami sendiri dan menghidupkannya! Begitu?

Tidak juga. Semua pekerjaan di sini dilakukan oleh .frame modifier (lebar: self.radius * 2, tinggi: self.radius * 2). Di dalam blok withAnimation {...} kita ubahNegarasebuah variabel, ia mengirimkan sinyal untuk menginisialisasi ulang CustomCircleView () dengan nilai radius baru, nilai baru ini jatuh ke pengubah .frame (), dan pengubah ini sudah dapat menganimasikan perubahan parameter. Bentuk CustomCircle () kami bereaksi terhadap ini dengan animasi, karena itu tidak bergantung pada apa pun selain ukuran area yang dipilih untuk itu. Mengubah area terjadi dengan animasi, (mis. Secara bertahap, menginterpolasi nilai-nilai antara antara itu-telah menjadi), oleh karena itu lingkaran kami digambar dengan animasi yang sama.

Mari kita sederhanakan (atau masih menyulitkan?) Bentuk kita sedikit. Kami tidak akan menghitung jari-jari berdasarkan ukuran area yang tersedia, tetapi kami akan mentransfer jari-jari dalam bentuk yang sudah jadi, yaitu. menjadikannya parameter struktur tersimpan.

struct CustomCircle: Shape{
    var radius: CGFloat
    public func path(in rect: CGRect) -> Path{
        //let radius = min(rect.width, rect.height) / 2
...
    }
}

struct CustomCircleView: View{
    var radius: CGFloat
    var body: some View{
        CustomCircle(radius: radius)
            .fill(Color.gray)
            //.frame(height: self.radius * 2)
...
    }
}


Nah, sihir itu hilang dan tidak dapat diperbaiki lagi.

Kami mengecualikan pengubah bingkai () dari CustomCircleView () kami, menggeser tanggung jawab untuk ukuran lingkaran ke bentuk itu sendiri, dan animasi menghilang. Tapi itu tidak masalah, untuk mengajarkan formulir untuk menghidupkan perubahan dalam parameternya tidak terlalu sulit. Untuk melakukan ini, Anda perlu menerapkan persyaratan protokol Animatable:

struct CustomCircle: Shape, Animatable{
    var animatableData: CGFloat{
         get{
             radius
         }
         set{
            print("new radius is \(newValue)")
            radius = newValue
         }
     }
    var radius: CGFloat
    public func path(in rect: CGRect) -> Path{
	...
}


Voila! Keajaiban kembali lagi!

Dan sekarang kita dapat dengan yakin mengatakan bahwa bentuk kita benar-benar animasi - ia dapat mencerminkan perubahan dalam parameternya dengan animasi. Kami memberi sistem jendela di mana ia dapat menjejalkan nilai-nilai yang diinterpolasi yang diperlukan untuk animasi. Jika ada jendela seperti itu, perubahan akan dianimasikan. Jika tidak, perubahan dilakukan tanpa animasi, mis. segera. Tidak ada yang rumit, bukan?

AnimatableModifier


Cara menghidupkan perubahan di dalam Tampilan


Tapi mari kita langsung ke View. Misalkan kita ingin menggerakkan posisi elemen di dalam wadah. Dalam kasus kami, itu akan menjadi persegi panjang sederhana warna hijau dan lebar 10 unit. Kami akan menghidupkan posisinya secara horizontal.

struct SimpleView: View{
    @State var position: CGFloat = 0
    var body: some View{
        VStack{
            ZStack{
                Rectangle()
                    .fill(Color.gray)
                BorderView(position: position)
            }
            Slider(value: self.$position, in: 0...1)
            Button(action: {
                withAnimation(.linear(duration: 1)){
                    self.position = 0
                }
            }){
                Text("set to 0")
            }
        }
    }
}

struct BorderView: View,  Animatable{
    public var animatableData: CGFloat {
        get {
            print("Reding position: \(position)")
            return self.position
        }
        set {
            self.position = newValue
            print("setting position: \(position)")
        }
    }
    let borderWidth: CGFloat
    init(position: CGFloat, borderWidth: CGFloat = 10){
        self.position = position
        self.borderWidth = borderWidth
        print("BorderView init")
    }
    var position: CGFloat
    var body: some View{
        GeometryReader{geometry in
            Rectangle()
                .fill(Color.green)
                .frame(width: self.borderWidth)
                .offset(x: self.getXOffset(inSize: geometry.size), y: 0)
                // .borderIn(position: position)
        }
    }
    func getXOffset(inSize: CGSize) -> CGFloat{
        print("calculating position: \(position)")
        return -inSize.width / 2 + inSize.width * position
    }
}


Kelas! Bekerja! Sekarang kita tahu segalanya tentang animasi!

Tidak juga. Jika Anda melihat konsol, maka kita akan melihat yang berikut:
Posisi
penghitungan init BorderView : 0,4595176577568054
Posisi
penghitungan init BorderView: 0,468130886554718
Posisi
penghitungan init BorderView : 0,0

Pertama, setiap perubahan dalam nilai posisi menggunakan slider menyebabkan BorderView menginisialisasi ulang dengan nilai baru. Itulah mengapa kita melihat pergerakan garis hijau yang mulus setelah slider, slider hanya sangat sering melaporkan perubahan variabel, dan itu terlihat seperti animasi, tetapi tidak. Menggunakan bilah geser sangat mudah saat Anda men-debug animasi. Anda dapat menggunakannya untuk melacak beberapa kondisi transisi.

Kedua, kita melihat bahwa posisi penghitungan hanya menjadi sama dengan 0, dan tidak ada log perantara, seperti halnya dengan animasi lingkaran yang benar. Mengapa?

Masalahnya, seperti pada contoh sebelumnya, ada di pengubah. Kali ini, pengubah .offset () mendapatkan nilai indentasi baru, dan ia menghidupkan perubahan itu sendiri. Itu sebenarnya, itu bukan perubahan pada parameter posisi yang kami maksudkan untuk animasi, tetapi perubahan horisontal dari indentasi dalam pengubah .offset () yang diturunkan darinya. Dalam hal ini, ini adalah pengganti yang tidak berbahaya, hasilnya sama. Tetapi karena mereka telah datang, mari kita menggali lebih dalam. Mari kita buat pengubah kita sendiri, yang akan menerima posisi (dari 0 hingga 1) pada input, itu akan menerima ukuran area yang tersedia dan menghitung indentasi.

struct BorderPosition: ViewModifier{
    var position: CGFloat
    func body(content: Content) -> some View {
        GeometryReader{geometry in
            content
            .offset(x: self.getXOffset(inSize: geometry.size), y: 0)
            .animation(nil)
        }
    }
    func getXOffset(inSize: CGSize) -> CGFloat{
        let offset = -inSize.width / 2 + inSize.width * position
        print("at position  \(position) offset is \(offset)")
        return offset
    }
}

extension View{
    func borderIn(position: CGFloat) -> some View{
        self.modifier(BorderPosition(position: position))
    }
}

Di BorderView asli, masing-masing, GeometryReader tidak lagi diperlukan, serta fungsi untuk menghitung indentasi:

struct BorderView: View,  Animatable{
    ...
    var body: some View{
            Rectangle()
                .fill(Color.green)
                .frame(width: self.borderWidth)
                .borderIn(position: position)
    }
}


Ya, kami masih menggunakan pengubah .offset () di dalam pengubah kami, tetapi setelah itu kami menambahkan pengubah .animation (nil), yang memblokir animasi offset kami sendiri. Saya mengerti bahwa pada tahap ini Anda dapat memutuskan bahwa cukup untuk menghapus kunci ini, tetapi kemudian kita tidak akan sampai ke dasar kebenaran. Dan kebenarannya adalah bahwa trik kami dengan data animatable untuk BorderView tidak berfungsi. Bahkan, jika Anda melihat dokumentasi untuk protokol Animatable , Anda akan melihat bahwa implementasi protokol ini hanya didukung untuk AnimatableModifier, GeometryEffect, dan Shape. Lihat tidak ada di antara mereka.

Pendekatan yang tepat adalah menghidupkan modifikasi


Pendekatan itu sendiri, ketika kami meminta Lihat untuk menganimasi beberapa perubahan, salah. Untuk tampilan, Anda tidak bisa menggunakan pendekatan yang sama seperti untuk formulir. Sebagai gantinya, animasi harus disematkan di setiap pengubah. Sebagian besar pengubah bawaan sudah mendukung animasi di luar kotak. Jika Anda ingin animasi untuk pengubah Anda sendiri, Anda dapat menggunakan protokol AnimatableModifier alih-alih ViewModifier. Dan di sana Anda dapat menerapkan hal yang sama seperti ketika animasi perubahan bentuk, seperti yang kami lakukan di atas.

struct BorderPosition: AnimatableModifier {
    var position: CGFloat
    let startDate: Date = Date()
    public var animatableData: CGFloat {
        get {
            print("reading position: \(position) at time \(Date().timeIntervalSince(startDate))")
            return position
        }
        set {
            position = newValue
            print("setting position: \(position) at time \(Date().timeIntervalSince(startDate))")
        }
    }
    func body(content: Content) -> some View {
...
    }
    ...
}


Sekarang semuanya benar. Pesan di konsol membantu untuk memahami bahwa animasi kami benar-benar berfungsi, dan .animation (nil) di dalam pengubah tidak mengganggu sama sekali. Tapi mari kita cari tahu bagaimana cara kerjanya.

Pertama, Anda perlu memahami apa itu pengubah.


Di sini kita memiliki pandangan. Seperti yang saya katakan di bagian sebelumnya, ini adalah struktur dengan parameter yang tersimpan dan instruksi perakitan. Instruksi ini, pada umumnya, tidak mengandung urutan tindakan, yang merupakan kode biasa yang kita tulis dalam gaya non-deklaratif, tetapi daftar sederhana. Ini mencantumkan View lainnya, pengubah yang diterapkan padanya, dan wadah di mana mereka disertakan. Kami belum tertarik pada wadah, tetapi mari kita bicara lebih lanjut tentang pengubah.

Pengubah lagi struktur dengan parameter yang disimpan, dan Lihat instruksi pemrosesan. Ini sebenarnya instruksi yang sama dengan View - kita dapat menggunakan pengubah lain, menggunakan wadah (misalnya, saya menggunakan GeometryReader sedikit lebih tinggi) dan bahkan View lainnya. Tetapi kami hanya memiliki konten yang masuk, dan kami perlu mengubahnya menggunakan instruksi ini. Parameter pengubah adalah bagian dari instruksi. Tetapi yang paling menarik adalah mereka disimpan.

Dalam artikel sebelumnya, saya mengatakan bahwa instruksi itu sendiri tidak disimpan, bahwa itu dilemparkan setiap kali setelah Lihat diperbarui. Semuanya begitu, tetapi ada nuansa. Sebagai hasil dari pekerjaan instruksi ini, kami tidak mendapatkan gambaran yang cukup, seperti yang saya katakan sebelumnya - itu adalah penyederhanaan. Pengubah tidak hilang setelah pengoperasian instruksi ini. Mereka tetap demikian sementara View induk ada.

Beberapa analogi primitif


Bagaimana kita menggambarkan tabel dengan gaya deklaratif? Baiklah, kita akan mendaftar 4 kaki dan meja. Mereka akan menggabungkan mereka ke dalam semacam wadah, dan dengan bantuan beberapa pengubah akan menentukan bagaimana mereka diikat satu sama lain. Misalnya, setiap kaki akan menunjukkan orientasi sehubungan dengan meja, dan posisi - kaki mana yang disematkan ke sudut mana. Ya, kita bisa membuang instruksi setelah perakitan, tetapi paku akan tetap ada di meja. Begitu juga para modifikator. Saat keluar dari fungsi tubuh, kita belum cukup meja. Menggunakan body, kami membuat elemen tabel (tampilan), dan pengencang (pengubah), dan meletakkan semuanya di laci. Meja itu sendiri dirakit oleh robot. Pengencang apa yang Anda masukkan ke dalam kotak untuk setiap kaki, Anda akan mendapatkan meja seperti itu.

Fungsi .modifier (BorderPosition (position: position)), yang dengannya kami mengubah struktur BorderPosition menjadi pengubah, hanya menempatkan sekrup tambahan di laci ke kaki meja. Struktur BorderPosition adalah sekrup ini. Render, pada saat rendering, mengambil kotak ini, mengeluarkan leg darinya (Rectangle () dalam kasus kami), dan secara berurutan mendapatkan semua pengubah dari daftar, dengan nilai yang tersimpan di dalamnya. Fungsi tubuh masing-masing pengubah adalah instruksi tentang cara mengencangkan kaki ke atas meja dengan sekrup ini, dan struktur itu sendiri dengan sifat yang disimpan, ini adalah sekrup itu.

Mengapa penting untuk memahami ini dalam konteks animasi? Karena animasi memungkinkan Anda untuk mengubah parameter dari satu pengubah tanpa mempengaruhi yang lain, dan kemudian membuat ulang gambar. Jika Anda melakukan hal yang sama dengan mengubah beberapa@Stateparameter - ini akan menyebabkan reinisialisasi View bersarang, struktur pengubah, dan sebagainya, di sepanjang rantai. Tapi animasinya tidak.

Bahkan, ketika kita mengubah nilai posisi ketika kita menekan tombol, itu berubah. Sampai akhir. Tidak ada status peralihan disimpan dalam variabel itu sendiri, yang tidak dapat dikatakan tentang pengubah. Untuk setiap bingkai baru, nilai-nilai parameter pengubah berubah sesuai dengan kemajuan animasi saat ini. Jika animasi berlangsung 1 detik, maka setiap 1/60 detik (iphone menunjukkan jumlah frame per detik), nilai animatableData di dalam pengubah akan berubah, maka akan dibaca oleh render untuk rendering, setelah itu, setelah 1/60 detik lainnya akan menjadi diubah lagi, dan baca lagi oleh render.

Apa karakteristiknya, pertama-tama kita mendapatkan keadaan akhir dari seluruh Tampilan, mengingatnya, dan baru kemudian mekanisme animasi mulai membuka nilai posisi yang diinterpolasi ke dalam pengubah. Keadaan awal tidak disimpan di mana pun. Di suatu tempat di perut SwiftUI, hanya perbedaan antara kondisi awal dan akhir yang disimpan. Perbedaan ini setiap kali dikalikan dengan sebagian kecil dari waktu yang telah berlalu. Ini adalah bagaimana nilai interpolasi dihitung, yang kemudian diganti menjadi data animatable.

Perbedaan =

Baja
- Apakah Nilai Saat Ini = Baja - Perbedaan * (1 - Waktu Berlalu) Waktu Berlalu = Waktu Dari MulaiAnimasi / DurasiAnimasi Nilai

saat ini perlu dihitung berapa kali dari jumlah frame yang perlu kami tampilkan.

Mengapa "Apakah" tidak digunakan secara eksplisit? Faktanya adalah bahwa SwiftUI tidak menyimpan keadaan awal. Hanya perbedaan yang disimpan: jadi, jika terjadi beberapa jenis kegagalan, Anda dapat mematikan animasi, dan pergi ke keadaan "menjadi" saat ini.

Pendekatan ini memungkinkan Anda untuk membuat animasi dapat dibalik. Misalkan, di suatu tempat di tengah satu animasi, pengguna menekan tombol lagi, dan kami lagi mengubah nilai variabel yang sama. Dalam hal ini, yang perlu kita lakukan untuk mengalahkan perubahan ini dengan indah adalah mengambil "Current" di dalam animasi pada saat perubahan baru sebagai "It", mengingat Perbedaan baru, dan memulai animasi baru berdasarkan "Became" baru dan "Perbedaan" baru. . Ya, pada kenyataannya, transisi dari satu animasi ke animasi lain ini mungkin sedikit lebih sulit untuk mensimulasikan inersia, tetapi artinya, saya pikir, dapat dimengerti.

Yang menarik adalah bahwa animasi setiap frame meminta nilai saat ini di dalam pengubah (menggunakan pengambil). Ini, seperti yang Anda lihat dari catatan layanan di log, bertanggung jawab atas status "Baja". Kemudian, menggunakan setter, kita mengatur status baru yang berlaku untuk frame ini. Setelah itu, untuk frame berikutnya, nilai saat ini dari modifier kembali diminta - dan lagi "Telah menjadi", yaitu. Nilai akhir animasi bergerak. Sangat mungkin bahwa salinan struktur pengubah digunakan untuk animasi, dan pengambil satu struktur (pengubah nyata dari Tampilan aktual) digunakan untuk mendapatkan nilai "Baja", dan satu setter lainnya (pengubah sementara yang digunakan untuk animasi) digunakan. Saya belum menemukan cara untuk memastikan hal ini, tetapi dengan indikasi tidak langsung semuanya terlihat seperti itu. Bagaimanapun,perubahan dalam animasi tidak memengaruhi nilai tersimpan dari struktur pengubah dari Tampilan saat ini. Jika Anda memiliki ide tentang bagaimana mencari tahu persis apa yang sebenarnya terjadi dengan pengambil dan penyetel, tulis tentang itu di komentar, saya akan memperbarui artikel.

Beberapa parameter


Hingga saat ini, kami hanya memiliki satu parameter untuk animasi. Mungkin timbul pertanyaan, tetapi bagaimana jika lebih dari satu parameter diteruskan ke pengubah? Dan apakah keduanya perlu dianimasikan pada saat bersamaan? Begini caranya dengan pengubah bingkai (width: height :) misalnya. Bagaimanapun, kita dapat secara bersamaan mengubah lebar dan tinggi Tampilan ini, dan kami ingin perubahan terjadi dalam satu animasi, bagaimana cara melakukannya? Lagipula, parameter AnimatableData adalah satu, apa yang harus diganti?

Jika Anda melihat, Apple hanya memiliki satu persyaratan untuk data animatable. Tipe data yang Anda gantikan harus memenuhi protokol VectorArithmetic. Protokol ini mensyaratkan objek untuk memastikan operasi aritmatika minimum yang diperlukan untuk dapat membentuk segmen dua nilai, dan menginterpolasi titik-titik di dalam segmen ini. Operasi yang diperlukan untuk ini adalah penjumlahan, pengurangan dan penggandaan. Kesulitannya adalah kita harus melakukan operasi ini dengan satu objek yang menyimpan beberapa parameter. Itu kita harus mengemas seluruh daftar parameter kita dalam wadah yang akan menjadi vektor. Apple menyediakan objek seperti itu di luar kotak, dan menawarkan kami untuk menggunakan solusi turnkey untuk kasus yang tidak terlalu sulit. Ini disebut AnimatablePair.

Mari kita ubah tugas sedikit. Kami membutuhkan pengubah baru yang tidak hanya akan memindahkan bilah hijau, tetapi juga mengubah ketinggiannya. Ini akan menjadi dua parameter pengubah independen. Saya tidak akan memberikan kode lengkap dari semua perubahan yang perlu dilakukan, Anda bisa melihatnya di github di file SimpleBorderMove. Saya hanya akan menampilkan pengubah itu sendiri:

struct TwoParameterBorder: AnimatableModifier {
    var position: CGFloat
    var height: CGFloat
    let startDate: Date = Date()
    public var animatableData: AnimatablePair<CGFloat, CGFloat> {
        get {
           print("animation read position: \(position), height: \(height)")
           return AnimatablePair(position, height)
        }
        set {
            self.position = newValue.first
            print("animating position at \(position)")
            self.height = newValue.second
            print("animating height at \(height)")
        }
    }
    init(position: CGFloat, height: CGFloat){
        self.position = position
        self.height = height
    }
    func body(content: Content) -> some View {
        GeometryReader{geometry in
            content
                .animation(nil)
                .offset(x: -geometry.size.width / 2 + geometry.size.width * self.position, y: 0)
                .frame(height: self.height * (geometry.size.height - 20) + 20)
        }
    }
}


Saya menambahkan bilah geser lain dan tombol untuk mengubah kedua parameter sekaligus secara acak di tampilan induk dari SimpleView, tetapi tidak ada yang menarik, jadi untuk kode lengkap, selamat datang di github.

Semuanya berfungsi, kami benar-benar mendapatkan perubahan yang konsisten dalam pasangan parameter yang dikemas dalam tuple AnimatablePair. Tidak buruk.

Tidak ada yang membingungkan dalam implementasi ini? Secara pribadi, saya tegang ketika saya melihat desain ini:

        
self.position = newValue.first
self.height = newValue.second

Saya tidak menunjukkan di mana pun parameter mana yang harus menjadi yang pertama dan yang kedua. Bagaimana SwiftUI memutuskan nilai mana yang harus dimasukkan pertama dan nilai mana yang kedua? Nah, bukankah itu cocok dengan nama-nama parameter fungsi dengan nama-nama atribut struktur?

Gagasan pertama adalah urutan atribut dalam parameter fungsi dan tipenya, seperti yang terjadi pada @EnvironmentObject. Di sana, kita cukup meletakkan nilai-nilai di dalam kotak, tanpa memberi mereka label, dan kemudian kita mengeluarkannya, juga tanpa menunjukkan label apa pun. Di sana, ketik penting, dan dalam satu jenis, pesan. Dalam urutan apa mereka dimasukkan ke dalam kotak, dengan cara yang sama dan mendapatkannya. Saya mencoba urutan berbeda dari argumen fungsi, urutan argumen untuk menginisialisasi struktur, urutan atribut struktur itu sendiri, umumnya membenturkan kepala saya ke dinding, tetapi tidak dapat membingungkan SwiftUI sehingga mulai menganimasikan posisi dengan nilai ketinggian dan sebaliknya.

Kemudian saya sadar. Saya sendiri menunjukkan parameter mana yang akan menjadi yang pertama dan yang kedua dalam pengambil. SwiftUI tidak perlu tahu persis bagaimana kita menginisialisasi struktur ini. Itu bisa mendapatkan nilai animatableData sebelum perubahan, mendapatkannya setelah perubahan, menghitung perbedaan di antara mereka, dan mengembalikan perbedaan yang sama, diskalakan proporsional dengan interval waktu yang telah berlalu, ke penyetel kami. Secara umum tidak perlu tahu apa-apa tentang nilai itu sendiri di dalam AnimatableData. Dan jika Anda tidak membingungkan urutan variabel dalam dua baris yang berdekatan, maka semuanya akan berurutan, tidak peduli betapa rumitnya struktur dari sisa kode.

Tapi mari kita periksa. Bagaimanapun, kita dapat membuat vektor penampung kita sendiri (oh, aku menyukainya, buat implementasi kita sendiri dari objek yang ada, Anda mungkin telah memperhatikan ini dari artikel sebelumnya).



Kami menggambarkan struktur dasar, menyatakan dukungan untuk protokol VectorArithmetic, membuka kesalahan tentang protokol yang tidak sesuai, klik perbaikan, dan kami mendapatkan deklarasi dari semua fungsi yang diperlukan dan parameter yang dihitung. Tinggal mengisi saja.

Dengan cara yang sama, kita mengisi objek kita dengan metode yang diperlukan untuk protokol AdditiveArithmetic (VectorArithmetic menyertakan dukungannya).

struct MyAnimatableVector: VectorArithmetic{
    static func - (lhs: MyAnimatableVector, rhs: MyAnimatableVector) -> MyAnimatableVector {
        MyAnimatableVector(position: lhs.position - rhs.position, height: lhs.height - rhs.height)
    }
    
    static func + (lhs: MyAnimatableVector, rhs: MyAnimatableVector) -> MyAnimatableVector {
        MyAnimatableVector(position: lhs.position + rhs.position, height: lhs.height + rhs.height)
    }
    
    mutating func scale(by rhs: Double) {
        self.position = self.position * CGFloat(rhs)
        self.height = self.height * CGFloat(rhs)
    }
    
    var magnitudeSquared: Double{
         Double(self.position * self.position) + Double(self.height * self.height)
    }
    
    static var zero: MyAnimatableVector{
        MyAnimatableVector(position: 0, height: 0)
    }
    
    var position: CGFloat
    var height: CGFloat
}

  • Saya pikir mengapa kita membutuhkan + dan - jelas.
  • skala adalah fungsi penskalaan. Kami mengambil perbedaan "Itu - Itu telah menjadi" dan kalikan dengan tahap animasi saat ini (dari 0 ke 1). "Itu menjadi + Perbedaan * (1 - Tahap)" dan akan ada nilai saat ini yang harus kita telusuri di animatableData
  • Nol mungkin diperlukan untuk menginisialisasi objek baru yang nilainya akan digunakan untuk animasi. Animasi menggunakan .zero di awal, tapi saya tidak tahu persis bagaimana caranya. Namun, saya tidak berpikir ini penting.
  • magnitudeSquared adalah produk skalar dari vektor yang diberikan dengan dirinya sendiri. Untuk ruang dua dimensi, ini berarti panjang vektor kuadrat. Ini mungkin digunakan untuk dapat membandingkan dua objek satu sama lain, bukan secara elemen, tetapi secara keseluruhan. Tampaknya tidak digunakan untuk tujuan animasi.

Secara umum, fungsi β€œ- =” β€œ+ =” juga termasuk dalam dukungan protokol, tetapi untuk struktur mereka dapat dihasilkan secara otomatis dalam formulir ini.

    static func -= (lhs: inout MyAnimatableVector, rhs: MyAnimatableVector) {
        lhs = lhs - rhs
    }

    static func += (lhs: inout MyAnimatableVector, rhs: MyAnimatableVector) {
        lhs = lhs + rhs
    }


Untuk lebih jelasnya, saya menjabarkan semua logika ini dalam bentuk diagram. Gambar bisa diklik. Apa yang kita dapatkan selama animasi disorot dalam warna merah - setiap centang berikutnya (1/60 detik) timer memberi nilai t baru, dan kami, dalam setter pengubah kami, mendapatkan nilai baru data animatable. Begitulah cara animasi bekerja di bawah tenda. Pada saat yang sama, penting untuk memahami bahwa pengubah adalah struktur yang tersimpan, dan salinan pengubah saat ini dengan keadaan saat ini yang baru digunakan untuk menampilkan animasi.





Mengapa AnimatableData hanya bisa menjadi struktur


Ada satu hal lagi. Anda tidak bisa menggunakan kelas sebagai objek AnimatableData. Secara formal, Anda bisa mendeskripsikan untuk kelas semua metode yang diperlukan dari protokol terkait, tetapi ini tidak akan lepas landas, dan inilah sebabnya. Seperti yang Anda ketahui, kelas adalah tipe data referensi, dan struktur adalah tipe data berbasis nilai. Ketika Anda membuat satu variabel berdasarkan yang lain, dalam kasus kelas, Anda menyalin tautan ke objek ini, dan dalam kasus struktur, Anda membuat objek baru berdasarkan nilai-nilai yang sudah ada. Berikut adalah contoh kecil yang menggambarkan perbedaan ini:

    struct TestStruct{
        var value: CGFloat
        mutating func scaled(by: CGFloat){
            self.value = self.value * by
        }
    }
    class TestClass{
        var value: CGFloat
        func scaled(by: CGFloat){
             self.value = self.value * by
        }
        init(value: CGFloat){
            self.value = value
        }
    }
        var stA = TestStruct(value: 5)
        var stB = stA
        stB.scaled(by: 2)
        print("structs: a = \(stA.value), b = \(stB.value))") //structs: a = 5.0, b = 10.0)
        var clA = TestClass(value: 5)
        var clB = clA
        clB.scaled(by: 2)
        print("classes: a = \(clA.value), b = \(clB.value))") //classes: a = 10.0, b = 10.0)

Dengan animasi hal yang persis sama terjadi. Kami memiliki objek AnimatableData yang mewakili perbedaan antara "dulu" dan "menjadi". Kita perlu menghitung bagian dari perbedaan ini untuk mencerminkan di layar. Untuk melakukan ini, kita harus menyalin perbedaan ini dan mengalikannya dengan angka yang mewakili tahap animasi saat ini. Dalam kasus struktur, ini tidak akan mempengaruhi perbedaan itu sendiri, tetapi dalam kasus kelas itu akan. Frame pertama yang kita gambar adalah status "tadinya". Untuk melakukan ini, kita perlu menghitung Baja + Perbedaan * Tahap Saat Ini - Perbedaan. Dalam kasus kelas, dalam frame pertama kita mengalikan selisihnya dengan 0, memusatkannya, dan semua frame berikutnya digambar sehingga perbedaannya = 0. i.e. animasi tampaknya digambar dengan benar, tetapi sebenarnya kita melihat transisi instan dari satu negara ke yang lain, seolah-olah tidak ada animasi.

Anda mungkin dapat menulis semacam kode tingkat rendah yang membuat alamat memori baru untuk hasil perkalian - tetapi mengapa? Anda bisa menggunakan struktur - mereka dibuat untuk itu.

Bagi mereka yang ingin benar-benar memahami bagaimana SwiftUI menghitung nilai-nilai perantara, dengan operasi apa dan pada saat apa, pesan-pesan didorong ke konsol dalam proyek . Selain itu, saya memasukkan sleep 0,1 detik di sana untuk mensimulasikan perhitungan sumber daya intensif di dalam animasi, bersenang-senang :)

Animasi layar: .transition ()


Hingga saat ini, kami berbicara tentang menghidupkan perubahan dalam nilai yang diteruskan ke pengubah atau formulir. Ini adalah alat yang sangat kuat. Tetapi ada alat lain yang juga menggunakan animasi - ini adalah animasi dari tampilan dan hilangnya View.

Dalam artikel terakhir, kita berbicara tentang fakta bahwa dalam gaya deklaratif if-else, ini sama sekali tidak mengontrol aliran kode dalam runtime, melainkan pandangan SchrΓΆdinger. Ini adalah wadah yang berisi dua Tampilan pada saat yang sama, yang memutuskan mana yang akan ditampilkan sesuai dengan kondisi tertentu. Jika Anda melewatkan blok lain, maka EmptyView ditampilkan, bukan tampilan kedua.

Beralih di antara dua Tampilan juga dapat dianimasikan. Untuk melakukan ini, gunakan pengubah .transition ().

struct TransitionView: View {
    let views: [AnyView] = [AnyView(CustomCircleTestView()), AnyView(SimpleBorderMove())]
    @State var currentViewInd = 0
    var body: some View {
        VStack{
            Spacer()
            ZStack{
                ForEach(views.indices, id: \.self){(ind: Int) in
                    Group{
                        if ind == self.currentViewInd{
                            self.views[ind]
                        }
                    }
                }
            }
            HStack{
                ForEach(views.indices, id: \.self){(ind: Int) in
                    RoundedRectangle(cornerRadius: 10)
                        .fill(ind == self.currentViewInd ? Color.green : Color.gray)
                        .overlay(
                            Text("\(ind + Int(1))"))
                        .onTapGesture{
                            withAnimation{
                                self.currentViewInd = ind
                            }
                    }
                }
            }
                .frame(height: 50)
            Spacer()
        }
    }
}

Mari kita perhatikan cara kerjanya. Pertama-tama, di muka, bahkan pada tahap inisialisasi dari tampilan induk, kami membuat dan menempatkan beberapa Tampilan dalam array. Array bertipe AnyView, karena elemen array harus memiliki tipe yang sama, jika tidak mereka tidak dapat digunakan di ForEach. Jenis hasil buram dari artikel sebelumnya , ingat?

Selanjutnya, kami menentukan enumerasi indeks array ini, dan untuk masing-masing indeks kami tampilkan tampilan dengan indeks ini. Kami terpaksa melakukan ini, dan tidak langsung beralih dari View, karena untuk bekerja dengan ForEach, kami perlu menetapkan pengenal internal untuk setiap elemen sehingga SwiftUI dapat beralih pada konten koleksi. Sebagai alternatif, kita harus membuat pengidentifikasi proksi di setiap Tampilan, tetapi mengapa, jika indeks dapat digunakan?

Kami membungkus setiap tampilan dari koleksi dalam suatu kondisi, dan menunjukkannya hanya jika itu aktif. Namun, jika konstruksi lain tidak bisa ada di sini, kompiler akan mengambilnya untuk kontrol aliran, jadi kami menempatkan semua ini dalam Grup sehingga kompiler memahami persis apa itu Lihat, atau lebih tepatnya, instruksi untuk ViewBuilder untuk membuat wadah ConditionalContent opsional <View1, View2>.

Sekarang, ketika mengubah nilai currentViewInd, SwiftUI menyembunyikan tampilan aktif sebelumnya, dan menunjukkan yang sekarang. Bagaimana Anda menyukai navigasi ini di dalam aplikasi?


Semua yang masih harus dilakukan adalah menempatkan perubahan currentViewInd di bungkus withAnimation, dan beralih antar windows akan menjadi lancar.

Tambahkan .transition modifier, tentukan .scale sebagai parameter. Ini akan membuat animasi tampilan dan lenyapnya masing-masing tampilan ini berbeda - menggunakan penskalaan alih-alih transparansi yang digunakan oleh SwiftUI default.

                ForEach(views.indices, id: \.self){(ind: Int) in
                    Group{
                        if ind == self.currentViewInd{
                            self.views[ind]
                                .transition(.scale)
                        }
                    }
                }


Perhatikan bahwa tampilan muncul dan menghilang dengan animasi yang sama, hanya hilangnya menghilang dalam urutan terbalik. Bahkan, kita dapat menetapkan animasi untuk penampilan dan hilangnya tampilan secara individual. Transisi asimetris digunakan untuk ini.

                    Group{
                        if ind == self.currentViewInd{
                            self.views[ind]
                               .transition(.asymmetric(
                                    insertion: insertion: AnyTransition.scale(scale: 0.1, anchor: .leading).combined(with: .opacity),
                                    removal: .move(edge: .trailing)))
                        }
                    }


Animasi .scale yang sama digunakan untuk muncul di layar, tetapi sekarang kami telah menentukan parameter untuk penggunaannya. Itu tidak dimulai dengan ukuran nol (titik), tetapi dengan ukuran 0,1 dari yang biasa. Dan posisi awal jendela kecil bukan di tengah layar, tetapi bergeser ke tepi kiri. Selain itu, tidak satu transisi bertanggung jawab atas penampilan, tetapi dua. Mereka dapat dikombinasikan dengan .combined (with :). Dalam hal ini, kami telah menambahkan transparansi.

Hilangnya tampilan sekarang disajikan oleh animasi lain - menyapu tepi kanan layar. Saya membuat animasi sedikit lebih lambat sehingga Anda dapat melihatnya.

Dan seperti biasa, saya tidak sabar untuk menulis versi animasi transit saya sendiri. Ini bahkan lebih sederhana daripada bentuk atau pengubah animasi.

struct SpinTransitionModifier: ViewModifier {
    let angle: Double
    let anchor: UnitPoint
    func body(content: Content) -> some View {
        content
            .rotationEffect(Angle(degrees: angle), anchor: anchor)
            .clipped()
    }
}

extension AnyTransition {
    static func spinIn(anchor: UnitPoint) -> AnyTransition {
        .modifier(
            active: SpinTransitionModifier(angle: -90, anchor: anchor),
            identity: SpinTransitionModifier(angle: 0, anchor: anchor))
    }
    static func spinOut(anchor: UnitPoint) -> AnyTransition {
        .modifier(
            active: SpinTransitionModifier(angle: 90, anchor: anchor),
            identity: SpinTransitionModifier(angle: 0, anchor: anchor))
    }
}


Untuk mulai dengan, kita menulis pengubah biasa di mana kita mentransfer angka tertentu - sudut rotasi dalam derajat, serta titik relatif di mana rotasi ini terjadi. Kemudian, kami memperluas jenis AnyTransition dengan dua fungsi. Itu bisa saja satu, tapi menurut saya lebih nyaman. Saya merasa lebih mudah untuk menetapkan nama yang berbicara kepada masing-masing dari mereka daripada mengontrol derajat rotasi secara langsung dalam Tampilan itu sendiri.

Jenis AnyTransition memiliki metode pengubah statis, di mana kami melewati dua pengubah, dan kami mendapatkan objek AnyTransition yang menggambarkan transisi yang mulus dari satu negara ke yang lain. identitas adalah pengubah keadaan normal dari tampilan animasi. Aktif adalah keadaan awal animasi untuk tampilan tampilan, atau akhir animasi untuk penghilangan, mis. ujung segmen lainnya, negara bagian di mana akan diinterpolasi.

Jadi, spinIn menyiratkan bahwa saya akan menggunakannya untuk membuat tampilan muncul dari luar layar (atau ruang yang dialokasikan untuk Tampilan) dengan memutar searah jarum jam di sekitar titik yang ditentukan. spinOut berarti tampilan akan menghilang dengan cara yang sama, berputar di sekitar titik yang sama, juga searah jarum jam.

Menurut ide saya, jika Anda menggunakan titik yang sama untuk tampilan dan hilangnya Tampilan, Anda mendapatkan efek memutar seluruh layar di sekitar titik ini.

Semua animasi didasarkan pada mekanik pengubah standar. Jika Anda menulis pengubah khusus, Anda harus menerapkan persyaratan protokol AnimatableModifier, seperti yang kami lakukan pada TwoParameterBorder, atau menggunakan pengubah bawaan di dalamnya yang menyediakan animasi default sendiri. Dalam hal ini, saya mengandalkan animasi .rotationEffect () bawaan di dalam pengubah SpinTransitionModifier saya.

Pengubah .transition () hanya menjelaskan apa yang harus dipertimbangkan sebagai titik awal dan akhir dari animasi. Jika kita perlu meminta status AnimatableData sebelum memulai animasi, untuk meminta pengubah AnimatableData keadaan saat ini, menghitung perbedaan, dan kemudian menganimasikan penurunan dari 1 ke 0, lalu .transition () hanya mengubah data asli. Anda tidak terikat pada kondisi Tampilan Anda, Anda tidak didasarkan padanya. Anda secara eksplisit menentukan sendiri kondisi awal dan akhir, dari mereka Anda mendapatkan AnimatableData, menghitung perbedaan dan menganimasinya. Kemudian, di akhir animasi, tampilan Anda saat ini muncul.

Omong-omong, identitas adalah pengubah yang akan tetap diterapkan pada Tampilan Anda di akhir animasi. Kalau tidak, kesalahan di sini akan menyebabkan lompatan di akhir animasi penampilan, dan awal animasi penghilangan. Jadi transisi dapat dianggap sebagai "dua dalam satu" - menerapkan pengubah spesifik secara langsung ke tampilan + kemampuan untuk menganimasikan perubahannya saat tampilan muncul dan menghilang.

Sejujurnya, mekanisme kontrol animasi ini tampaknya sangat kuat bagi saya, dan saya sedikit menyesal kami tidak dapat menggunakannya untuk animasi apa pun. Saya tidak akan menolak untuk membuat animasi tertutup tanpa akhir. Namun, kita akan membicarakannya di artikel selanjutnya.

Untuk melihat dengan lebih baik bagaimana perubahan itu sendiri terjadi, saya mengganti Lihat pengujian kami dengan kotak dasar, ditandatangani dengan angka, dan dibingkai.

                    Group{
                        if ind == self.currentViewInd{
                            //self.views[ind]
                            Rectangle()
                                .fill(Color.gray)
                                .frame(width: 100, height: 100)
                                .border(Color.black, width: 2)
                                .overlay(Text("\(ind + 1)"))
                              .transition(.asymmetric(
                                  insertion: .spinIn(anchor: .bottomTrailing),
                                  removal: .spinOut(anchor: .bottomTrailing)))
                        }
                    }


Dan untuk membuat gerakan ini lebih baik, saya menghapus .clipped () dari pengubah SpinTransitionModifier:

struct SpinTransitionModifier: ViewModifier {
    let angle: Double
    let anchor: UnitPoint
    func body(content: Content) -> some View {
        content
            .rotationEffect(Angle(degrees: angle), anchor: anchor)
            //.clipped()
    }
}


Omong-omong, sekarang kita perlu SpinTransitionModifier di modifier kita sendiri sama sekali. Itu dibuat hanya untuk menggabungkan dua pengubah, rotasi, Efek, dan kliping () menjadi satu, sehingga animasi rotasi tidak melampaui ruang lingkup yang dipilih untuk Tampilan kami. Sekarang, kita dapat menggunakan .rotationEffect () langsung di dalam .modifier (), kita tidak memerlukan perantara dalam bentuk SpinTransitionModifier.

Saat Melihat Mati


Poin yang menarik adalah siklus hidup tampilan jika ditempatkan dalam if-else. Lihat, meskipun dimulai, dan direkam sebagai elemen array, tidak disimpan dalam memori. Semua diaNegaraparameter diatur ulang ke default saat berikutnya mereka muncul di layar. Ini hampir sama dengan inisialisasi. Terlepas dari kenyataan bahwa objek-struktur itu sendiri masih ada, render menghapusnya dari bidang pandangnya, karena itu tidak. Di satu sisi, ini mengurangi penggunaan memori. Jika Anda memiliki sejumlah besar Tampilan kompleks dalam array, render harus menggambar semuanya secara konstan, bereaksi terhadap perubahan - ini memengaruhi kinerja secara negatif. Jika saya tidak salah, itulah yang terjadi sebelum pembaruan Xcode 11.3. Sekarang, tampilan tidak aktif diturunkan dari memori render.

Di sisi lain, kita harus memindahkan semua kondisi penting di luar cakupan Pandangan ini. Untuk ini, yang terbaik adalah menggunakan variabel @EnvironmentObject.

Kembali ke siklus hidup, harus juga dicatat bahwa pengubah .onAppear {}, jika seseorang terdaftar di dalam Tampilan ini, berfungsi segera setelah mengubah kondisi dan tampilan Tampilan di layar, bahkan sebelum animasi dimulai. Karenanya, onDisappear {} dipicu setelah akhir animasi penghilangan. Ingatlah ini jika Anda berencana untuk menggunakannya dengan animasi transisi.

Apa berikutnya?


Fiuh Ternyata cukup banyak, tetapi secara rinci, dan, saya harap, secara cerdas. Jujur, saya berharap untuk berbicara tentang animasi pelangi sebagai bagian dari satu artikel, tetapi saya tidak bisa berhenti pada waktunya dengan detailnya. Jadi tunggu kelanjutannya.

Bagian berikut menunggu kita:

  • penggunaan gradien: linier, melingkar dan sudut - semuanya akan berguna
  • Warna bukan warna sama sekali: pilih dengan bijak.
  • animasi melingkar: cara memulai dan berhenti, dan cara berhenti segera (tanpa animasi, mengubah animasi - ya, ada juga)
  • animasi aliran saat ini: prioritas, ganti, animasi berbeda untuk objek yang berbeda
  • detail tentang timing animasi: kami akan mengarahkan timing di tail dan di sure, hingga implementasi timingCurve kami sendiri (oh, teruskan aku tujuh :))
  • bagaimana mengetahui saat saat animasi yang diputar
  • Jika SwiftUI tidak cukup

Saya akan membicarakan semua ini secara rinci menggunakan contoh membuat animasi pelangi, seperti pada gambar:


Saya tidak pergi dengan cara mudah, tetapi mengumpulkan semua garu yang bisa saya jangkau, mewujudkan animasi ini pada prinsip-prinsip yang dijelaskan di atas. Kisah tentang ini seharusnya menjadi sangat informatif, dan kaya akan trik dan segala jenis peretasan, tentang yang hanya ada sedikit laporan, dan yang akan bermanfaat bagi mereka yang memutuskan untuk menjadi perintis di SwiftUI. Ini akan muncul kira-kira dalam satu atau dua minggu. Ngomong-ngomong, Anda bisa berlangganan agar tidak ketinggalan. Tetapi ini, tentu saja, hanya jika materi itu tampak bermanfaat bagi Anda, dan metode penyajiannya disetujui. Kemudian, langganan Anda akan membantu membawa dengan cepat artikel baru ke atas, membawanya ke khalayak yang lebih luas lebih awal. Kalau tidak, tulis di komentar apa yang salah.

All Articles