Azimjon Abdurasulov

UIKit 1: View ierarxiyasi va koordinatalar — frame, bounds, hitTest va responder chain

📱 UIKit va SwiftUI — chuqur

UIKit 1: View ierarxiyasi va koordinatalar — frame, bounds, hitTest va responder chain

Holat: 2026-yil sentabr. iOS interfeysining poydevori UIView va CALayer ierarxiyasiga tayanadi. Ko'plab dasturchilar frame va bounds farqini yoki teginish (touch) hodisalari qanday tarqalishini yuzaki tushunadi. Ushbu darsda iOS 26.5 simulyatorida real o'lchangan faktlar asosida koordinatalar konvertatsiyasi, bounds.origin siljishi, hitTest ning 0.01 alpha qoidasi va Responder Chain arxitekturasi tahlil qilinadi.

Mundarija

  1. UIView va CALayer: arxitektura va mas'uliyat taqsimoti
  2. Frame vs Bounds vs Center vs Transform
  3. Transform ostidagi o'lchovlar (90 gradus burilish)
  4. Bounds.origin siljishi: UIScrollView qanday ishlaydi?
  5. Koordinatalar konvertatsiyasi (convert:to/from)
  6. Hit-Testing va point(inside:with:) qoidalari
  7. Tashqariga chiquvchi subview va clipsToBounds tuzog'i
  8. Responder Chain: hodisaning UIWindow va sahnagacha sayohati
  9. Xulosa
  10. Manbalar

1. UIView va CALayer: arxitektura va mas'uliyat taqsimoti {#uiview-calayer}

Har bir UIView orqasida bitta CoreAnimation qatlami — **CALayer** turadi. Ular orasidagi munosabat:

  • **UIView (UIResponder vorisi):** Foydalanuvchi teginishlarini (touch events), imo-ishoralarni (gestures) ushlaydi, accessibility va Auto Layout bilan ishlaydi.
  • **CALayer (NSObject vorisi):** Bevosita GPU render qilish, burchaklarni yumaloqlash (cornerRadius), soya (shadow), chegaralar (border) va apparat darajasidagi animatsiyalar uchun javobgar.

O'lchangan tizimli faktlar:

  • view.layer.delegate === view: **true** (har bir view o'z layer'ining rasmiy delegati hisoblanadi).
  • view.alpha = 0.5 o'rnatilganda, avtomatik ravishda layer.opacity = 0.5 ga aylanadi.
  • Standart holatda view.frame == layer.frame tengligi saqlanadi.

2. Frame vs Bounds vs Center vs Transform {#frame-bounds}

Bu to'rt xususiyat view geometriyasini to'liq belgilaydi:

XususiyatQaysi koordinata tizimida?Vazifasi
**frame**Superview (ota view) koordinatalaridaView'ning ota view ichidagi joylashuvi va uni to'liq o'rab turuvchi to'g'ri to'rtburchak (bounding box).
**bounds**Mahalliy (o'zining) koordinatalaridaView'ning o'z ichki maydoni. Odatda origin = (0, 0), size = (width, height).
**center**Superview (ota view) koordinatalaridaView'ning ota view ichidagi markaziy nuqtasi (frame.origin + frame.size / 2).
**transform**Affinely (CGAffineTransform)2D masshtablash, burish (rotation) va siljitish matritsasi.

Oltin qoida: Agar view'ga transform qo'llanilgan bo'lsa (transform != .identity), uning **frame xususiyatiga to'g'ridan-to'g'ri qiymat berish yoki unga ishonish xavflidir!** Bunday holatda faqat bounds va center bilan ishlash kerak.


3. Transform ostidagi o'lchovlar (90 gradus burilish) {#transform}

iOS 26.5 simulyatorida $100 \times 50$ o'lchamli view 90 gradusga burilganda o'lchangan haqiqiy natija:

import UIKit

@MainActor
func testTransform() {
  let view = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 50))
  view.transform = CGAffineTransform(rotationAngle: .pi / 2)

  // O'lchangan natija:
  // view.bounds = (0.0, 0.0, 100.0, 50.0)
  // view.frame  = (25.0, -25.0, 50.0, 100.0)
}

