TekinAli ⎇ main

UIKit ve SwiftUI'da text field'ları klavyenin üstünde tutmak

Yazılım klavyesinin layout'la ilişkisi ve bugünün araçları: UIKit'te UIKeyboardLayoutGuide, SwiftUI'da klavye safe area'sı; focus ve klavyeyi kapatmak için @FocusState, onSubmit ve scrollDismissesKeyboard.

Bir uygulamayı en hızlı bozuk gösteren şeylerden biri, yazdığınız alanın üstünü kapatan bir klavye. Altta "Giriş yap" butonu olan bir login formu, bir sohbet giriş çubuğu, uzun bir kayıt formu: klavye açıldığı anda ekranın bir kısmı arkasında kayboluyor. iPad'de yüzen klavye ise her seferinde ekranın farklı bir yerini kapatabiliyor.

Bugün hem UIKit'in hem SwiftUI'ın bunun için yerleşik araçları var ve bunlar elle yazılan birçok kodun yerini alıyor. Bu yazıda klavyenin layout'la ilişkisini, UIKeyboardLayoutGuide'ın ve SwiftUI'ın klavye safe area'sının nasıl çalıştığını, focus ve klavyeyi kapatma işini ve hâlâ sık düşülen tuzakları anlatıyorum.

Klavye layout'u nasıl etkiliyor

Yazılım klavyesi sizin view hiyerarşinizin parçası değil. Pencerenin üzerine kayarak geliyor ve ekranın alt kısmında bir text field ya da "Devam" butonu varsa üstünü kapatıyor. Sistem ona nasıl yapacağını söylemediğiniz sürece layout'unuzu değiştirmiyor.

iOS 15'ten önce bunu söylemek hep aynı ritüel demekti: keyboardWillShow / keyboardWillHide (ya da daha iyisi keyboardWillChangeFrameNotification) dinlenir, userInfo içinden bitiş frame'i okunur, view'un koordinat sistemine çevrilir, safe area düşülür, sonra ya bir constraint'in constant'ı değiştirilir ya da scroll view'un contentInset'i ayarlanır. İdeal olanı bunu klavyenin animasyon eğrisiyle aynı anda yapmaktır. Bu sırada en az bir detayı atlamak çok kolay: iPad'deki yüzen klavye, cihaz döndürme ya da observer'ı kaldırmak.

iOS 14 ve iOS 15 ile ne değişti

iOS 15'ten beri her UIView'un klavyeyi sizin yerinize takip eden bir keyboardLayoutGuide'ı (UIKeyboardLayoutGuide) var. iOS 14'ten beri de SwiftUI klavyeyi bir safe area bölgesi olarak ele alıyor (SafeAreaRegions.keyboard), yani layout sistemi klavyeden zaten kaçınıyor. Deployment target'ınız iOS 15 veya üstüyse (2026'da büyük ihtimalle öyle) notification tabanlı yaklaşım artık legacy kod.

UIKit: UIKeyboardLayoutGuide

Fikir basit: guide'ın üst kenarı klavyenin üst kenarı. Klavye kapalıyken safe area'nın altında duruyor. İçeriğinizi ona bağlarsanız gerisini, animasyon dahil, Auto Layout hallediyor.

import UIKit

final class LoginViewController: UIViewController {
    private let emailField = UITextField()
    private let loginButton = UIButton(configuration: .filled()) // iOS 15+

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        emailField.borderStyle = .roundedRect
        emailField.placeholder = "E-posta"
        loginButton.configuration?.title = "Giriş yap"

        let stack = UIStackView(arrangedSubviews: [emailField, loginButton])
        stack.axis = .vertical
        stack.spacing = 12
        stack.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(stack)

        NSLayoutConstraint.activate([
            stack.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor),
            stack.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor),
            // Bütün numara bu: klavyenin (ya da safe area'nın) 16 pt üstünde dur.
            stack.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor, constant: -16)
        ])

        // iPad: yüzen ya da ayrılmış klavyeyi de takip et (iOS 15+).
        view.keyboardLayoutGuide.followsUndockedKeyboard = true
    }
}

