SwiftUI di rak

Setiap kali kerangka baru muncul dalam bahasa pemrograman, cepat atau lambat, orang-orang muncul yang belajar bahasa darinya. Ini mungkin merupakan kasus dalam pengembangan iOS pada saat kemunculan Swift: pada awalnya dianggap sebagai tambahan untuk Objective-C - tetapi saya belum menemukannya. Sekarang, jika Anda mulai dari awal, pilihan bahasa tidak lagi sepadan. Swift melampaui kompetisi.

Hal yang sama, tetapi dalam skala yang lebih kecil, terjadi dengan kerangka kerja. Penampilan SwiftUI tidak terkecuali. Saya mungkin perwakilan dari generasi pertama pengembang yang memulai dengan mempelajari SwiftUI, mengabaikan UIKit. Ini memiliki harga - sejauh ini hanya ada sedikit materi pelatihan dan contoh kode kerja. Ya, jaringan sudah memiliki sejumlah artikel yang menceritakan tentang fitur tertentu, alat tertentu. Pada www.hackingwithswift.com yang sama sudah ada cukup banyak contoh kode dengan penjelasan. Namun, mereka tidak banyak membantu mereka yang memutuskan untuk belajar SwiftUI dari awal, seperti saya. Sebagian besar materi di jaringan adalah jawaban untuk pertanyaan spesifik dan dirumuskan. Pengembang yang berpengalaman dapat dengan mudah mengetahui cara kerja semuanya, mengapa demikian, dan mengapa harus diterapkan. Untuk pemula, pertama-tama Anda harus memahami pertanyaan apa yang harus ditanyakan, dan baru setelah itu ia bisa sampai ke artikel ini.



Di bawah potongan, saya akan mencoba mensistematisasikan dan memilah-milah apa yang telah saya pelajari saat ini. Format artikel ini hampir menjadi panduan, meskipun, sebuah lembar contekan yang saya buat dalam bentuk di mana saya sendiri ingin membacanya di awal perjalanan saya. Untuk pengembang berpengalaman yang belum mempelajari SwiftUI secara mendalam, ada juga beberapa contoh kode yang menarik, dan penjelasan tekstual dapat dibaca secara diagonal.

Saya harap artikel ini akan menghemat waktu ketika Anda juga ingin merasakan sedikit keajaiban.

Sebagai permulaan, sedikit tentang diri Anda


Saya praktis tidak memiliki latar belakang pengembangan seluler, dan pengalaman yang signifikan
dalam 1s tidak bisa membantu banyak di sini. Tentang bagaimana dan mengapa saya memutuskan untuk belajar SwiftUI, saya akan memberi tahu Anda lain waktu, jika itu akan menarik bagi seseorang, tentu saja.

Kebetulan permulaan saya dalam pengembangan ponsel bertepatan dengan rilis iOS 13 dan SwiftUI. Ini pertanda, pikirku, dan memutuskan untuk segera memulai dengan itu, mengabaikan UIKit. Saya merasa kebetulan yang lucu bahwa saya mulai bekerja dengan 1c pada saat-saat seperti itu: kemudian formulir yang dikelola muncul. Dalam kasus 1c, mempopulerkan teknologi baru memakan waktu hampir lima tahun. Setiap kali seorang pengembang diperintahkan untuk menerapkan beberapa fungsi baru, ia menghadapi pilihan: melakukannya dengan cepat dan andal, dengan alat-alat yang sudah dikenal, atau menghabiskan banyak waktu untuk meributkan yang baru, dan tanpa jaminan hasil. Pilihan biasanya dibuat untuk mendukung kecepatan dan kualitas saat ini, dan waktu investasi dalam alat baru tertunda untuk waktu yang sangat lama.

Sekarang, tampaknya, situasi dengan SwiftUI hampir sama. Semua orang tertarik, semua orang mengerti bahwa ini adalah masa depan, tetapi sejauh ini hanya sedikit yang meluangkan waktu untuk mempelajarinya. Kecuali untuk proyek kesayangan.

Secara umum, tidak masalah bagi saya kerangka kerja mana yang harus dipelajari, dan saya memutuskan untuk mengambil kesempatan meskipun ada pendapat umum bahwa akan mungkin untuk meluncurkannya dalam produksi dalam satu atau dua tahun. Dan karena kebetulan saya berada di antara para perintis, saya memutuskan untuk berbagi pengalaman praktis. Saya ingin mengatakan bahwa saya bukan seorang guru, dan secara umum dalam pengembangan ponsel - ketel. Namun demikian, saya sudah menempuh cara tertentu, di mana saya mencari semua Internet untuk mencari informasi, dan saya dapat dengan yakin mengatakan bahwa itu tidak cukup dan secara praktis tidak sistematis. Tetapi dalam bahasa Rusia, tentu saja, praktis tidak ada. Jika demikian, saya memutuskan untuk mengumpulkan kekuatan saya, menyingkirkan kompleks penipu itu, dan berbagi dengan komunitas apa yang saya berhasil pahami sendiri. Saya akan melanjutkan dari asumsi bahwa pembaca sudah setidaknya paling tidak terbiasa dengan SwiftUI, dan saya tidak akan menguraikan hal-hal seperti VStack{…} , Text(…) , dll.

Saya menekankan sekali lagi bahwa saya akan lebih jauh menggambarkan kesan saya sendiri tentang upaya untuk mencapai hasil yang diinginkan dari SwiftUI. Saya tidak dapat memahami sesuatu, dan dari beberapa percobaan menarik kesimpulan yang salah atau tidak akurat, sehingga segala koreksi dan klarifikasi sangat kami harapkan.

Untuk pengembang berpengalaman, artikel ini mungkin tampak penuh dengan deskripsi hal-hal yang jelas, tetapi jangan menilai dengan ketat. Tutorial untuk boneka di SwiftUI belum ditulis.

Apa ini SwiftUI?


