UIKit 8: iOS Muammolar va Ularning Aniq Yechimlari — Crash, Concurrency va Memory Buglar
📱 UIKit va SwiftUI — chuqur
UIKit 8: iOS Muammolar va Ularning Aniq Yechimlari — Crash, Concurrency va Memory Buglar
iOS dasturchi kundalik amaliyotida turli xil xatolar, qotishlar (crash) va noxush holatlarga duch keladi. Ushbu darsda ishlab chiqarish (production) muhitida eng ko'p uchraydigan 9 ta jiddiy muammo, ularning haqiqiy konsol xato matni (exact error text), kelib chiqish sababi va Swift 6 hamda zamonaviy UIKit standartlari asosidagi aniq yechimlari tahlil qilinadi.
1. Swift 6 Concurrency: Nonisolated kontekstdan @MainActor holatini o'zgartirish {#main-actor-mutation}
Muammo va Xato Matni
Swift 6 to'liq ma'lumotlar poygasi xavfsizligi (complete data-race safety) rejimida quyidagi kod kompilyatorda xatolik beradi:
error: main actor-isolated property 'title' can not be mutated from a nonisolated context
| `- note: mutation of this property is only permitted within the actor
| `- note: consider declaring an isolated method on 'MainActor' to perform the mutation
Yoki UIKit elementini fondagi GCD oqimidan o'zgartirishda:
warning: main actor-isolated property 'items' can not be mutated from a Sendable closure
Sabab
ProfileViewModel sinfi @MainActor bilan himoyalangan. Biroq Task.detached chaqirilganda, u joriy kontekst aktyoridan uzilib, umumiy fon oqimida (nonisolated context) ishga tushadi. Fon oqimidan turib bosh oqim (MainActor) mulkini to'g'ridan-to'g'ri o'zgartirish ma'lumotlar poygasiga (data race) olib keladi.
Aniq Yechim
Bosh oqim mulkini yangilash uchun MainActor.run orqali hop qilish yoki strukturaviy Task (u o'z ota kontekstining aktyor izolatsiyasini meros qilib oladi) ishlatiladi:
import UIKit
@MainActor
public final class ProfileViewModel {
public var title: String = ""
public func updateTitle(_ newTitle: String) {
self.title = newTitle
}
}
public func updateSafely(model: ProfileViewModel) async {
// Yechim 1: Strukturaviy Task ota aktyor izolatsiyasini saqlaydi
Task { @MainActor in
model.title = "Yangi sarlavha"
}
// Yechim 2: Detached task ichidan MainActor.run orqali o'tish
Task.detached {
let fetchedTitle = "Serverdan kelgan matn"
await MainActor.run {
model.updateTitle(fetchedTitle)
}
}
}
2. Swift 6 Concurrency: Non-Sendable turni chegaradan uzatish {#sending-risks-data-race}
Muammo va Xato Matni
Thread-safe bo'lmagan referens ob'ektini boshqa vazifaga (Task) uzatishda:
error: sending value of non-Sendable type '() async -> ()' risks causing data races [#SendingRisksDataRace]
note: Passing value of non-Sendable type '() async -> ()' as a 'sending' argument to static method 'detached(name:priority:operation:)' risks causing races in between local and caller code
note: access can happen concurrently
Sabab
Oddiy class ProfileLoader reference tur bo'lib, uning ichki holati oqimlar o'rtasida himoyalanmagan. Bir vaqtning o'zida ham chaqiruvchi oqimda, ham detached task ichida loader.name ga yozish xotirani buzadi.
Aniq Yechim
- Qiymat turlaridan (
struct) foydalanish — barcha maydonlari Sendable bo'lgan struct avtomatik Sendable hisoblanadi. - Agar sinf kerak bo'lsa, uni
actorga aylantirish yokifinal classqilib,Sendableprotokoliga moslash:
import Foundation
// Yechim A: Qiymat turi (Struct) — ma'lumotlar xavfsiz nusxalanadi
public struct UserProfileData: Sendable {
public var name: String
public init(name: String) { self.name = name }
}
// Yechim B: Actor — barcha kirish nuqtalari sinxronizatsiya qilinadi
public actor SafeProfileLoader {
private var name: String = ""
public func updateName(_ newName: String) {
self.name = newName
}
public func getName() -> String {
return self.name
}
}
public func processData() async {
let data = UserProfileData(name: "Azimjon")
Task.detached {
// Xavfsiz: UserProfileData Sendable
let localCopy = data
_ = localCopy.name
}
let actorLoader = SafeProfileLoader()
Task.detached {
await actorLoader.updateName("Toshkent")
}
}
3. Crash: Unowned ob'ekt xotiradan o'chgach unga murojaat qilish {#unowned-crash}
Muammo va Xato Matni
Ilova birdaniga qulab, konsolda quyidagi xatolik chiqadi:
Fatal error: Attempted to read an unowned reference but object 0x1052a5c90 was already destroyed
*(yoki Fatal error: Attempted to read an unowned reference but the object was already deallocated)*
Sabab
unowned ko'rsatkich ARC'ga quyidagi qat'iy va'dani beradi: "Ushbu ob'ekt mening butun hayot davrim davomida xotirada mavjud bo'ladi, unga havola hech qachon nil bo'lmaydi". Ammo agar referent ob'ekt kutilmaganda xotiradan o'chib ketsa (deallocated), unowned ko'rsatkich nollanmaydi (nil ga aylanmaydi). Uning maydoniga murojaat qilinganda Swift runtime unowned hisoblagichini tekshiradi va ob'ekt yo'q qilinganini ko'rib, dasturni ataylab to'xtatadi.
Aniq Yechim
Agar ikkita ob'ektning hayot davri (lifecycle) 100% bir vaqtda tugashiga to'liq kafolat bo'lmasa, har doim weak ishlatish kerak:
import Foundation
public final class Company {
public let name: String = "Tech Corp"
public init() {}
}
public final class Employee {
// XAVFLI: unowned let company: Company
// TO'G'RI: Zaif ko'rsatkich ob'ekt o'chganda avtomatik nil bo'ladi
public weak var company: Company?
public init(company: Company?) {
self.company = company
}
public func printCompany() {
guard let company = company else {
// Ob'ekt o'chirilgan, qulash bo'lmaydi
return
}
_ = company.name
}
}
4. Crash: Nil qiymatni majburiy ochish (Force Unwrap) {#force-unwrap-crash}
Muammo va Xato Matni
Ilova ishlab turgan joyida qulab tushadi:
Fatal error: Unexpectedly found nil while unwrapping an Optional value
Sabab
! operatori orqali Optional ichida qiymat bor deb kafolat beriladi, biroq ish vaqtida (runtime) uning qiymati nil bo'lib chiqadi. Eng keng tarqalgan sabablar:
@IBOutletelementigaviewDidLoad()dan oldin murojaat qilish;- Serverdan kelgan JSON modelidagi majburiy maydonning kutilmaganda kelmasligi;
Dictionaryindeksidan topilmagan qiymatnidict["kalit"]!orqali ochish.
Aniq Yechim
Majburiy ochish (!) o'rniga xavfsiz konstruksiyalar qo'llaniladi:
import Foundation
public struct UserResponse {
public let token: String?
}
public func handleLogin(response: UserResponse) -> String {
// 1. guard let orqali muddatidan oldin qaytish
guard let token = response.token, !token.isEmpty else {
return "Xato: Token mavjud emas"
}
// 2. Nil-coalescing (??) orqali zaxira qiymat berish
let sessionName = response.token ?? "Mehmon"
return "Muvaffaqiyatli: \(sessionName)"
}
5. Crash: Massiv chegarasidan tashqariga chiqish (Index out of range) {#index-out-of-range-crash}
Muammo va Xato Matni
Ilova to'satdan to'xtab, konsolda ushbu satr paydo bo'ladi:
Swift/ContiguousArrayBuffer.swift:692: Fatal error: Index out of range
Sabab
array[index] orqali murojaat qilinganda index < 0 yoki index >= array.count bo'lgan. Ayniqsa, UITableViewDataSource yoki UICollectionViewDataSourceda:
numberOfRowsInSectiondaitems.countqaytariladi (masalan 10 ta).- Qaysidir fon oqimi
itemsmassivini tozalab tashlaydi (items.removeAll()). - Asosiy oqim esa
cellForRowAtda 5-elementni chaqirganda massiv bo'sh bo'ladi va crash ro'y beradi!
Aniq Yechim
- Massivlar uchun xavfsiz subscript kengaytmasidan (safe subscript) foydalanish.
- Ma'lumotlarni o'zgartirish va UI yangilashni faqat
@MainActororqali sinxronlashtirish. - Yoki zamonaviy
UICollectionViewDiffableDataSourcearxitekturasiga o'tish (unda indeks xatolari umuman bo'lmaydi):
import Foundation
public extension Collection {
/// Chegaradan tashqariga chiqsa nil qaytaruvchi xavfsiz indeks
subscript(safe index: Index) -> Element? {
return indices.contains(index) ? self[index] : nil
}
}
public func readItem(at index: Int, from list: [String]) -> String {
if let item = list[safe: index] {
return item
}
return "Standart element"
}
6. Xotira Sizishi (Retain Cycle) va O'chmaydigan View Controller {#retain-cycles}
Muammo
Foydalanuvchi ekranni yopadi (dismiss yoki pop), lekin xotira kamaymaydi. Instruments Memory Graph yoki Leaks orqali tekshirilganda, yopilgan DetailViewController xotirada mangu qolib ketgani ko'rinadi. Uning deinit bloki hech qachon chaqirilmaydi.
Sabab
Quyidagi ikkita asosiy sabab kuchli bog'lanish siklini (strong reference cycle) hosil qiladi:
- Closure:
selfclosure'ni ushlaydi, closure esa o'z ichidaselfni kuchli havolada saqlaydi. - Delegate: Delegat protokoli
weakdeb belgilanmagan.
Aniq Yechim
Closure'larda doimo [weak self] capture list ishlatish, delegat ko'rsatkichlarini esa har doim weak qilish:
import UIKit
public protocol DetailViewDelegate: AnyObject {
func detailDidFinish()
}
public final class DetailViewController: UIViewController {
// Delegat har doim weak bo'lishi shart:
public weak var delegate: DetailViewDelegate?
public var onCompletion: (() -> Void)?
public override func viewDidLoad() {
super.viewDidLoad()
// RETENTION CYCLE TUZATILDI:
self.onCompletion = { [weak self] in
guard let self = self else { return }
self.view.backgroundColor = .systemBackground
}
}
deinit {
// Agar barcha bog'lanishlar to'g'ri bo'lsa, bu blok doim ishlaydi
}
}
7. Auto Layout: Bajarib bo'lmaydigan cheklovlar ziddiyati {#autolayout-conflicts}
Muammo va Xato Matni
Ilova interfeysi sekinlashadi va konsol qizil ogohlantirishlar bilan to'ladi:
Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want:
(
"<NSLayoutConstraint:0x600003b28140 UIView:0x10531c.width == 300>",
"<NSLayoutConstraint:0x600003b28190 UIView:0x10531c.width == 200>"
)
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x600003b28190 UIView:0x10531c.width == 200>
Sabab
View elementiga bir-biriga zid keluvchi cheklovlar berilgan (masalan, bir vaqtning o'zida kengligi 300 va 200 bo'lishi talab qilingan, yoki chap va o'ng chegaralar ota ekranning qat'iy o'lchamiga mos kelmaydi). Auto Layout dvigateli qaysidir cheklovni majburan buzish orqali ekranni chizishga harakat qiladi, bu esa CPU quvvatini yo'qotadi va kutilmagan sakrashlarni keltirib chiqaradi.
Aniq Yechim
- Cheklovlarga prioritet berish (
priority): Majburiy bo'lmagan cheklovni.defaultHigh(750) yoki999darajasiga tushirish. - Cheklovlarga
identifierberish — shunda konsolda0x60000...o'rniga aniq tushunarli nom chiqadi:
import UIKit
public func setupFlexibleCard(container: UIView, card: UIView) {
card.translatesAutoresizingMaskIntoConstraints = false
container.addSubview(card)
let widthConstraint = card.widthAnchor.constraint(equalToConstant: 300)
// Identifikator xatoni oson topishga yordam beradi:
widthConstraint.identifier = "Card_Preferred_Width"
// Prioritetni 999 qilish orqali ziddiyat paytida bu cheklov o'z o'rnini bo'shatadi:
widthConstraint.priority = UILayoutPriority(999)
NSLayoutConstraint.activate([
card.centerXAnchor.constraint(equalTo: container.centerXAnchor),
card.centerYAnchor.constraint(equalTo: container.centerYAnchor),
card.leadingAnchor.constraint(greaterThanOrEqualTo: container.leadingAnchor, constant: 16),
card.trailingAnchor.constraint(lessThanOrEqualTo: container.trailingAnchor, constant: -16),
widthConstraint
])
}
8. Main Thread Checker: UIKit'ni fon oqimidan chaqirish {#main-thread-checker}
Muammo va Xato Matni
Xcode orqali ishga tushirilganda, ilova to'xtab qoladi yoki konsolda shunday yozuv chiqadi:
=================================================================
Main Thread Checker: UI API called on a background thread: -[UIView setNeedsLayout]
PID: 28419, TID: 1049283, Thread name: (none), Queue name: com.apple.root.default-qos, QoS: 21
Backtrace:
4 UIKitCore 0x000000018591d2c0 -[UIView setNeedsLayout] + 68
5 MyApp 0x00000001004128a4 $s5MyApp14DataControllerC10fetchData...
Sabab
UIKit barcha vizual amallarni faqat va faqat bosh oqimda (Main Thread) bajarishi shart. Fondagi oqimdan turib UILabel.text, view.setNeedsLayout() yoki navigationController.pushViewController chaqirilsa, grafik dvigatel (Core Animation) poygaga duch keladi va chizish xatolari yuz beradi.
Aniq Yechim
- Zamonaviy Swift'da UI bilan ishlovchi barcha klasslarni
@MainActorbilan belgilash. - Agar eski asinxron callback'lardan foydalanilayotgan bo'lsa, UI amallarini
DispatchQueue.main.asyncyokiMainActor.runichiga o'rash:
import UIKit
public final class SafeNetworkService {
@MainActor
public static func updateLabelText(_ label: UILabel, with text: String) {
label.text = text
}
public func loadData(completion: @escaping @Sendable (String) -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
let result = "Server javobi"
// Xavfsiz qaytarish:
DispatchQueue.main.async {
completion(result)
}
}
}
}
9. TableView va CollectionView Cell Reuse Buglari (Ma'lumotlar oqishi) {#cell-reuse-bugs}
Muammo
Foydalanuvchi ro'yxatni pastga aylantirganda (scroll):
- 1-qatorda yuklangan rasm to'satdan 15-qatorda ko'rinib qoladi;
- Oldingi katakchaning yoqilgan
UISwitchholati yangi katakchada ham yoqiq turadi; - Matnlar va ranglar tartibsiz almashib ketadi.
Sabab
UIKit xotirani tejash maqsadida katakchalarni qayta ishlatadi (cell reuse). Yangi ma'lumot kelganda, o'tgan satrdan qolgan UI holati (masalan, yuklanayotgan asinxron rasm vazifasi) tozalanmagan bo'ladi. Asinxron yuklash tugagach, u allaqachon boshqa foydalanuvchiga tegishli bo'lgan yangi katakchaga o'z rasmini chizib yuboradi.
Aniq Yechim
- Katakcha ichidagi barcha o'tkinchi holatlarni
prepareForReuse()metodida tozalash. - Asinxron topshiriqlarni bekor qilish (
cancel()):
import UIKit
public final class ProductCell: UITableViewCell {
private var imageLoadingTask: Task<Void, Never>?
public let titleLabel = UILabel()
public let productImageView = UIImageView()
public override func prepareForReuse() {
super.prepareForReuse()
// 1. Matn va rasmni asl holatiga qaytarish:
titleLabel.text = nil
productImageView.image = nil
// 2. Davom etayotgan asinxron tarmoq yuklashini bekor qilish:
imageLoadingTask?.cancel()
imageLoadingTask = nil
}
public func configure(title: String, imageURL: URL?) {
titleLabel.text = title
guard let url = imageURL else { return }
imageLoadingTask = Task { [weak self] in
// Rasm yuklash simulyatsiyasi:
guard !Task.isCancelled else { return }
// Yuklab olingan rasmni tayinlash:
self?.productImageView.image = UIImage()
}
}
}
10. Virtual Klaviatura va Safe Area bilan To'qnashuv {#keyboard-layout-guide}
Muammo
Matn kiritish maydoni (UITextField) ekran pastida joylashgan bo'lsa, foydalanuvchi klaviaturani ochganda u maydonni to'liq to'sib qo'yadi.
Eski Usul va Yangi Yechim
Ilgari buning uchun UIResponder.keyboardWillShowNotification xabarnomasiga obuna bo'lish, klaviaturaning balandligini (UIResponder.keyboardFrameEndUserInfoKey) hisoblash va constraint'larni qo'lda o'zgartirish talab etilar edi.
iOS 15 dan boshlab kiritilgan **view.keyboardLayoutGuide** bu muammoni 1 qatorda hal qiladi:
import UIKit
public func setupInputBar(in viewController: UIViewController, inputBar: UIView) {
inputBar.translatesAutoresizingMaskIntoConstraints = false
viewController.view.addSubview(inputBar)
NSLayoutConstraint.activate([
inputBar.leadingAnchor.constraint(equalTo: viewController.view.leadingAnchor),
inputBar.trailingAnchor.constraint(equalTo: viewController.view.trailingAnchor),
inputBar.heightAnchor.constraint(equalToConstant: 50),
// Klaviatura ochilganda inputBar avtomatik yuqoriga suriladi:
inputBar.bottomAnchor.constraint(equalTo: viewController.view.keyboardLayoutGuide.topAnchor)
])
}
Xulosa va Nazorat Savollari
Ushbu muammolarni oldindan bilish va ularga qarshi to'g'ri arxitektura tanlash ilovaning barqarorligi (crash-free rate) 99.9% dan yuqori bo'lishini ta'minlaydi.
O'zingizni tekshiring:
- Nega
unownedo'rnigaweakishlatish xavfsizroq? Ularning xotiradagi ko'rsatkich hisoblagichi (reference count) qanday farqlanadi? keyboardLayoutGuidekiritilguniga qadar klaviatura qanday boshqarilar edi?- Katakcha qayta ishlatilganda (
prepareForReuse) nega asinxron vazifalarni bekor qilish (cancel()) shart? - Auto Layout cheklovlariga
identifierberishning nosozliklarni bartaraf etishdagi (debugging) foydasi nimada?