E'tibor bering:

  • View'ning o'z ichki bounds o'lchami o'zgarmasdan $100 \times 50$ ligicha qoldi.
  • Superview ko'radigan tashqi frame esa burilish natijasida $50 \times 100$ ga aylandi va markazni saqlab qolish uchun origin manfiy $(-25.0)$ ga surildi!

UIView Geometriyasi: Frame vs Bounds va Transform


4. Bounds.origin siljishi: UIScrollView qanday ishlaydi? {#bounds-origin}

Ko'pchilik dasturchilar bounds.origin doimo (0, 0) bo'ladi deb o'ylashadi. Biroq bounds.origin o'zgartirilsa, view'ning o'z ichki chizish kamerasi siljiydi!

Haqiqiy o'lchov tajribasi:

import UIKit

@MainActor
func testBoundsOriginShift() {
  let root = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
  let parent = UIView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
  let child = UIView(frame: CGRect(x: 10, y: 50, width: 40, height: 40))

  root.addSubview(parent)
  parent.addSubview(child)

  let before = child.convert(CGPoint.zero, to: root) // (10.0, 50.0)

  // Otasining bounds.origin.y sini 20 ga suramiz
  parent.bounds.origin = CGPoint(x: 0, y: 20)

  let after = child.convert(CGPoint.zero, to: root) // (10.0, 30.0)
}

O'lchangan fakt:

  • Bolaning child.frame xususiyati o'zgarmadi: hamon (10.0, 50.0, 40.0, 40.0).
  • Lekin ekranda (root'da) bola **20 nuqta yuqoriga surildi ((10.0, 30.0))**!
  • Aynan shu mexanizm orqali **UIScrollView** barcha bolalarining frame'ini o'zgartirmasdan, faqat o'zining bounds.originini o'zgartirish orqali millionlab elementlarni bir zumda scroll qiladi.

UIScrollView Bounds Origin Mexanizmi


5. Koordinatalar konvertatsiyasi (convert:to/from) {#coordinates}

Turli ierarxiyalarda joylashgan ikki view o'rtasidagi masofani hisoblashda qo'lda qo'shish-ayirish qat'iyan man etiladi:

import UIKit

@MainActor
func calculateAbsolutePosition(button: UIButton, in window: UIWindow) -> CGRect {
  // Tugmaning window ichidagi mutlaq koordinatasini topish:
  let globalFrame = button.convert(button.bounds, to: window)
  return globalFrame
}

6. Hit-Testing va point(inside:with:) qoidalari {#hit-test}

Foydalanuvchi ekranga tekkanda, iOS qaysi view teginishni qabul qilishini aniqlash uchun chuqurlik bo'yicha qidiruv (Reverse Pre-order Depth-First Search) o'tkazadi:

1. view.hitTest(point, with: event) chaqiriladi.
2. Agar quyidagilardan biri ro'y bersa, tekshirish darhol to'xtatiladi va nil qaytadi:
   - view.isHidden == true
   - view.isUserInteractionEnabled == false
   - view.alpha < 0.01  <-- DIQQAT!
   - !view.point(inside: point, with: event)
3. Aks holda, ota view o'zining barcha bolalarini oxirgi qo'shilganidan boshlab (eng yuqori qatlamdan) tekshiradi.

O'lchangan faktlar (iOS 26.5):

  • child.isHidden = true bo'lsa -> bola e'tiborsiz qoldiriladi (hit = parent).
  • child.isUserInteractionEnabled = false bo'lsa -> bola o'tkazib yuboriladi (hit = parent).
  • Alpha chegarasi:
  • child.alpha = 0.005 bo'lsa -> UIKit buni ko'rinmas deb hisoblaydi va teginishni bolaga bermaydi (hit = parent).
  • child.alpha = 0.02 bo'lsa -> teginish bolaga uzatiladi (hit = child).
  • Demak, UIKit'da teginish qabul qilish uchun minimal alpha ko'rsatkichi **0.01** hisoblanadi!

7. Tashqariga chiquvchi subview va clipsToBounds tuzog'i {#clip-trap}

iOS dasturchilarining 80% i ushbu xatoga yo'l qo'yadi:

Faraz qilaylik, $200 \times 200$ o'lchamli ota view bor. Unda clipsToBounds = false qilib, ichiga $100 \times 100$ o'lchamli tugma qo'yildi va u ota chegarasidan tashqariga chiqib turibdi (x: 220, y: 220).

Foydalanuvchi tashqariga chiqib turgan tugmaga bossa, nima bo'ladi?

  • Tugma vizual ko'rinib turadi.
  • Lekin bosilganda **hech narsa bo'lmaydi (hit = nil)**!

Nega? Chunki iOS avval ota view'ning point(inside:with:) metodini tekshiradi. Nuqta otaning (200, 200) chegarasidan tashqarida bo'lgani uchun ota false qaytaradi va UIKit uning bolalarini tekshirib ham o'tirmaydi!

Yechim: point(inside:with:) ni kengaytirish

import UIKit

public final class ExpandedHitAreaView: UIView {
  public var touchAreaPadding = UIEdgeInsets(top: -20, left: -20, bottom: -20, right: -20)

  public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
    let extendedBounds = bounds.inset(by: touchAreaPadding)
    return extendedBounds.contains(point)
  }
}

8. Responder Chain: hodisaning UIWindow va sahnagacha sayohati {#responder-chain}

hitTest teginilgan dastlabki ob'ektni (First Responder) topgandan so'ng, hodisa Responder Chain zanjiri bo'ylab yuqoriga uzatiladi.

iOS 26.5 da UIButton dan boshlangan real responder chain zanjiri:

UIButton
   |
UIView (tugma turgan ota ko'rinish)
   |
UIViewController (ekran boshqaruvchisi)
   |
UIDropShadowView / UITransitionView (tizimli konteynerlar)
   |
UIWindow (oyna)
   |
UIWindowScene (_UIScreenBasedWindowScene)
   |
UIApplication
   |
AppDelegate

Agar biron bir bo'g'in hodisani qayta ishlamasa (super.touchesBegan(...)), hodisa nextResponder orqali keyingisiga o'tadi. Hech kim ushlamasa, hodisa shunchaki bekor qilinadi.

Hit-Testing va Responder Chain Arxitekturasi


9. Xulosa {#xulosa}

  • UIView foydalanuvchi muloqoti (events) uchun, uning orqasidagi CALayer esa apparat renderi uchun javobgardir.
  • transform qo'llanilganda frame o'zgaradi, lekin bounds o'zgarmaydi; bunday paytda faqat bounds va center bilan ishlash shart.
  • UIScrollView o'z bolalarining joylashuvini emas, balki o'zining bounds.origin nuqtasini surish orqali scroll qiladi.
  • hitTest teginishni qabul qilishi uchun view'ning alpha ko'rsatkichi kamida 0.01 bo'lishi shart.
  • Ota view chegarasidan tashqariga chiqqan subview clipsToBounds = false bo'lsa ham, otaning point(inside:with:) kengaytirilmasa, teginishni qabul qilmaydi.
  • Responder Chain hodisalarni eng quyi view'dan to UIWindow, Scene va UIApplication'gacha uzatuvchi tizimdir.

Manbalar {#manbalar}

  1. View and Window Architecture — Apple Documentation
  2. UIView hitTest(_:with:) — Apple Documentation
  3. Understanding the Responder Chain — Apple Documentation
  4. Core Animation Basics — Apple Documentation Archive

Markdown manba

iOS Lessons