Jadi, saya mungkin akan mulai dengan apa itu semua, ini SwiftUI Anda. Di sini lagi, masa laluku yang pertama muncul. Analogi dengan formulir terkelola hanya semakin kuat ketika saya menonton beberapa video tutorial tentang cara tata letak antarmuka di Storyboard (mis. Ketika bekerja dengan UIKit). Saya mengambil nostalgia sesuai dengan bentuk "tidak terkendali" dalam 1s: penempatan elemen secara manual pada formulir, dan terutama binding ... Oh, ketika penulis video pelatihan selama sekitar 20 menit berbicara tentang seluk-beluk pengikatan berbagai elemen satu sama lain dan tepi layar, saya ingat dengan senyuman 1C - semuanya sama sebelum bentuk-bentuk yang dikendalikan. Yah, hampir ... sedikit lebih buruk, tentu saja, baik, dan karenanya - lebih mudah. Dan SwiftUI, secara kasar, mengelola formulir dari Apple. Tidak ada ikatan. Tidak ada storyboard dan segways. Anda cukup menggambarkan struktur tampilan dalam kode Anda. Dan itu saja. Semua parameter, ukuran, dll. Diatur langsung dalam kode - tetapi cukup sederhana. Lebih tepatnya, Anda dapat mengedit parameter objek yang ada di Canvas, tetapi untuk ini, Anda harus terlebih dahulu menambahkannya dalam kode. Sejujurnya, saya tidak tahu bagaimana ini akan bekerja di tim pengembangan besar, di mana biasanya memisahkan tata letak desain dan konten View itu sendiri, tetapi sebagai pengembang indie saya sangat menyukai pendekatan ini.

Gaya deklaratif


SwiftUI mengasumsikan bahwa deskripsi struktur View Anda sepenuhnya dalam kode. Selain itu, Apple menawarkan gaya penulisan kode ini kepada kami. Yaitu, kira-kira seperti ini:
“Ini Pandangan. Itu (untuk beberapa alasan saya ingin mengatakan "tampilan", dan, karenanya, berlaku kemunduran untuk kata perempuan) terdiri dari dua bidang teks dan satu gambar. Bidang teks disusun satu demi satu secara horizontal. Gambar di bawah mereka dan ujung-ujungnya dipotong dalam bentuk lingkaran. "
Kedengarannya tidak biasa, bukan? Biasanya, dalam kode kita menggambarkan proses itu sendiri, apa yang perlu dilakukan untuk mencapai hasil yang kita miliki di kepala kita:
"Sisipkan blok, masukkan bidang teks ke dalam blok ini, diikuti oleh bidang teks lain, dan setelah itu, ambil gambar, potong tepinya dengan membulatkannya, dan tempel di bawah."
Kedengarannya seperti instruksi untuk furnitur dari Ikea. Dan di swiftUI, kita segera melihat apa hasilnya. Bahkan tanpa Canvas atau debugging, struktur kode dengan jelas mencerminkan struktur View. Jelas apa dan dalam urutan apa yang akan ditampilkan dan dengan efek apa.

Artikel yang sangat baik tentang FunctionBuilder, dan bagaimana memungkinkan Anda untuk menulis kode dengan gaya deklaratif sudah ada di Habré .

Pada prinsipnya, banyak yang telah ditulis tentang gaya deklaratif dan kelebihannya, jadi saya akan mengakhirinya. Saya akan menambahkan dari diri saya bahwa saya sudah terbiasa dengannya dan benar-benar merasakan betapa nyamannya menulis kode dengan gaya ini ketika datang ke antarmuka. Dengan ini, Apple menabrak apa yang disebut bullseye!

Terdiri dari apa tampilan?


Tapi mari kita lihat lebih dekat. Apple menyarankan gaya deklaratifnya seperti ini:

 struct ContentView: View { var text1 = "some text" var text2 = "some more text" var body: some View { VStack{ Text(text1) .padding() .frame(width: 100, height: 50) Text(text2) .background(Color.gray) .border(Color.green) } } } 

Harap dicatat, View adalah struktur dengan beberapa parameter. Untuk membuat struktur View , kita perlu mengatur body parameter yang dihitung, yang mengembalikan some View . Kami akan membicarakan ini nanti. Isi dari body: some View { … } closure body: some View { … } adalah deskripsi dari apa yang akan ditampilkan di layar. Sebenarnya, ini adalah semua yang diperlukan untuk struktur kami untuk memenuhi persyaratan protokol View. Saya terutama menyarankan fokus pada body .

Jadi, rak


Secara total, saya menghitung tiga jenis elemen dari mana tubuh View dibangun:

  • Pandangan Lain
    Yaitu Setiap Tampilan berisi satu atau lebih View lainnya. Mereka, pada gilirannya, juga dapat mengandung baik tampilan sistem seperti Text() , dan kustom, yang kompleks yang ditulis oleh pengembang. Ternyata semacam boneka bersarang dengan tingkat bersarang tanpa batas.
  • Pengubah
    Dengan bantuan pengubah, semua keajaiban terjadi. Berkat mereka, kami secara singkat dan jelas memberi tahu SwiftUI pandangan seperti apa yang ingin kami lihat. Cara kerjanya, kami masih akan mengetahuinya, tetapi hal utama adalah bahwa pengubah menambahkan bagian yang diperlukan ke konten View tertentu.
  • Wadah
    Kontainer pertama yang dimulai dengan "Hello, world" standar adalah HStack dan VStack . Beberapa saat kemudian, Group , Section , dan lainnya muncul. Bahkan, kontainer adalah View yang sama, tetapi mereka memiliki fitur. Anda memberikan mereka beberapa konten yang ingin Anda tampilkan. Seluruh fitur wadah itu entah bagaimana harus mengelompokkan dan menampilkan elemen konten ini. Dalam pengertian ini, kontainer mirip dengan pengubah, dengan satu-satunya perbedaan adalah bahwa pengubah dimaksudkan untuk mengubah satu tampilan yang sudah jadi, dan wadah mengatur tampilan ini (elemen konten, atau blok sintaksis deklaratif) dalam urutan tertentu, misalnya, secara vertikal atau horizontal ( VStack{...} HStack{...} ). Ada juga wadah khusus, seperti ForEach atau GeometryReader , kita akan membicarakannya nanti.

    Secara umum, saya menganggap wadah sebagai Tampilan apa pun, di mana Konten dapat diteruskan sebagai parameter.

