Azimjon Abdurasulov

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

  1. Qiymat turlaridan (struct) foydalanish — barcha maydonlari Sendable bo'lgan struct avtomatik Sendable hisoblanadi.
  2. Agar sinf kerak bo'lsa, uni actor ga aylantirish yoki final class qilib, Sendable protokoliga 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:

  • @IBOutlet elementiga viewDidLoad() dan oldin murojaat qilish;
  • Serverdan kelgan JSON modelidagi majburiy maydonning kutilmaganda kelmasligi;
  • Dictionary indeksidan topilmagan qiymatni dict["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:

  1. numberOfRowsInSection da items.count qaytariladi (masalan 10 ta).
  2. Qaysidir fon oqimi items massivini tozalab tashlaydi (items.removeAll()).
  3. Asosiy oqim esa cellForRowAt da 5-elementni chaqirganda massiv bo'sh bo'ladi va crash ro'y beradi!

Aniq Yechim

  1. Massivlar uchun xavfsiz subscript kengaytmasidan (safe subscript) foydalanish.
  2. Ma'lumotlarni o'zgartirish va UI yangilashni faqat @MainActor orqali sinxronlashtirish.
  3. Yoki zamonaviy UICollectionViewDiffableDataSource arxitekturasiga 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:

  1. Closure: self closure'ni ushlaydi, closure esa o'z ichida selfni kuchli havolada saqlaydi.
  2. Delegate: Delegat protokoli weak deb 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

  1. Cheklovlarga prioritet berish (priority): Majburiy bo'lmagan cheklovni .defaultHigh (750) yoki 999 darajasiga tushirish.
  2. Cheklovlarga identifier berish — shunda konsolda 0x60000... 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

  1. Zamonaviy Swift'da UI bilan ishlovchi barcha klasslarni @MainActor bilan belgilash.
  2. Agar eski asinxron callback'lardan foydalanilayotgan bo'lsa, UI amallarini DispatchQueue.main.async yoki MainActor.run ichiga 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 UISwitch holati 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

  1. Katakcha ichidagi barcha o'tkinchi holatlarni prepareForReuse() metodida tozalash.
  2. 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:

  1. Nega unowned o'rniga weak ishlatish xavfsizroq? Ularning xotiradagi ko'rsatkich hisoblagichi (reference count) qanday farqlanadi?
  2. keyboardLayoutGuide kiritilguniga qadar klaviatura qanday boshqarilar edi?
  3. Katakcha qayta ishlatilganda (prepareForReuse) nega asinxron vazifalarni bekor qilish (cancel()) shart?
  4. Auto Layout cheklovlariga identifier berishning nosozliklarni bartaraf etishdagi (debugging) foydasi nimada?

Foydali Havolalar

Markdown manba

iOS Lessons