Bilmeye değer iki property var:

  • followsUndockedKeyboard (iOS 15+) varsayılan olarak false. Bu durumda guide yalnızca sabit (docked) klavyeyi takip ediyor. iPad'de yüzen ya da ayrılmış klavyeyi de takip etmesini istiyorsanız true yapın.
  • usesBottomSafeArea (iOS 17+), klavye kapalıyken guide'ın alt safe area'ya uyup uymayacağını belirliyor. Bir giriş çubuğunun ekranın en altına yapışmasını istiyorsanız false yapın.

Scroll view'lar: frame'i küçült ya da inset'i ayarla

Formlar genelde bir scroll view içinde olur. İki yaklaşım var, varsayılan olarak ilkini öneriyorum.

Scroll view'u guide'a bağlamak. Scroll view'un frame'i klavyenin başladığı yerde biter, yani her alana kaydırarak hâlâ ulaşılabilir. Düzenlenen alanın kendiliğinden görünür olmasını istiyorsanız scrollRectToVisible'ı kendiniz çağırın:

final class FormViewController: UIViewController, UITextFieldDelegate {
    private let scrollView = UIScrollView()

    override func viewDidLoad() {
        super.viewDidLoad()
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        scrollView.keyboardDismissMode = .interactive // aşağı kaydırınca kapanır
        view.addSubview(scrollView)

        NSLayoutConstraint.activate([
            scrollView.topAnchor.constraint(equalTo: view.topAnchor),
            scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            scrollView.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor)
        ])
    }

    func textFieldDidBeginEditing(_ textField: UITextField) {
        let rect = textField.convert(textField.bounds, to: scrollView)
        scrollView.scrollRectToVisible(rect.insetBy(dx: 0, dy: -16), animated: true)
    }
}

contentInset'i ayarlamak. Klasik yöntem bu. İçeriğin klavyenin arkasında da çizilmeye devam etmesi gerektiğinde, örneğin yarı saydam bir sohbet arka planında, hâlâ doğru seçim bu. Ayrı show/hide notification'ları yerine keyboardWillChangeFrameNotification kullanın ve inset ile birlikte verticalScrollIndicatorInsets'i (iOS 11.1+) de güncelleyin:

@objc private func keyboardWillChangeFrame(_ note: Notification) {
    guard let endFrame = note.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
    let frameInView = view.convert(endFrame, from: view.window)
    let overlap = max(0, view.bounds.maxY - frameInView.minY - view.safeAreaInsets.bottom)
    scrollView.contentInset.bottom = overlap
    scrollView.verticalScrollIndicatorInsets.bottom = overlap
}

Inset yaklaşımı için özel bir nedeniniz yoksa guide'a bağlayın. Daha az kod ve daha fazla uç durumu kendisi hallediyor.

SwiftUI: klavye safe area'sı ve focus

iOS 14 ve sonrasında SwiftUI, klavye bir safe area bölgesi olduğu için içeriği zaten klavyenin yolundan çekiyor. iOS 13 için yazılmış özel "alanı yukarı taşı" kodları güncel sistemlerde framework'le çatışmaya eğilimli.

SwiftUI'da asıl ihtiyacınız olan şey focus yönetimi. Aşağıda eksiksiz bir kayıt formu var:

import SwiftUI

struct SignUpForm: View {
    enum Field: Hashable { case name, email, password }

    @State private var name = ""
    @State private var email = ""
    @State private var password = ""
    @FocusState private var focusedField: Field?          // iOS 15+

    var body: some View {
        NavigationStack {                                  // iOS 16+
            ScrollView {
                VStack(spacing: 16) {
                    TextField("Ad Soyad", text: $name)
                        .focused($focusedField, equals: .name)
                        .submitLabel(.next)                // iOS 15+

                    TextField("E-posta", text: $email)
                        .keyboardType(.emailAddress)
                        .textInputAutocapitalization(.never)
                        .focused($focusedField, equals: .email)
                        .submitLabel(.next)

                    SecureField("Parola", text: $password)
                        .focused($focusedField, equals: .password)
                        .submitLabel(.join)
                }
                .textFieldStyle(.roundedBorder)
                .padding()
            }
            .scrollDismissesKeyboard(.interactively)       // iOS 16+
            .onSubmit {                                    // iOS 15+
                switch focusedField {
                case .name: focusedField = .email
                case .email: focusedField = .password
                default:
                    focusedField = nil                     // klavyeyi kapatır
                    register()
                }
            }
            .toolbar {
                ToolbarItemGroup(placement: .keyboard) {   // iOS 15+
                    Spacer()
                    Button("Bitti") { focusedField = nil }
                }
            }
            .navigationTitle("Kayıt")
        }
    }