Dan itu saja. Semua elemen dari SwiftUI murni dapat dikaitkan dengan salah satu dari jenis ini. Ya, ini tidak cukup untuk mengisi Tampilan Anda dengan fungsionalitas, tetapi hanya ini yang Anda butuhkan untuk menunjukkan fungsionalitas Anda di layar.

.modifiers () - bagaimana pengaturannya?


Mari kita mulai dengan yang paling sederhana. Pengubah sebenarnya adalah hal yang sangat sederhana. Dia hanya mengambil beberapa View , menerapkan beberapa perubahan padanya (atau tidak?) , Dan mengembalikannya kembali. Yaitu Pengubah adalah fungsi dari View itu sendiri, yang mengembalikan self , setelah sebelumnya melakukan beberapa modifikasi.

Di bawah ini adalah contoh kode yang dengannya saya mendeklarasikan pengubah saya sendiri. Lebih tepatnya, saya membebani frame(width:height:) pengubah yang ada frame(width:height:) , dengan mana Anda dapat memperbaiki dimensi tertentu dari Tampilan tertentu. Dari kotak untuk itu, Anda perlu menentukan lebar dan tinggi, dan saya harus meneruskan objek CGSize ke dalamnya dengan satu argumen, yang merupakan deskripsi hanya panjang dan lebar. Mengapa saya membutuhkan ini, saya akan katakan nanti.

 struct FrameFromSize: ViewModifier{ let size: CGSize func body(content: Content) -> some View { content .frame(width: size.width, height: size.height) } } 

Dengan kode ini, kami telah membuat struktur yang sesuai dengan protokol ViewModifier . Protokol ini mengharuskan kita bahwa fungsi body() diimplementasikan dalam struktur ini, inputnya akan berupa beberapa Content , dan output akan memiliki some View : tipe yang sama dengan parameter body dari View kita (kita akan berbicara tentang beberapa View di bawah) . Content macam apa ini?

Konten + ViewBuilder = Lihat


Dalam dokumentasi bawaan tentangnya dikatakan:
`content` adalah proxy untuk tampilan yang akan membuat modifier diwakili oleh` Self` yang diterapkan padanya.
Ini adalah tipe proksi, yang merupakan prefab Tampilan yang dapat diterapkan pengubah. Semacam produk setengah jadi. Sebenarnya, Content adalah penutupan gaya deklaratif yang menggambarkan struktur tampilan. Jadi, jika kita memanggil pengubah ini untuk beberapa Tampilan, maka yang dia lakukan adalah mendapatkan penutup dari body dan meneruskannya ke fungsi body kita, di mana kita menambahkan lima sen ke penutupan ini.

Sekali lagi, View adalah struktur utama yang menyimpan semua parameter yang diperlukan untuk menghasilkan gambar di layar. Termasuk instruksi perakitan, yang isinya. Dengan demikian, penutupan dalam gaya deklaratif ( Content ) yang diproses menggunakan ViewBuilder mengembalikan kita Tampilan.

Mari kembali ke pengubah kami. Pada prinsipnya, deklarasi struktur FrameFromSize sudah cukup untuk mulai menerapkannya. Di dalam body kita bisa menulis seperti ini:

 RoundedRectangle(cornerRadius: 4).modifier(FrameFromSize(size: size)) 

modifier adalah metode protokol tampilan yang mengekstraksi konten dari View dimodifikasi, meneruskannya ke fungsi tubuh dari struktur pengubah, dan meneruskan hasilnya lebih jauh ke pemrosesan ViewBuilder , atau ke pengubah berikutnya, jika kita memiliki rantai modifikasi.

Tetapi Anda dapat membuatnya lebih ringkas dengan mendeklarasikan pengubah Anda sendiri sebagai suatu fungsi, sehingga memperluas kemampuan protokol View.

 extension View{ func frame(_ size: CGSize) -> some View { self.modifier(FrameFromSize(size: size)) } } 

Dalam hal ini, saya membebani .frame(width: height:) pengubah yang ada .frame(width: height:) varian lain dari parameter .frame(width: height:) . Sekarang, kita dapat menggunakan opsi memanggil modifier frame(size:) untuk View apa pun. Ternyata, tidak ada yang rumit.

Sedikit tentang kesalahan
Ngomong-ngomong, saya berpikir bahwa itu tidak perlu untuk memperluas seluruh protokol, itu akan cukup untuk memperluas secara khusus RoundedRectangle dalam kasus saya, dan itu seharusnya bekerja, seperti yang tampak bagi saya - tetapi tampaknya Xcode tidak mengharapkan kelancangan seperti itu, dan jatuh dengan kesalahan yang tidak dapat dipahami “ Abort trap: 6 "dan proposal untuk mengirim dump ke pengembang. Secara umum, di SwiftUI, deskripsi kesalahan sejauh ini sangat sering tidak sepenuhnya mengungkapkan penyebab kesalahan ini.

Dengan cara yang sama, Anda dapat membuat pengubah khusus apa pun, dan menggunakannya dengan cara yang sama seperti SwiftUI bawaan:

 RoundedRectangle(cornerRadius: 4).frame(size) 

Nyaman, ringkas, jelas.

Saya membayangkan rantai modifikasi ketika manik-manik digantung pada seutas benang - Pandangan kami. Analogi ini juga benar dalam arti bahwa urutan modifikasi disebut hal.



Hampir semua yang ada di SwiftUI adalah View
Omong-omong, komentar yang menarik. Sebagai parameter input, latar belakang tidak menerima warna, tetapi Lihat. Yaitu Kelas Warna bukan hanya deskripsi warna, ini adalah Tampilan lengkap, yang dapat diterapkan pengubah dan lebih banyak lagi. Dan sebagai latar belakang, dengan demikian, Anda dapat melewati View lainnya.

