Berinteraksi dengan aplikasi, kami pada suatu titik mengaktifkan keyboard sistem untuk mengetik pesan atau mengisi kolom yang diperlukan. Pernahkah Anda mengalami situasi ketika keyboard ditampilkan, tetapi tidak ada bidang untuk memasukkan pesan, atau sebaliknya - ada keyboard, di mana memasukkan teks, tidak terlihat? Bug dapat dikaitkan dengan masalah dalam aplikasi tertentu, serta kekurangan umum pada keyboard sistem.Konstantin Mordan , pengembang iOS dari Mail.ru, melihat semuanya dalam karyanya: setelah menganalisis metode kontrol keyboard di iOS, ia memutuskan untuk berbagi bug utama dan pendekatan yang ia gunakan untuk mendeteksi dan memperbaikinya.Perhatian: di bawah potongan, kami menempatkan banyak gif untuk menunjukkan bug dengan jelas. Anda akan menemukan lebih banyak contoh dalam laporan video Konstantin di AppsConf.Menerapkan panggilan keyboard sistem
Mari kita mulai dengan memahami cara menerapkan panggilan keyboard secara umum.
Bayangkan Anda sedang mengembangkan aplikasi yang tugasnya merakit Aika (karakter South Park) ke dalam seluruh Kanada menggunakan keyboard. Ketika Aiku ditekan di perut, keyboard itu pergi, sehingga mengangkat kaki pahlawan kita ke kepala.
Untuk mengimplementasikan tugas, Anda dapat menggunakan
InputAccessoryView atau memproses pemberitahuan sistem.
InputAccessoryView
Mari kita lihat opsi pertama.
Di ViewController, buat Tampilan yang akan naik bersama dengan keyboard, dan berikan bingkai. Penting bahwa Tampilan ini tidak boleh ditambahkan sebagai subview. Selanjutnya, kita menimpa properti
canBecomeFirstResponder dan mengembalikan true. Setelah kita mendefinisikan ulang properti
UIResponder -
inputAccessoryView dan meletakkan tampilan di sana. Untuk menutup keyboard, tambahkan
tapGesture dan di handlernya,
setel ulang responden pertama , yang kami buat Lihat.
class ViewController: UIViewController { var tummyView: UIView { let frame = CGRect(x: x, y: y, width: width, height: height) let v = TummyView(frame: frame) return v } override var canBecomeFirstResponder: Bool { return true } override var input AccessoryView: UIView? { return tummyView } func tapHandler ( ) { tummyView.resignFirstResponder ( ) } }
Tugas selesai, dan sistem itu sendiri memproses perubahan kondisi keyboard, menampilkannya dan memunculkan View, yang bergantung padanya.
Pemrosesan Pemberitahuan Sistem
Dalam hal memproses pemberitahuan, kami harus memproses sendiri pemberitahuan dari grup berikut:
- ketika keyboard akan / ditampilkan: keyboardWillShowNotification, keyboardDidShowNotification;
- ketika keyboard akan / disembunyikan: keyboardWillHideNotification, keyboardDidHideNotification;
- ketika bingkai keyboard akan / diubah: keyboardWilChangeFrameNotification, keyboardDidChangeFrameNotification.
Untuk menerapkan kasus kami, mari gunakan
keyboardWilChangeFrameNotification , karena notifikasi ini dikirim baik saat keyboard ditampilkan dan ketika disembunyikan.
Kami membuat
keyboardTracker, di dalamnya kami berlangganan untuk menerima notifikasi
keyboardWillChangeFrame , dan di handler kami mendapatkan bingkai keyboard, mengonversinya dari sistem koordinat layar ke sistem koordinat jendela, menghitung tinggi keyboard dan mengubah nilai Y dari Tampilan, yang harus dinaikkan oleh keyboard, ke ketinggian ini.
class KeyboardTracker { func enable ( ) { notificationCenter.add0observer(self, seletor: #selector( keyboardWillChangeFrame), name: UIResponder.keyboardWillChangeFrameNotification, object: nil) } func keyboardWillChangeFrame ( notification: NSNotification) { let screenCoordinatedKeyboardFrame = (userInfo [ UIResponder.keyboardFrameEndUserInfoKey ] as! NSValue ) .cgRectValue let keyboardFrame = window.convert ( screenCoordinatedKeyboardFrame, from: nil ) let windowHeight = window.frame.height let keyboardHeight = windowHeight - keyboardFrame.minY delegate.keyboardWillChange ( keyboardHeight ) } }
Tentang ini, tugas kita selesai, keyboard naik, mengumpulkan Ike ke Kanada.
Seperti yang bisa kita lihat, implementasi bekerja dengan keyboard cukup mudah di kedua kasus, sehingga semua orang bebas memilih metode yang sesuai sendiri. Dalam proyek kami, kami membuat pilihan yang mendukung pemberitahuan, jadi contoh dan wawasan lebih lanjut akan dikaitkan dengan pemrosesan pemberitahuan.
Mencari bug
Jika cara memanggil keyboard sangat sederhana, lalu dari mana datangnya bug? Tentu saja, jika aplikasi hanya mereproduksi skrip untuk membuka dan menutup keyboard, maka tidak akan ada masalah. Tetapi jika Anda mengubah hal-hal yang biasa, ingatlah bahwa tidak hanya aplikasi kita dapat menggunakan keyboard, tetapi juga yang lain, dan pengguna juga dapat beralih di antara mereka, maka kejutan tidak dapat dihindari.
Mari kita lihat sebuah contoh. Untuk melakukan ini, gunakan aplikasi kami dengan Ike: buka keyboard, beralih ke Notes, cetak sesuatu dan kembali ke aplikasi.
Masalah apa yang sudah terlihat? Pertama, tidak ada keyboard di App Switcher, meskipun ketika Anda meminimalkan aplikasi itu, dan bukannya itu, konten lain terlihat. Kedua, ketika Anda kembali ke aplikasi, keyboard masih tidak ada, dan kaki Ike jatuh ke bawah layar.
Mari kita lihat alasan perilaku ini. Seperti yang kita semua ingat dari diagram siklus aplikasi, transisi aplikasi dari keadaan aktif ke keadaan tidak aktif pertama di latar depan dan kemudian di latar belakang membutuhkan waktu.
Bagaimana dengan siklus hidup keyboard? Di iOS, untuk setiap unit waktu, keyboard hanya dapat dimiliki oleh salah satu aplikasi yang sedang berjalan, tetapi pemberitahuan tentang perubahan status keyboard diterima oleh semua aplikasi yang menandatanganinya.
Saat beralih dari satu aplikasi ke aplikasi lainnya, sistem akan mereset firstRespondernya, yang bertindak sebagai pemicu untuk menyembunyikan keyboard. Sistem mengirimkan notifikasi keyboardWillHide pertama untuk keyboard menghilang dan kemudian keyboardDidHideNotification. Pemberitahuan terbang ke aplikasi kedua. Dalam aplikasi baru, kami membuka keyboard: sistem mengirimkan keyboardWillShowNotification untuk keyboard muncul dan kemudian mengirimkan keyboardDidShowNotification -
demo , dengan fase siklus.

Jika Anda melihat
fragmen laporan (dari 8:39), Anda akan melihat saat ketika, setelah menyembunyikan keyboard, sistem mengirimkan keyboardDidHideNotification untuk meletakkan aplikasi pertama dalam keadaan tidak aktif. Ketika Anda beralih ke aplikasi olahraga dan meluncurkan keyboard, sistem mengirimkan keyboardWillShowNotification. Tetapi karena proses peralihan dan mulai cepat, dan waktu transisi antara fase siklus hidup dapat lebih lama, pemberitahuan yang diterima tidak hanya akan memproses aplikasi untuk olahraga, tetapi juga aplikasi untuk bir, yang belum berhasil beralih ke latar belakang.
Setelah mengetahui alasannya, sekarang mari kita mencari solusi untuk masalah dengan Ike.
Keputusan yang buruk
Hal pertama yang terlintas dalam pikiran adalah ide berhenti berlangganan / berlangganan pemberitahuan ketika meminimalkan / memaksimalkan aplikasi melalui mengaktifkan / menonaktifkan KeyboardTracker.
Untuk berhenti berlangganan, kami menggunakan metode applicationWillResignActive atau penangan notifikasi serupa dari sistem; untuk berlangganan, kami menggunakan applicationDidBecomeActive, tetapi agar tidak ketinggalan apa pun, kami juga akan memberi tahu metode applicationWillEnterForeground, yang dipanggil saat aplikasi masuk sebagai foreground tetapi belum menjadi aktif.
Ketika Anda memulai keyboard di aplikasi, kemungkinan besar semuanya akan berhasil, tetapi dengan tes yang lebih kompleks, misalnya, membuka keyboard dan mencoba merekam panggilan melalui suara, solusinya tidak akan berfungsi.
Apa yang terjadi Setelah mengklik tombol panggilan pesan suara, aplikasi firstResponder aplikasi direset, keyboard ditutup, metode applicationWillResignActive dipanggil dan kami berhenti berlangganan. Setelah menutup peringatan, sistem memulihkan keadaan aplikasi, tetapi sebelum metode applicationWillEnterForeground, dan terutama applicationDidBecomeActive, dipanggil.
Keputusan bagus
Solusi lain adalah penggunaan kaldu pelindung (Bool).
var wasTummyViewFirstResponderBeforeApp0idEnterBackground func willResignActive( notification: NSNotification) { wasTextFieldFirstResponderBeforeAppDidEnterBackground = tummyView.isFirstResponder } func willEnterForeground ( notification: NSNotification) { if wasTextFieldFirstResponderBeforeAppDidEnterBackground { UIView.performWithourAnimation { tummyView.becomeFirstResponder ( ) } } }
Kami ingat apakah keyboard dibuka sebelum topik, bagaimana aplikasi berhenti aktif, dan dalam metode applicationWillEnterForeground kami mengembalikan keadaan sebelumnya. Satu-satunya hal yang tersisa untuk diperbaiki adalah lubang di pengalih aplikasi.
pengalih aplikasi
Pengalih aplikasi menampilkan snapshot aplikasi yang dilakukan sistem setelah aplikasi masuk ke latar belakang. Tangkapan layar menunjukkan bahwa snapshot aplikasi kita dibuat pada saat keyboard sudah digunakan oleh aplikasi lain. Ini tidak kritis, tetapi hanya perlu beberapa klik untuk memperbaikinya.
Solusi yang bagus
Solusinya dapat dipinjam dari aplikasi perbankan yang telah belajar menyembunyikan data sensitif, dan juga membaca dari
Apple .
Anda dapat menyembunyikan data dalam metode applicationDidEnterBackground, mengaburkan dan menampilkan layar splash, dan dalam metode applicationWillEnterForeground kembali ke hierarki tampilan yang biasa.
Opsi ini tidak sesuai dengan kita, karena pada saat metode applicationDidEnterBackground dipanggil, aplikasi kita tidak lagi memiliki keyboard.
Keputusan bagus
Kami akan menggunakan metode familiar willResignActive, willEnterForeground dan didBecomeActive.
Meskipun aplikasi kami masih memiliki keyboard, Anda harus membuat snapshot aplikasi Anda sendiri dalam metode willResignActive dan memasukkannya ke dalam hierarki.
func willResignActive( notificaton: NSNotification) { let keyWindow = UIApplication.shared.keyWindow imageView = UIImageView( frame: keyWindow.bounds) imageView.image = snapshot ( ) let lastSubview = keyWindow.subviews.last lastSubview( imageView) }
Di metode willEnterForeground dan didBecomeActive, kami mengembalikan hierarki tampilan dan menghapus snapshot kami.
func willEnterForeground( notification: NSNotification) { imageView.removeFromSuperview( ) } func didBecomeActive( notification: NSNotification) { imageView.removeFromSuperview( ) }
Hasilnya, kami memperbaiki kedua kasus: di pengalih aplikasi, gambar yang indah dan keyboard tidak lagi melompat ketika beralih. Tampaknya ini bukan hal yang penting, tetapi untuk pengembangan produk, poin-poin ini sangat penting.
Berita buruk
Solusi sukses kami untuk masalah Ike terkait dengan kasus ketika keyboard dibuka sebelum meminimalkan aplikasi. Jika peralihan terjadi tanpa memperluas keyboard, sekali lagi kita akan melihat bahwa kaki Ike kita jatuh di bawah.
Ini bukan hanya masalah bagi aplikasi kita, perilaku ini juga diamati untuk Facebook, yang berfungsi dengan notifikasi, dan bahkan untuk iMessage, yang menggunakan inputAccessoryView untuk mengontrol keyboard. Hal ini disebabkan oleh fakta bahwa sebelum beralih ke latar belakang, aplikasi mengatur untuk memproses notifikasi keyboard orang lain.
pemberhentian keyboard secara interaktif
Tambahkan beberapa fungsi ke aplikasi kita dengan Ike, ajarkan program untuk menyembunyikan keyboard secara interaktif.
Keputusan yang buruk
Salah satu cara untuk membuat fungsi ini adalah mengubah bingkai tampilan keyboard. Kami membuat panGestureRecognizer, dalam penangannya kami menghitung nilai baru koordinat Y untuk keyboard, tergantung pada posisi jari kami, menemukan tampilan keyboard dan memperbaruinya dengan nilai koordinat Y.
func panGestureHandler( ) { let yPosition: CGFloat = value keyboardView( )?.frame.origin.y = yPosition }
Keyboard ditampilkan di jendela terpisah, jadi Anda perlu memeriksa seluruh array windows di aplikasi, memeriksa setiap elemen array apakah itu jendela keyboard dan, jika demikian, dapatkan tampilan dari itu yang menunjukkan keyboard.
func keyboardView( ) -> UIView? { let windows = UIApplication.shared.windows let view = windows.first { (window) -> Bool in return keyboardView( fromWindow: window) != nil } return view }
Sayangnya, solusi ini tidak akan berfungsi secara normal pada iPhone X dan lebih tinggi, karena ketika Anda menggerakkan jari Anda, Anda dapat sedikit menyentuh indikator yang lebih rendah, yang bertanggung jawab untuk meminimalkan aplikasi. Setelah itu, persembunyian interaktif berhenti bekerja.
Masalahnya terletak pada array windows.

Setelah gerakan, sistem membuat jendela keyboard baru di atas yang sudah ada. Itu tidak terpikirkan, tetapi benar. Akibatnya, ternyata array berisi dua jendela keyboard dengan koordinat yang sama, tetapi yang pertama disembunyikan.

Ternyata, dengan mengulangi susunan jendela, kami menemukan yang pertama yang memenuhi persyaratan, dan mulai bekerja dengannya, meskipun fakta bahwa itu tersembunyi.
Bagaimana ini diperbaiki? Mengubah array windows.
func panGeastureHandler( ) { let yPosition: CGFloat = 0.0 keyboardView( )?.frame.origin.y = yPosition } func keyboardView( ) -> UIView? { let windows = UIApplication.shared.windows.reversed( ) let view = windows.first { (window) -> Bool in return keyboardView( fromWindow: window) != nil } return view }
Fitur Keyboard di iPad
Keyboard di iPad, di samping kondisi biasa, memiliki status tidak terkunci. Pengguna dapat memindahkannya di sekitar layar, membaginya menjadi dua bagian dan bahkan meluncurkan aplikasi dalam mode slide over (di atas yang lain). Tentu saja, penting bahwa dalam semua mode ini keyboard bekerja tanpa bug.
Mari kita periksa Hayke kita.
Sayangnya, ini bukan masalahnya sekarang. Setelah pengguna mulai menggerakkan keyboard di sekitar layar, kaki Ike terbang di atas kepalanya dan muncul di tempatnya hanya setelah pembukaan keyboard berikutnya. Mari kita coba memperbaikinya pada kasing dengan keyboard terpisah.
Alasan
Mari kita mulai dengan menganalisis notifikasi. Setelah mengklik tombol split, kami mendapat dua grup pemberitahuan - keyboardWillChangeFrameNotification, keyboardWillHideNotification, keyboardDidChangeFrameNotification, keyboardDidHideNotification. Perbedaan antar grup hanya ada pada koordinat keyboard.
Ketika kami mengklik tombol split, keyboard berkurang dan grup pemberitahuan pertama tiba. Saat keyboard terbelah dan naik - kami mendapat paket pemberitahuan kedua.
Yang penting adalah kami menerima notifikasi bahwa keyboard telah menghilang, tetapi tidak ada yang ditampilkan. Omong-omong, ini merupakan nilai tambah lain dalam penggunaan keyboardWillChangeFrameNotification.
Lalu, mengapa kaki Ike terbang begitu kita mulai menggerakkan keyboard di sekitar layar?
Pada saat ini, sistem mengirimi kami keyboardWillChangeFrameNotification, tetapi koordinat yang ada (0,0, 0,0, 0,0, 0,0), karena sistem tidak tahu pada titik mana keyboard akan setelah gerakan selesai.
Jika kita mengganti nol dalam kode saat ini yang memproses perubahan bingkai keyboard, ternyata ketinggian keyboard sama dengan ketinggian jendela. Itulah alasan mengapa kaki Ike terbang dari layar.
Keputusan bagus
Untuk mengatasi masalah kami, pertama-tama kita akan belajar untuk memahami kapan keyboard dalam mode tidak terkunci dan pengguna dapat memindahkannya di sekitar layar.
Untuk melakukan ini, bandingkan saja ketinggian jendela dan keyboard maksimal. Jika mereka sama, maka keyboard dalam keadaan normal, jika maxY kurang dari ketinggian jendela, maka pengguna memindahkan keyboard. Akibatnya, kode berikut ini muncul di keyboardTracker:
class KeyboardTracker { func enable( ) { notificationCenter.addObserver( self, selector:#selector( keyboardWillChangeFrame), name:UIResponder.keyboardWillChangeFrameNotification, object:nil) } func keyboardWillChangeFrame( notification: NSNotification) { let screenCoordinatedKeyboardFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue let leyboardFrame = window.convert ( screenCoordinatedKeyboardFrame, from: nil) let windowHeight = window.frame.height let keyboardHeight = windowHeight - keyboardFrame.minY let isKeyboardUnlocked = isIPad ( ) && keyboardFrame/maxY < windowHeight if isKeyboardUnlocked { keyboardHeight = 0.0 } delegate.keyboardWillChange ( keyboardHeight) } }
Kami mengatur ketinggiannya menjadi nol, dan sekarang dengan gerakan keyboard, kaki Ike turun dan diperbaiki di sana.
Satu-satunya kesalahpahaman yang tersisa adalah fakta bahwa ketika membelah keyboard, kaki Ike tidak segera jatuh. Bagaimana cara memperbaikinya?
Kami akan mengajarkan keyboardTracker untuk bekerja tidak hanya dengan keyboardWillChangeFrameNotification, tetapi juga dengan keyboardDidChangeFrame. Anda tidak perlu menulis kode baru, cukup tambahkan centang bahwa ini adalah iPad agar tidak melakukan perhitungan yang tidak perlu.
class KeyboardTracker { func keyboardDidChangeFrame( notification: NSNotification) { if isIPad ( ) == false { return }
Bagaimana cara mendeteksi bug?
Penebangan yang melimpah
Pada proyek kami, log ditulis dalam format berikut: di kurung siku nama modul dan submodule, yang menjadi milik log, dan kemudian teks log itu sendiri. Misalnya, seperti ini:
[keyboard][tracker] keyboardWillChangeFrame: calculated height - 437.9
Dalam kode tersebut, tampilannya adalah sebagai berikut - logger dibuat dengan tag tingkat atas dan dikirimkan ke pelacak. Di dalam pelacak, logger dengan tag tingkat kedua, yang digunakan untuk masuk ke dalam kelas, dipisahkan dari logger.
class KeyboardTracker { init(with logger: Logger) { self.trackerLogger = logger.dequeue(withTag: "[tracker]") } func keyboardWillChangeFrame(notification: NSNotification) { let height = 0.0 trackerLogger.debug("\(#function): calculated height - \(height)") } }
Jadi saya berjanji seluruh keyboardTracker, yang bagus. Jika penguji menemukan masalah, saya mengambil file log dan mencari di mana frame tidak cocok. Ini terlalu banyak waktu, oleh karena itu, selain penebangan, metode lain mulai diterapkan.
Watchdog
Dalam proyek kami, Watchdog digunakan untuk mengoptimalkan aliran UI. Ini
diceritakan oleh Dmitry Kurkin di salah satu AppsConf sebelumnya .
Watchdog adalah proses atau utas yang
mengawasi proses atau utas lainnya. Mekanisme ini memungkinkan Anda untuk memantau status keyboard dan tampilan yang bergantung padanya dan melaporkan masalah.
Untuk mengimplementasikan fungsionalitas seperti itu, kami membuat penghitung waktu yang satu detik sekali akan memeriksa lokasi tampilan yang benar dengan kaki Hayk atau mencatat ini jika ada kesalahan.
class Watchdog { var timer: Timer? func start ( ) { timer = Timer ( timeInterval: 1.0, repeats: true, block: { ( timer ) in self.woof ( ) } ) } }
Omong-omong, Anda dapat mencatat tidak hanya hasil akhir, tetapi juga perhitungan menengah.
Sebagai hasilnya, pencatatan yang melimpah + Watchdog memberikan data akurat tentang masalah, keadaan keyboard, dan mengurangi waktu untuk memperbaiki bug, tetapi tidak banyak membantu pengguna beta yang harus menanggung kesalahan hingga rilis berikutnya.
Tetapi bagaimana jika anjing penjaga dapat dilatih tidak hanya untuk menemukan masalah, tetapi juga untuk memperbaikinya?
Dalam kode di mana pengawas memberikan kesimpulan bahwa koordinat tampilan tidak menyatu, kami menambahkan metode fixTummyPosition dan secara otomatis menempatkan koordinat di tempat.
Dalam opsi ini, banyak informasi berguna yang terakumulasi di log saya, dan pengguna tidak melihat masalah visual sama sekali. Ini sepertinya hebat, tapi sekarang saya tidak bisa mengetahui tentang masalah dengan keyboard.
Ini membantu untuk menambah metode pengawas kemampuan untuk menghasilkan cache tes ketika kesalahan terdeteksi. Tentu saja, kode ini ditambahkan di bawah konfigurasi remout.
Sekarang, setelah rilis berikutnya, Anda dapat mengaktifkan generasi uji crash dan, jika pengguna memiliki masalah dengan keyboard, aplikasinya crash dan berkat log yang dikumpulkan, Anda dapat memperbaiki bug.
Dasbor
Trik terakhir yang kami perkenalkan adalah mengirim statistik saat wahtchdog mencatat statistik. Berdasarkan data yang diperoleh, kami merencanakan jumlah kesalahan yang terdeteksi dan setelah iterasi pertama, jumlah operasi berkurang empat kali. Tentu saja, itu tidak mungkin untuk mengurangi masalah menjadi nol, tetapi keluhan utama dari pengguna berhenti.
Minggu depan Saint AppsConf akan diadakan di St. Petersburg, di mana Anda dapat mengajukan pertanyaan tidak hanya kepada Konstantin, tetapi juga kepada banyak pembicara dari jalur iOS.