    private func register() { /* ... */ }
}

Birkaç not:

  • Klavyeyi kapatmak artık sadece focusedField = nil. Eski SwiftUI örneklerinde sık görülen, UIApplication.shared'a resignFirstResponder gönderme numarasına gerek kalmadı.
  • .onSubmit bağlandığı view'un içindeki text field'lar için çalışıyor, yani tek bir handler bütün formu yönetebiliyor.
  • .scrollDismissesKeyboard .immediately ve .never değerlerini de alıyor. Mesajlar uygulamasındaki hissi veren .interactively.

Hareket etmesin: ignoresSafeArea(.keyboard)

Bazen bir şeylerin hareket etmemesini istersiniz: tam ekran bir arka plan görseli ya da klavyenin alt kısmı düz bir şekilde örtmesi gereken bir layout. Bu view'lara .ignoresSafeArea(.keyboard) (iOS 14+) uygulayın:

ZStack {
    Image("background")
        .resizable()
        .scaledToFill()
        .ignoresSafeArea(.keyboard)   // arka plan yerinde kalır
    VStack {
        Spacer()
        TextField("Mesaj", text: $draft)
            .padding()
    }
}

Sık görülen tuzaklar

  • Yaklaşımları karıştırmak. keyboardLayoutGuide'a bağlı bir constraint artı hâlâ contentInset değiştiren eski bir notification handler, içeriği iki kez kaydırır. Geçiş yaparken eski observer'ı silin.
  • Constraint çakışmaları. Bir view hem klavye guide'ına hem de sabit bir üst constraint'e bağlıysa, klavye açıldığında birinin esnemesi gerekir. Birinin önceliğini düşürün ya da yüksekliği bir scroll view'un emmesine izin verin.
  • Modifier yanlış view'da. .ignoresSafeArea(.keyboard)'u nereye bağladığınız önemli. Derinlerdeki bir alt view'a değil, layout'u değişmemesi gereken container'a koyun ve sonucu cihazda kontrol edin.
  • iPad. Yüzen klavyeyi ve donanım klavyesini test edin. Donanım klavyesiyle yalnızca kısayol çubuğunu görebilirsiniz ve elle yazılmış frame hesabı bu durumda kolayca yanlış sonuç verebilir.
  • Eski SwiftUI geçici çözümleri. GeometryReader artı klavye publisher'ları ya da SwiftUI'ın ilk dönemi için yazılmış bir "adaptive keyboard" modifier'ı, iOS 14 ve sonrasındaki yerleşik davranışla genelde çatışır. Başka bir şeyi debug etmeden önce bunları kaldırın.

Kontrol listesi

  1. UIKit, iOS 15+: içeriği ya da scroll view'un altını view.keyboardLayoutGuide.topAnchor'a bağlayın.
  2. iPad'de yüzen klavye sizin için önemliyse followsUndockedKeyboard = true yapın. iOS 17+ için usesBottomSafeArea'ya bakın.
  3. contentInset ve keyboardWillChangeFrameNotification ikilisini yalnızca içeriğin klavyenin arkasında görünmeye devam etmesi gerekiyorsa kullanın.
  4. SwiftUI, iOS 14+: klavye safe area'sına güvenin, gereken yerde .ignoresSafeArea(.keyboard) ile dışarıda kalın.
  5. Alanlar arası geçiş ve klavyeyi kapatmak için @FocusState ile .submitLabel / .onSubmit (iOS 15+) kullanın.
  6. Tap gesture numaraları yerine .scrollDismissesKeyboard(.interactively) (iOS 16+) kullanın.
  7. Geçiş yaparken eski klavye notification kodunu silin.

Eskiden elle yazılan klavye kodunun büyük kısmı artık sistemin bir özelliği. Yeni kodda genelde tek bir constraint ya da tek bir @FocusState işi görüyor.

tekinali.db · salt okunur · ● bağlı

SQLite 3.53.3 · 8 tablo, 2 view.

Soldaki bir tabloya tıkla, aşağıdan bir sorgu seç ya da kendin yaz. Yardım: \?

↑↓ seç · Enter aç · Esc kapat