Pengubah - hanya untuk modifikasi
Mungkin perlu diperhatikan satu hal lagi. Pengubah yang tidak mengubah konten sumber diabaikan begitu saja oleh SwiftUI dan tidak dipanggil. Yaitu Anda tidak akan dapat membuat pemicu berdasarkan pengubah yang menyebabkan beberapa peristiwa, tetapi tidak melakukan tindakan apa pun dengan konten. Apple terus-menerus mendorong kami untuk meninggalkan beberapa tindakan saat runtime saat merender antarmuka, dan mempercayai gaya deklaratif.

Masih Melihat


Sebelumnya kami berbicara tentang apa yang terdiri dari tubuh, tubuh View , atau instruksi perakitannya. Mari kita kembali ke View itu sendiri. Pertama-tama, ini adalah struktur di mana beberapa parameter dapat dideklarasikan, dan body hanyalah salah satunya. Seperti yang telah kami katakan, mencari tahu apa Content , body adalah instruksi tentang cara mengumpulkan Tampilan yang diinginkan, yang merupakan penutupan dalam gaya deklaratif. Tapi apa yang harus mengembalikan penutupan kita?

beberapa View - kenyamanan




Dan kami dengan lancar sampai pada sebuah pertanyaan bahwa untuk waktu yang lama saya tidak bisa mengetahuinya, meskipun ini tidak menghentikan saya dari menulis kode kerja. Apa ini some View ? Dokumentasi mengatakan bahwa deskripsi ini adalah "tipe hasil buram" - tetapi itu tidak masuk akal.

some kata kunci adalah versi "umum" dari deskripsi jenis yang dikembalikan oleh penutupan yang tidak bergantung pada apa pun selain kode itu sendiri. Yaitu Hasil dari mengakses properti yang dihitung dari badan View kami haruslah beberapa struktur yang memenuhi protokol View. Mungkin ada banyak dari mereka - Teks, Gambar, atau mungkin beberapa struktur yang Anda nyatakan. Keseluruhan chip dari beberapa kata kunci adalah untuk menyatakan "generik" yang sesuai dengan protokol View. Ini ditentukan secara statis oleh kode yang diterapkan di dalam tubuh View Anda, dan Xcode cukup mampu mengurai kode ini, dan menghitung tanda tangan spesifik dari nilai pengembalian (well, hampir selalu) . Dan beberapa hanya upaya untuk tidak membebani pengembang dengan upacara yang tidak perlu. Cukup bagi pengembang untuk mengatakan: "akan ada semacam Tampilan", dan yang mana - atur sendiri. Kuncinya di sini adalah bahwa jenis beton ditentukan bukan oleh parameter input, seperti dengan tipe generik yang biasa, tetapi langsung oleh kode. Karena itu, di atas, generik saya kutip.

Xcode harus dapat menentukan jenis tertentu tanpa mengetahui dengan tepat nilai yang Anda berikan ke struktur ini. Ini penting untuk dipahami - setelah dikompilasi, ekspresi Some View diganti dengan tipe spesifik dari View Anda. Tipe ini cukup deterministik, dan bisa sangat kompleks, misalnya, seperti ini: Group<TupleView<(Text, ForEach<[SomeClass], SomeClass.ID, Text>)>> .

Kode sampel dapat dipulihkan dari jenis ini:

 Group{ Text(…) ForEach(…){(value: SomeClass) in Text(…) } } 

ForEach , seperti yang bisa dilihat dari tipe signature, bukan loop runtime. Ini hanya View yang dibangun berdasarkan array dari objek SomeClass . sebagai pengenal subView tertentu yang terkait dengan elemen koleksi, ID elemen ditunjukkan, dan untuk setiap elemen subView tipe Text subView . Text dan TupleView digabungkan dalam TupleView , dan semua ini ditempatkan dalam Group . Kami akan berbicara lebih banyak tentang ForEach lebih rinci.

Bayangkan berapa banyak tulisan yang akan terjadi jika kita dipaksa untuk menggambarkan tanda tangan persis seperti body parameter? Untuk menghindari ini, some kata kunci telah dibuat.

Ringkasan
some , ini "generik - dan sebaliknya". Kami mendapatkan generik klasik dari luar fungsi, dan sudah mengetahui tipe spesifik dari tipe generik, Xcode menentukan cara kerja fungsi kami. some- tidak tergantung pada parameter input, tetapi hanya pada kode itu sendiri. Ini hanyalah singkatan, yang memungkinkan untuk tidak menentukan jenis tertentu, tetapi untuk menunjukkan hanya keluarga dari nilai yang dikembalikan oleh fungsi (protokol).

beberapa View - dan konsekuensi


Pendekatan untuk menghitung tipe statis dari ekspresi di dalam tubuh memunculkan, menurut saya, dua poin penting:

  • Saat dikompilasi, Xcode mem-parsing isi tubuh untuk menghitung tipe pengembalian spesifik. Dalam tubuh yang kompleks, ini bisa memakan waktu. Dalam beberapa badan yang sangat kompleks, ia mungkin tidak dapat mengatasi waktu yang waras sama sekali, dan ia akan langsung mengatakannya.

    Secara umum, View perlu dijaga sesederhana mungkin. Struktur kompleks paling baik ditempatkan pada tampilan terpisah. Dengan demikian, seluruh rantai tipe nyata digantikan oleh satu jenis - CustomView Anda, yang memungkinkan kompiler tidak menjadi gila dengan semua kekacauan ini.
    Ngomong-ngomong, benar-benar sangat nyaman untuk men-debug sepotong kecil View besar, di sini, dengan cepat, menerima dan mengamati hasilnya di Canvas.
  • Kita tidak bisa mengendalikan arus secara langsung. Jika - jika SwiftUI masih dapat memprosesnya dengan membuat “Schrödinger View” dengan tipe <_ConditionalContent <Text, TextField >> maka operator kondisi trinar hanya dapat digunakan untuk memilih nilai parameter tertentu, tetapi bukan tipe, atau bahkan untuk memilih urutan pengubah.

    Tetapi perlu mengembalikan urutan pengubah yang sama, dan catatan seperti itu tidak lagi menjadi masalah.

