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
framevaboundsfarqini yoki teginish (touch) hodisalari qanday tarqalishini yuzaki tushunadi. Ushbu darsda iOS 26.5 simulyatorida real o'lchangan faktlar asosida koordinatalar konvertatsiyasi,bounds.originsiljishi,hitTestning 0.01 alpha qoidasi va Responder Chain arxitekturasi tahlil qilinadi.
Mundarija
- UIView va CALayer: arxitektura va mas'uliyat taqsimoti
- Frame vs Bounds vs Center vs Transform
- Transform ostidagi o'lchovlar (90 gradus burilish)
- Bounds.origin siljishi: UIScrollView qanday ishlaydi?
- Koordinatalar konvertatsiyasi (convert:to/from)
- Hit-Testing va point(inside:with:) qoidalari
- Tashqariga chiquvchi subview va clipsToBounds tuzog'i
- Responder Chain: hodisaning UIWindow va sahnagacha sayohati
- Xulosa
- 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.5o'rnatilganda, avtomatik ravishdalayer.opacity = 0.5ga aylanadi.- Standart holatda
view.frame == layer.frametengligi saqlanadi.
2. Frame vs Bounds vs Center vs Transform {#frame-bounds}
Bu to'rt xususiyat view geometriyasini to'liq belgilaydi:
| Xususiyat | Qaysi koordinata tizimida? | Vazifasi |
|---|---|---|
**frame** | Superview (ota view) koordinatalarida | View'ning ota view ichidagi joylashuvi va uni to'liq o'rab turuvchi to'g'ri to'rtburchak (bounding box). |
**bounds** | Mahalliy (o'zining) koordinatalarida | View'ning o'z ichki maydoni. Odatda origin = (0, 0), size = (width, height). |
**center** | Superview (ota view) koordinatalarida | View'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
transformqo'llanilgan bo'lsa (transform != .identity), uning **framexususiyatiga to'g'ridan-to'g'ri qiymat berish yoki unga ishonish xavflidir!** Bunday holatda faqatboundsvacenterbilan 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
boundso'lchami o'zgarmasdan $100 \times 50$ ligicha qoldi. - Superview ko'radigan tashqi
frameesa burilish natijasida $50 \times 100$ ga aylandi va markazni saqlab qolish uchunoriginmanfiy $(-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.framexususiyati 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'ziningbounds.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 = truebo'lsa -> bola e'tiborsiz qoldiriladi (hit = parent).child.isUserInteractionEnabled = falsebo'lsa -> bola o'tkazib yuboriladi (hit = parent).- Alpha chegarasi:
child.alpha = 0.005bo'lsa -> UIKit buni ko'rinmas deb hisoblaydi va teginishni bolaga bermaydi (hit = parent).child.alpha = 0.02bo'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}
UIViewfoydalanuvchi muloqoti (events) uchun, uning orqasidagiCALayeresa apparat renderi uchun javobgardir.transformqo'llanilgandaframeo'zgaradi, lekinboundso'zgarmaydi; bunday paytda faqatboundsvacenterbilan ishlash shart.UIScrollViewo'z bolalarining joylashuvini emas, balki o'ziningbounds.originnuqtasini surish orqali scroll qiladi.hitTestteginishni qabul qilishi uchun view'ningalphako'rsatkichi kamida0.01bo'lishi shart.- Ota view chegarasidan tashqariga chiqqan subview
clipsToBounds = falsebo'lsa ham, otaningpoint(inside:with:)kengaytirilmasa, teginishni qabul qilmaydi. - Responder Chain hodisalarni eng quyi view'dan to UIWindow, Scene va UIApplication'gacha uzatuvchi tizimdir.