Kecuali tubuh


Namun, mungkin ada parameter lain dalam struktur yang dapat Anda gunakan. Sebagai parameter kita dapat mendeklarasikan hal-hal berikut.

Parameter eksternal


Ini adalah parameter struktur sederhana yang harus kami lewati dari luar selama inisialisasi agar View dapat membuatnya entah bagaimana:

 struct TextView: View { let textValue: String var body: some View { Text(textValue) } } 

Dalam contoh ini, textValue untuk struktur TextView adalah parameter yang harus diisi secara eksternal karena tidak memiliki nilai default. Mengingat bahwa struktur mendukung pembuatan inisialisasi otomatis, kami dapat menggunakan Tampilan ini hanya:

  TextView(textValue: "some text") 

Dari luar, Anda juga dapat mentransfer penutupan yang perlu dilakukan saat suatu peristiwa terjadi. Misalnya, Button(lable:action:) tidak hanya itu: melakukan penutupan tindakan yang lewat ketika tombol diklik.

status - parameter


SwiftUI sangat aktif menggunakan fitur Swift 5.1 baru - Wrapper Properti .

Pertama-tama, ini adalah variabel keadaan - parameter yang disimpan dari struktur kami, perubahan yang harus tercermin pada layar.Mereka dibungkus pembungkus khusus @State- untuk tipe primitif, dan @ObservedObject- untuk kelas. Kelas harus memenuhi protokol ObservableObject- ini berarti kelas ini harus dapat memberi tahu pelanggan (Lihat, yang menggunakan nilai ini dengan pembungkus @ObservedObject) tentang perubahan properti mereka. Untuk melakukan ini, cukup bungkus properti yang diperlukan di @Published.

Jika Anda tidak mencari cara mudah, atau Anda memerlukan fungsionalitas tambahan, alih-alih pembungkus ini, Anda dapat menggunakan ObservableObjectPublisherdan mengirim pemberitahuan secara manual menggunakan peristiwa willSet()parameter ini, seperti dijelaskan, misalnya, di sini .

Ingat, saya mengatakan itubodyApakah ini properti yang hanya dapat dihitung? Pada awalnya, saya tidak segera memahami semuanya tentang variabel State, dan saya mencoba mendeklarasikan beberapa variabel State di dalam bodytanpa pembungkus apa pun. Masalahnya ternyata bodyadalah, seperti yang saya katakan, instruksi tanpa kewarganegaraan. Tampilan dihasilkan sesuai dengan instruksi ini, dan seluruh konteks yang dinyatakan di dalam tubuh pergi ke TPA. Selanjutnya, hanya parameter struktur yang disimpan yang hidup. Saat mengubah Status-parameter, semua parameter kita Viewdiperbarui. Instruksi sekali lagi diambil, nilai saat ini dari semua parameter struktur disubstitusi ke dalamnya, gambar pada layar dikumpulkan, instruksi dibuang lagi sampai waktu berikutnya. Variabel dideklarasikan di dalam body- bersama dengan itu. Untuk pengembang berpengalaman, ini mungkin jelas, tetapi pada awalnya, saya tersiksa dengan ini, tidak memahami esensi dari proses tersebut.

Dan satu lagi komentar
Anda tidak dapat menggunakan didSet willSetperistiwa parameter struktur yang dibungkus pembungkus. Kompiler memungkinkan Anda untuk menulis kode ini, tetapi tidak bisa dijalankan. Mungkin karena pembungkusnya adalah semacam kode templat yang dieksekusi ketika peristiwa ini terjadi.

Contoh Keadaan Klasik :

 struct ContentView: View { @State var tapCount = 0 var body: some View { VStack { Button(action: {self.tapCount += 1}, label: { Text("Tap count \(tapCount)") }) } } } 

Mengikat parameter


Nah, untuk mencerminkan beberapa perubahan dalam tampilan Lihat @State @ObservedObject. Tapi bagaimana perubahan ini bisa terjadi View? Untuk melakukan ini, SwiftUI memiliki PropertyWrapper lain - @Binding. Mari kita tambahkan contoh kita dengan tombol untuk menghitung klik. Misalkan kita memiliki orangtua View, yang mencerminkan, antara lain, penghitung klik, dan anak Viewdengan tombol. Pada tampilan induk, penghitung dideklarasikan sebagai @State- dapat dimengerti, tetapi kami ingin penghitung di layar diperbarui. Tetapi di anak perusahaan, penghitung harus dinyatakan sebagai @Binding. Ini adalah Wrapper Properti lain, dengan bantuan yang kami nyatakan parameter struktur yang tidak hanya akan berubah, tetapi juga kembali ke induk View. Ini semacam inoutpenanda untukView. Ketika nilai berubah dalam tampilan anak, perubahan ini diterjemahkan kembali ke tampilan induk, tempat asalnya. Dan seperti halnya inout, kita perlu menandai nilai yang dikirimkan dengan simbol khusus $,untuk menunjukkan bahwa kita sedang menunggu nilai yang dikirim untuk berubah di dalam tampilan lain. Bereaksi dalam aksi.

 struct ContentView: View { @State var tapCount = 0 var body: some View { VStack{ SomeView(count: $tapCount) Text("you tap \(tapCount) times") } } } 

Ini juga tercermin dalam tipe data. @Binding var tapCount: Intmisalnya, ini bukan hanya Inttipe lagi , itu

 Binding<Int> 

Ini berguna untuk mengetahui, misalnya, jika Anda ingin menulis penginisialisasi Anda sendiri View.

 struct SomeView: View{ @Binding var tapCount: Int init(count: Binding<Int>){ self._tapCount = count //    -    } var body: some View{ Button(action: {self.tapCount += 1}, label: { Text("Tap me") }) } } 

Harap dicatat bahwa di dalam initAnda @PropertyWrapperharus menggunakan garis bawah untuk merujuk ke parameter yang dibungkus dalam beberapa parameter self._- ini berfungsi di inisialisasi, ketika selfmasih dalam proses pembuatan. Lebih tepatnya, dengan bantuan self._kita merujuk ke parameter beserta pembungkusnya. Menerapkan langsung ke nilai di dalam pembungkus dilakukan tanpa menggarisbawahi.

Pada gilirannya, jika Anda memiliki variabel yang dibungkus dalam beberapa jenis input PropertyWrapper, kami mendapatkan jenis pembungkus, dalam hal ini

 Binding<Int> 

Anda Intdapat mengakses nilai jenis secara langsung .wrappedValue.

Dan seperti biasa, menyapu pribadi
, Binding . View View. View , @Binding-. , View State @Binding — , State- Binding. -, , .

Proyek Lingkungan


Singkatnya, EnvironmentObjectparameternya seperti Binding, hanya segera untuk semua orang Viewdalam hierarki, tanpa harus memberikannya secara eksplisit.

 ContentView().environmentObject(session) 

Biasanya, keadaan aplikasi saat ini, atau beberapa bagian darinya, yang dibutuhkan oleh banyak View sekaligus, ditransmisikan. Misalnya, data tentang pengguna, sesi, atau sesuatu yang serupa, masuk akal untuk meletakkannya di EnvironmentObject sekali, di tampilan root. Di setiap View, di mana mereka diperlukan, mereka dapat diekstraksi dari lingkungan dengan mendeklarasikan variabel dengan wrapper @EnvironmentObject, misalnya seperti ini

  @EnvironmentObject var session: Session 

Pengidentifikasi nilai tertentu adalah tipe itu sendiri. Jika Anda memasukkan EnvironmentObjectbeberapa nilai dari jenis yang sama, maka urutannya penting. Untuk sampai ke 3, misalnya, nilai, Anda harus mendapatkan semua nilai secara berurutan, bahkan jika Anda tidak membutuhkannya. Oleh karena itu, EnvironmentObjectsangat cocok untuk mencerminkan keadaan aplikasi, tetapi tidak cocok untuk melewatkan beberapa nilai dari jenis yang sama View. Mereka harus ditransmisikan secara manual, melalui Binding.

@ Lingkungan hampir sama. Dalam arti, ini adalah keadaan lingkungan, mis. OS Lebih mudah untuk melewati pembungkus ini, misalnya, posisi layar (vertikal atau horizontal), tema terang atau gelap digunakan, dll. Juga, melalui pembungkus ini Anda dapat mengakses database saat menggunakan CoreData:

 @Environment(\.managedObjectContext) var moc: NSManagedObjectContext 

Omong-omong, banyak hal menarik telah dilakukan untuk bekerja dengan CoreData di SwiftUI. Tapi soal ini, mungkin, lain kali. Jadi artikelnya telah berkembang melampaui semua harapan.

@PropertyWrapper khusus


Pada umumnya, PropertyWrapperini adalah jalan pintas untuk setter dan pengambil, sama untuk semua parameter yang dibungkus dalam yang sama property wrapper. Anda dapat sepenuhnya memulihkan fungsi ini sendiri dengan menghapus deklarasi wrapper dan menulis parameter pengambil {} setter {}, tetapi Anda harus melakukan ini setiap kali, untuk masing-masing View, menduplikasi kode. Misalnya, menggunakannya PropertyWrappersangat nyaman untuk menyembunyikan pekerjaan UserDefaults.

 @propertyWrapper struct UserDefault<T> { var key: String var initialValue: T var wrappedValue: T { set { UserDefaults.standard.set(newValue, forKey: key) } get { UserDefaults.standard.object(forKey: key) as? T ?? initialValue } } } 

Dengan demikian, kita dapat menyimpan tipe data primitif dalam penyimpanan UserDefaults. Apple mengklaim kecepatan akses yang sangat baik ke penyimpanan ini, jadi mungkin tidak perlu untuk cache data ini dalam memori dalam bentuk parameter atau variabel struktur, kecuali tentu saja mereka digunakan dalam loop besar dan tugas-tugas yang menuntut kecepatan.

Dengan ini, Anda dapat membuat tipe rintisan (dalam hal ini, enumerasi) untuk mendeklarasikan variabel statis untuk mengakses nilai spesifik yang disimpan UserDefaults, menggunakan pembungkus yang baru saja dibuat:

 enum UserPreferences { @UserDefault(key: "isCheatModeEnabled", initialValue: false) static var isCheatModeEnabled: Bool @UserDefault(key: "highestScore", initialValue: 10000) static var highestScore: Int @UserDefault(key: "nickname", initialValue: "cloudstrife97") static var nickname: String } 

Hasilnya dapat digunakan dengan sangat ringkas, berfokus pada logika dan tampilan visual, dan semua pekerjaan dilakukan di bawah tenda.

 UserPreferences.isCheatModeEnabled = true UserPreferences.highestScore = 25000 UserPreferences.nickname = "squallleonhart” 

Contoh awalnya dijelaskan di sini .

Wadah


Nah, hal terakhir yang saya diskusikan dari daftar saya adalah kontainer. Kami sudah sebagian menyinggung ini ketika kami berbicara tentang body. Faktanya, wadah adalah hal biasa View. Satu-satunya perbedaan adalah bahwa sebagai salah satu parameter struktur ini, kami mengirimkan konten. Ingat bahwa konten adalah penutup yang mengandung satu atau lebih ekspresi deklaratif. Penutupan ini, jika diproses dengan bantuan @ViewBuilder, akan mengembalikan kepada kami Tampilan baru, menggabungkan dengan cara tertentu semua Tampilan yang tercantum dalam penutupan (blok konten). Pada saat yang sama, untuk wadah yang berbeda, mekanisme pemrosesan blok itu sendiri berbeda. VStackmengatur elemen konten secara vertikal, HStackhorizontal, dan sebagainya. Ini seperti pengubah, tapi kali ini bukan hanya satu Tampilan spesifik yang sedang dimodifikasi, tetapi keseluruhanContentdipindahkan ke wadah dan yang baru dihasilkan View. Apalagi yang baru ini Viewmemiliki tipe baru. Misalnya, untuk HStack{Text(…)}tipe ini akan TupleView<Text, Image>.

Namun, jangan lupa bahwa apapun View, termasuk wadah, adalah struktur yang dapat memiliki parameter lain selain bodi. Sebagai contoh, untuk waktu yang lama saya tidak bisa menemukan cara menghilangkan celah kecil di Text(«a») Text(«b»)dalam HStack. Saya menghabiskan banyak waktu dengan offset()dan position(), menghitung koordinat offset berdasarkan panjang garis, sampai saya secara tidak sengaja menemukan sintaks penuh deklarasi HStack:
HStack (spasi:, alingment:, konteks :).
Sederhananya, dua parameter pertama adalah opsional, dan dilewati dalam kebanyakan contoh. Kesalahan pemula - tidak melihat sintaks lengkap.

Foreach


Secara terpisah, ada baiknya dibicarakan ForEach. Ini adalah wadah yang berfungsi untuk merefleksikan di layar semua elemen koleksi yang ditransfer. Pertama-tama, Anda perlu memahami bahwa ini tidak sama dengan meminta beberapa koleksi forEach(…). Seperti yang kami katakan di atas, ia ForEachmengembalikan satu yang unik View, dibuat berdasarkan elemen dari koleksi yang ditransfer. Yaituitu hanya wadah lain di mana koleksi ditransfer, dan instruksi tentang cara merefleksikan elemen-elemen koleksi di layar.
Selain itu, ia ForEachharus ditempatkan di dalam wadah lain yang sudah menentukan bagaimana mengelompokkan banyak entitas ini - dengan menempatkannya secara vertikal, horizontal, atau, misalnya, menempatkannya dalam daftar ( List).

ForEachmenerima tiga parameter: koleksi ( data: RandomAccesCollection), alamat pengidentifikasi elemen koleksi ( id: Hashable), dan konten ( content: ()->Content). Yang ketiga yang telah kita bahas: seperti wadah lainnya, ia ForEachmenerima Content- yaitu. korsleting. Tetapi tidak seperti wadah biasa, di mana contenttidak mengandung parameter, itu ForEachmelewati elemen koleksi penutupan yang dapat digunakan untuk menggambarkan konten.

Koleksinya ForEachtidak cocok untuk apa pun, tetapi hanya RandomAccesCollection. Untuk berbagai koleksi yang tidak teratur, cukup memanggil metode sorted(by:)yang dapat Anda peroleh RandomAccesCollection.

ForEach- Ini adalah set yang subViewdibuat untuk setiap elemen koleksi berdasarkan konten yang dikirimkan. Penting untuk dicatat bahwa SwiftUI perlu mengetahui subViewelemen mana yang terkait dengan koleksi. Untuk ini, masing View- masing harus memiliki pengenal. Parameter kedua diperlukan tepat untuk ini. Jika elemen koleksi adalah Hashabletipe, seperti string, Anda dapat menulis secara sederhana id: \.self. Ini berarti bahwa string itu sendiri akan menjadi pengidentifikasi. Jika elemen koleksi adalah kelas dan memenuhi protokolIdentifiable- maka argumen kedua bisa dilewatkan. Dalam hal ini, id dari setiap item dalam koleksi akan menjadi pengidentifikasi subView. Jika objek Anda memiliki beberapa jenis alat peraga yang memberikan keunikan, dan yang memenuhi protokol Hashable, Anda dapat menentukannya seperti ini:

 ForEach(values, id: \.value){item in …} 

Dalam contoh saya, valuesadalah array objek kelas SomeObjectyang propsnya dideklarasikan value: Int. Bagaimanapun, Anda harus memastikan bahwa setiap pengidentifikasi yang Viewterkait dengan elemen koleksi Anda adalah unik . Misalnya, dalam konteks Anda, beberapa parameter objek Anda dapat berubah. Viewitu harus dicocokkan 1 hingga 1 dengan objek data (elemen koleksi), jika tidak maka tidak akan jelas ke mana harus mengembalikan perubahan @Bindingparameter View.

Ngomong-ngomong, mengorganisir merangkak elemen koleksi yang tidak memenuhi Identifikasi juga dapat dilakukan dengan menggunakan indeks. Misalnya, seperti ini:
 ForEach(keys.indices){ind in SomeView(key: self.keys[ind]) } 

Dalam hal ini, traversal akan dibangun bukan oleh elemen itu sendiri, tetapi oleh indeks mereka. Untuk koleksi kecil, ini bisa diterima. Pada koleksi dengan sejumlah besar elemen, ini mungkin dapat mempengaruhi kinerja, terutama ketika elemen koleksi bukan tipe referensi, tetapi, misalnya, string tebal atau data JSON. Secara umum, gunakan dengan hati-hati.

Poin penting tentang Contentapa yang dimaksud ForEach. Dia sangat murung, dan menolak untuk bekerja secara normal dengan penutupan, dengan lebih dari satu blok (mis., Dia melihat konten dari satu baris secara normal, tetapi dia sudah tidak memiliki 2 atau lebih). Ini diselesaikan dengan cukup sederhana, cukup sederhana untuk memasukkan semua konten ke dalamnya Groupe{}- peretasan seperti itu tidak lagi menjadi masalah.

Cukup nyatakan variabel internal dalam lingkup penutupan ini tidak akan berfungsi. Setiap penutupan yang diteruskan ke ViewBuilder tidak dapat berisi deklarasi variabel. Ingat, di awal artikel saya memberi contoh membuat pengubah .frame(size:)? Saya membuatnya karena alasan ini. Saya menghitung ukuran tombol berdasarkan jumlah tombol-tombol ini dalam satu baris dan jumlah baris (saya tidak senang dengan peregangan otomatis, tombol yang berbeda harus memiliki ukuran yang berbeda). Fungsi mengembalikan CGSize, dan beberapa tingkat struktur bersarang dirayapi di dalam. Jika mungkin untuk menjalankan fungsi satu kali, tulis hasilnya sebagai ukuran variabel, lalu panggil.ftame(width: size.width, height: size.height)"Aku akan melakukannya." Tetapi tidak ada kemungkinan seperti itu, dan saya tidak ingin melakukan fungsi dua kali - karena saya menghindari batasan ini dan memasukkan bagian dari kode ke dalam pengubah.

Tampilan wadah khusus


Nah, seperti yang terjadi, saya akan memberikan contoh membuat wadah kustom. Cukup sering, hubungan beberapa objek dari tipe "1: N" dapat dengan mudah direpresentasikan dalam bentuk kamus. Tidak dict: [KeyObject: [SomeObject]]sulit untuk mengeksekusi kueri dan mengubah hasilnya menjadi kamus jenis .

Dalam hal ini, objek kelas bertindak sebagai kunci kamus KeyObject(untuk ini, harus mendukung protokol Hashable), dan nilainya adalah array objek kelas lain - SomeObject.

 class SomeObject: Identifiable{ let value: Int public let id: UUID = UUID() init(value: Int){ self.value = value } } class KeyObject: Hashable, Comparable{ var name: String init(name: String){ self.name = name } static func < (lhs: KeyObject, rhs: KeyObject) -> Bool { lhs.name < rhs.name } static func == (lhs: KeyObject, rhs: KeyObject) -> Bool { return lhs.name == rhs.name } func hash(into hasher: inout Hasher) { hasher.combine(name) } } 

Jika aplikasi Anda merencanakan beberapa jenis analitik dengan pengelompokan, masuk akal untuk membuat wadah terpisah untuk menampilkan kamus seperti itu agar tidak menggandakan semua kode di setiap tampilan. Dan mengingat fakta bahwa pengelompokan dapat diubah oleh pengguna, kita harus menggunakan generik. Saya tidak memperumitnya dengan menambahkan desain visual, hanya menyisakan struktur wadah kami:

 struct TreeView<K: Hashable, V: Identifiable, KeyContent, ValueContent>: View where K: Comparable, KeyContent: View, ValueContent: View{ let data: [K: [V]] let keyContent: (K)->KeyContent let valueContent: (V)->ValueContent var body: some View{ VStack(alignment: .leading, spacing: 0){ ForEach(data.keys.sorted(), id: \.self){(key: K) in VStack(alignment: .trailing, spacing: 0){ self.keyContent(key) ForEach(self.data[key]!){(value: V) in self.valueContent(value) } } } } } } 

Seperti yang Anda lihat, wadah menerima kamus jenis [K: [V]](di mana Kadalah jenis objek kunci kamus, Vadalah jenis array yang terdiri dari nilai-nilai kamus), dan dua konteks: satu untuk menampilkan kunci kamus, dan yang lain untuk menampilkan nilai-nilai. Sayangnya, saya tidak menemukan contoh membuat kustom ViewBuilder-untuk wadah kustom (mungkin opsi ini tidak ada), jadi kita harus menggunakan yang standar ForEach. Karena hanya menerima pada input RandomAccessCollection, dan dict.keystidak, kami harus menggunakan penyortiran. Oleh karena itu persyaratan untuk mendukung protokol Comparablek KeyObject.

Saya menggunakan dua ForEachkontainer bersarang . Dalam kasus pertama, saya menggunakan hash dari item koleksi (\.self) sebagai pengidentifikasi setiap sarang View. Saya bisa melakukan ini karena kunci kamus harus mendukung protokol Hashable. Dalam kasus kedua, saya menambahkan SomeObjectdukungan protokol ke kelasIdentifiable. Ini memungkinkan saya untuk tidak menentukan kunci komunikasi sama sekali - id digunakan secara otomatis. Dalam kasus saya, id tidak disimpan di mana pun. Setiap kali sebuah objek dibuat - apakah itu kreasi dalam kode, atau mengambil menggunakan query database - id baru dihasilkan. Untuk antarmuka, ini tidak penting. Itu tidak akan berubah sepanjang kehidupan objek yaitu sesi, dan ini cukup untuk menampilkannya di bawah id ini. Dan jika lain kali Anda membuka aplikasi itu akan memiliki id yang berbeda - tidak ada hal buruk yang akan terjadi. Jika objek Anda sudah memiliki bidang kunci, Anda dapat dengan mudah membuat id sebagai parameter yang dihitung, dan tetap menggunakan dukungan protokol ini dan sintaks steno ForEach.

Contoh menggunakan wadah kami:

 struct ContentView: View { let dict: [KeyObject: [SomeObject]] = [ KeyObject(name: "1st group") : [SomeObject(value: 1), SomeObject(value: 2), SomeObject(value: 3)], KeyObject(name: "2nd group") : [SomeObject(value: 4), SomeObject(value: 5), SomeObject(value: 6)], KeyObject(name: "3rd group") : [SomeObject(value: 7), SomeObject(value: 8), SomeObject(value: 9)] ] var body: some View { TreeView(data: dict, keyContent: {keyObject in Text("the key is: \(keyObject.name)") } ){valueObject in Text("value: \(valueObject.value)") } } } 

dan hasilnya di layar di Canvas:



untuk dilanjutkan


Itu saja untuk saat ini. Saya juga ingin menyoroti semua garu yang saya injak, mencoba menggunakannya CoreDatabersamaan dengan SwiftUI, tetapi, terus terang, saya tidak berharap bahwa hanya dasar-dasar SwiftUI yang akan memakan banyak waktu dan artikelnya akan sangat tebal. Jadi, seperti kata mereka, harus dilanjutkan.

Jika Anda memiliki sesuatu untuk ditambahkan atau diperbaiki - selamat datang di komentar. Saya akan mencoba untuk mencerminkan komentar yang signifikan dalam artikel ini.

Source: https://habr.com/ru/post/id485548/


All Articles