Menguasai Awal dan Akhir Objek Swift Kamu

Menguasai Awal dan Akhir Objek Swift Kamu
Photo by Markus Spiske/Unsplash

Menguasai siklus hidup objek di Swift itu kayak lo lagi ngerakit komputer sendiri, gengs. Lo harus tahu betul kapan komponennya dipasang (inisialisasi), gimana memastikan semua terpasang dengan benar, dan kapan komponen itu "dicopot" atau dibersihkan kalau udah nggak dipakai lagi (deinisialisasi). Kalau lo nggak paham ini, bisa-bisa komputer lo nggak nyala atau malah nge-hang. Nah, di Swift, objek yang nggak diurus dengan baik bisa bikin aplikasi lo boros memori atau bahkan crash.

Jadi, siap-siap ya, karena di artikel ini kita bakal ngulik tuntas gimana cara menguasai awal dan akhir objek Swift lo. Ini fundamental banget biar kode lo nggak cuma jalan, tapi juga efisien, stabil, dan bisa diandalkan. Yuk, kita mulai petualangan kita!

Awal Mula Objek: Inisialisasi (Siap-siap Lahir!)

Setiap kali lo bikin objek baru dari class atau struct, proses yang namanya "inisialisasi" itu langsung jalan. Ibaratnya, ini kayak momen kelahirannya si objek. Tujuan utama inisialisasi adalah memastikan semua properti yang disimpan di objek itu punya nilai awal yang valid sebelum objek dipakai. Swift itu ketat banget soal ini: properti yang disimpan harus punya nilai awal.

Konsep Dasar init()

Di Swift, lo pake metode khusus yang namanya init() buat inisialisasi. Ini bukan fungsi biasa yang lo panggil pakai nama, tapi dia otomatis dipanggil pas lo bikin instance baru.

Misalnya, lo punya struct buat mewakili seorang PemainGame:

swift
struct PemainGame {
    let nama: String
    var level: Int
    var skor: Int// Ini adalah memberwise initializer otomatis untuk struct
    // Tapi kita bisa bikin kustom init juga
    init(nama: String, level: Int = 1, skor: Int = 0) {
        self.nama = nama
        self.level = level
        self.skor = skor
        print("Pemain \(nama) baru saja bergabung!")
    }
}// Cara pakainya:
let pemain1 = PemainGame(nama: "Budi") // level dan skor pakai nilai default
print("Pemain 1: \(pemain1.nama), Level \(pemain1.level), Skor \(pemain1.skor)")

Perhatikan di contoh di atas, kita bisa kasih nilai default buat parameter di init(). Ini bikin inisialisasi jadi lebih fleksibel. self.nama, self.level, dan self.skor itu artinya properti dari instance yang lagi dibikin.

Inisialisasi Properti yang Disimpan

Ada beberapa cara properti lo bisa dapet nilai awal:

  1. Kasih Nilai Default: Cara paling simpel, langsung kasih nilai di deklarasi properti.
swift
    struct Kotak {
        var panjang: Double = 10.0
        var lebar: Double = 5.0
    }
    let kotakKu = Kotak() // panjang dan lebar otomatis dapet nilai default
  1. init() Kustom: Ini yang paling sering lo lakuin. Lo bikin init() sendiri dan kasih nilai ke semua properti di dalamnya.
swift
    class Produk {
        let nama: String
        let harga: Double
  1. Optional Properties: Kalo properti itu boleh kosong (nil) pas awal, lo bisa deklarasiin sebagai Optional.
swift
    class User {
        let username: String
        var email: String? // Boleh nil di awal

Tipe-Tipe Inisialisasi Lanjutan (Khusus class)

Di class, inisialisasi itu agak lebih kompleks karena ada pewarisan. Ada dua tipe utama init buat class:

  1. Designated Initializers: Ini init "utama" atau "penuh" buat sebuah class. Dia harus menginisialisasi semua properti yang dideklarasiin di class itu, dan juga manggil designated init dari superclass-nya (kalau ada). Biasanya, class cuma punya satu atau beberapa designated initializers.
swift
    class Kendaraan {
        var jumlahRoda: Int
        var kecepatanMaks: Doubleinit(jumlahRoda: Int, kecepatanMaks: Double) {
            self.jumlahRoda = jumlahRoda
            self.kecepatanMaks = kecepatanMaks
            print("Kendaraan baru dibuat.")
        }
    }class Mobil: Kendaraan {
        var merek: String// Designated initializer untuk Mobil
        init(merek: String, jumlahRoda: Int, kecepatanMaks: Double) {
            self.merek = merek // Inisialisasi properti sendiri
            super.init(jumlahRoda: jumlahRoda, kecepatanMaks: kecepatanMaks) // Panggil designated init superclass
            print("Mobil merek \(merek) dibuat.")
        }
    }

Aturan mainnya: designated init harus mendelegasikan ke designated init superclass-nya. Ini namanya "delegasi ke atas".

  1. Convenience Initializers: Ini init "pembantu" yang tujuannya bikin inisialisasi jadi lebih gampang atau lebih singkat. convenience init harus memanggil init lain dari class yang sama (bisa designated init atau convenience init lain). Mereka nggak bisa langsung manggil init superclass.
swift
    class Warna {
        var red: Double
        var green: Double
        var blue: Double// Designated Initializer
        init(red: Double, green: Double, blue: Double) {
            self.red = red
            self.green = green
            self.blue = blue
        }// Convenience Initializer untuk warna grayscale
        convenience init(white: Double) {
            self.init(red: white, green: white, blue: white) // Panggil designated init yang sama
        }// Convenience Initializer untuk warna standar
        convenience init(namaWarna: String) {
            switch namaWarna.lowercased() {
            case "merah":
                self.init(red: 1.0, green: 0.0, blue: 0.0)
            case "biru":
                self.init(red: 0.0, green: 0.0, blue: 1.0)
            default:
                self.init(red: 0.0, green: 0.0, blue: 0.0) // Hitam default
            }
        }
    }

Aturan mainnya: convenience init harus mendelegasikan ke init lain dari class yang sama (baik designated atau convenience lain), sampai akhirnya ada designated init yang terpanggil. Ini namanya "delegasi menyamping".

Delegasi Inisialisasi: Aturan emasnya:

  • Designated init harus memanggil designated init dari superclass-nya.
  • Convenience init harus memanggil init lain dari class yang sama.
  • Convenience init akhirnya harus memanggil designated init (bisa langsung atau lewat convenience init lain).

Failable Initializers (init?)

Kadang, inisialisasi sebuah objek bisa aja gagal. Misalnya, lo mau bikin objek KartuKredit tapi nomor kartunya nggak valid. Di sini, failable initializer sangat berguna. Dia mengembalikan Optional dari tipe objek lo (nil kalau gagal).

swift
struct KartuKredit {
    let nomor: String
    let pemilik: Stringinit?(nomor: String, pemilik: String) {
        // Cek validitas nomor kartu
        guard nomor.count == 16 && nomor.allSatisfy({ $0.isNumber }) else {
            return nil // Gagal kalau nomor gak 16 digit angka semua
        }
        self.nomor = nomor
        self.pemilik = pemilik
    }
}let kartu1 = KartuKredit(nomor: "1234567890123456", pemilik: "Ali")
if let kartu = kartu1 {
    print("Kartu Ali berhasil dibuat: \(kartu.nomor)")
} else {
    print("Gagal membuat kartu Ali.")
}

Required Initializers (required init)

Kalau lo mau memastikan subclass lo wajib mengimplementasikan init tertentu, lo bisa tandai init itu dengan kata kunci required.

swift
class Bentuk {
    var nama: String
    required init(nama: String) {
        self.nama = nama
    }
}class Lingkaran: Bentuk {
    var radius: Double// Wajib mengimplementasikan required init dari superclass
    required init(nama: String) {
        self.radius = 0.0 // Inisialisasi properti sendiri
        super.init(nama: nama)
    }

Property Observers (willSet dan didSet)

Meskipun bukan bagian dari inisialisasi itu sendiri, willSet dan didSet itu penting buat dipahami karena mereka aktif setelah properti lo selesai diinisialisasi dan siap dipakai. Mereka "mengamati" perubahan nilai properti.

  • willSet: Dipanggil sesaat sebelum nilai properti diubah. Lo bisa akses nilai baru yang bakal disetel pake nama newValue (default).
  • didSet: Dipanggil segera setelah nilai properti diubah. Lo bisa akses nilai lama sebelum diubah pake nama oldValue (default).

swift
class Lampu {
    var statusNyala: Bool = false {
        willSet(newStatus) {
            print("Lampu akan diubah ke status: \(newStatus)")
        }
        didSet(oldStatus) {
            if statusNyala {
                print("Lampu dinyalakan! Status sebelumnya: \(oldStatus)")
            } else {
                print("Lampu dimatikan. Status sebelumnya: \(oldStatus)")
            }
        }
    }init(statusNyala: Bool) {
        self.statusNyala = statusNyala
    }
}

Penting diingat, willSet dan didSet itu nggak dipanggil saat properti diinisialisasi pertama kali. Mereka baru aktif setelah properti punya nilai awal dan ada perubahan berikutnya.

Akhir dari Objek: Deinisialisasi (Selamat Tinggal!)

Kalau inisialisasi itu momen kelahiran, deinisialisasi itu momen ketika objek lo "meninggal" atau dihapus dari memori. Proses ini penting banget buat memastikan sumber daya yang dipakai objek (kayak file yang dibuka, koneksi jaringan, atau observasi notifikasi) dibersihin dengan rapi sebelum objek beneran hilang.

Konsep Dasar deinit()

Sama kayak init(), deinit() juga metode khusus. Bedanya, deinit() cuma bisa ada di class, dan dia otomatis dipanggil persis sebelum instance dari class itu dihapus dari memori.

swift
class FileHandleManager {
    let namaFile: String
    // Anggap ini representasi file yang dibuka
    var fileDibuka: Bool = falseinit(namaFile: String) {
        self.namaFile = namaFile
        // Logika untuk membuka file
        fileDibuka = true
        print("File \(namaFile) berhasil dibuka.")
    }deinit {
        // Logika untuk menutup file
        if fileDibuka {
            print("Menutup file \(namaFile)..")
            fileDibuka = false
        }
        print("FileHandleManager untuk \(namaFile) dihapus dari memori.")
    }func tulisKeFile(data: String) {
        if fileDibuka {
            print("Menulis '\(data)' ke \(namaFile)")
        } else {
            print("File \(namaFile) tidak terbuka.")
        }
    }
}

Di contoh di atas, ketika manager disetel jadi nil, objek FileHandleManager nggak lagi punya "pemilik" (reference). Swift lalu tahu kalau objek itu udah nggak dibutuhkan dan bakal memanggil deinit()-nya sebelum membersihkan memori.

Kenapa struct nggak punya deinit()? Karena struct itu value type. Artinya, ketika lo bikin salinannya, lo bikin salinan data yang terpisah. Mereka nggak saling mereferensikan. Jadi, Swift nggak perlu mekanisme khusus buat ngurus struct dari memori kayak class yang reference type.

ARC (Automatic Reference Counting): Penjaga Memori Swift

Swift itu pintar dalam mengelola memori. Dia punya sistem yang namanya ARC (Automatic Reference Counting). Sesuai namanya, ARC otomatis menghitung berapa banyak "rujukan kuat" (strong reference) ke sebuah instance class. Selama ada setidaknya satu strong reference ke sebuah objek, ARC nggak akan menghapusnya dari memori. Begitu jumlah strong reference jadi nol, ARC akan memicu deinit() objek itu dan membersihkan memorinya.

ARC ini bikin kita jarang banget perlu ngurus memori secara manual, beda sama bahasa C/C++ yang harus malloc dan free sendiri. Tapi, ada satu jebakan yang sering bikin pusing: strong reference cycles.

Masalah Klasik: Strong Reference Cycles (Siklus Referensi Kuat)

Strong reference cycle terjadi ketika dua (atau lebih) objek class saling memiliki strong reference satu sama lain. Akibatnya, reference count mereka nggak pernah jadi nol, meskipun mereka udah nggak dipakai lagi di aplikasi lo. Objek-objek ini bakal "bertahan hidup" di memori selamanya, bikin aplikasi lo boros memori (ini yang disebut memory leak).

Yuk, kita lihat contohnya:

swift
class Person {
    let name: String
    var apartment: Apartment? // Strong reference ke Apartmentinit(name: String) {
        self.name = name
        print("\(name) is being initialized")
    }deinit {
        print("\(name) is being deinitialized")
    }
}class Apartment {
    let unit: String
    var tenant: Person? // Strong reference ke Personinit(unit: String) {
        self.unit = unit
        print("Apartment \(unit) is being initialized")
    }deinit {
        print("Apartment \(unit) is being deinitialized")
    }
}var john: Person?
var unit4A: Apartment?john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")john?.apartment = unit4A // John punya apartment 4A
unit4A?.tenant = john // Apartment 4A punya tenant Johnjohn = nil // Reference count John masih 1 (dari unit4A)
unit4A = nil // Reference count unit4A masih 1 (dari John)

Di contoh di atas, john punya strong reference ke unit4A, dan unit4A punya strong reference ke john. Meskipun kita set john = nil dan unit4A = nil, mereka tetap nggak bisa dihapus dari memori karena saling "mengunci". Ini adalah memory leak klasik.

Solusi Strong Reference Cycles: weak dan unowned References

Swift menyediakan dua kata kunci buat mengatasi strong reference cycles:

  1. weak References (weak var):

* Tidak menambah reference count. * Selalu bersifat Optional: Karena referensi weak bisa jadi nil kapan aja kalau objek yang dia rujuk dihapus duluan. Jadi, lo harus selalu nanganinnya sebagai Optional dan ngecek nil. * Kapan pakai? Biasanya dipakai saat ada hubungan "parent-child" atau "delegate", di mana objek child/delegate nggak harus mempertahankan parent/delegate-nya tetap hidup. Objek parent itu pemiliknya, dan child itu yang direferensikan secara weak.

Mari kita perbaiki contoh Person dan Apartment:

swift
    class Person {
        let name: String
        var apartment: Apartment?init(name: String) {
            self.name = name
            print("\(name) is being initialized")
        }deinit {
            print("\(name) is being deinitialized")
        }
    }class Apartment {
        let unit: String
        weak var tenant: Person? // Ini dia! Weak referenceinit(unit: String) {
            self.unit = unit
            print("Apartment \(unit) is being initialized")
        }deinit {
            print("Apartment \(unit) is being deinitialized")
        }
    }var john: Person?
    var unit4A: Apartment?john = Person(name: "John Appleseed") // John reference count: 1
    unit4A = Apartment(unit: "4A") // unit4A reference count: 1john?.apartment = unit4A // unit4A reference count: 2 (dari john & global var)
    unit4A?.tenant = john // Ini weak, jadi John reference count tetap 1john = nil // John reference count jadi 0, deinit John terpanggil
    unit4A = nil // unit4A reference count jadi 0, deinit unit4A terpanggil

Nah, sekarang nggak ada lagi memory leak!

  1. unowned References (unowned var):

* Tidak menambah reference count. Tidak bersifat Optional: Berarti lo harus yakin 100% kalau objek yang direferensikan unowned ini akan selalu ada selama unowned reference-nya itu ada. Kalau sampai objek yang dirujuknya tiba-tiba dihapus dan lo coba akses unowned reference itu, aplikasi lo bakal crash*. * Kapan pakai? Dipakai ketika kedua objek saling mereferensikan, tapi salah satu objek punya siklus hidup yang sama atau lebih panjang daripada objek yang lain, atau ketika objek itu nggak bisa hidup tanpa objek yang dirujuknya. Contohnya, hubungan Customer dan CreditCard. CreditCard nggak mungkin ada tanpa Customer, dan Customer bisa nggak punya CreditCard. Jadi CreditCard bisa punya unowned reference ke Customer karena Customer dijamin ada selama CreditCard itu ada.

swift
    class Customer {
        let name: String
        var card: CreditCard?init(name: String) {
            self.name = name
            print("Customer \(name) is initialized")
        }deinit {
            print("Customer \(name) is deinitialized")
        }
    }class CreditCard {
        let number: Int
        unowned let customer: Customer // Ini dia! Unowned referenceinit(number: Int, customer: Customer) {
            self.number = number
            self.customer = customer
            print("CreditCard #\(number) is initialized")
        }deinit {
            print("CreditCard #\(number) is deinitialized")
        }
    }var john: Customer?
    john = Customer(name: "John Appleseed") // Customer reference count: 1
    john!.card = CreditCard(number: 123456789012_3456, customer: john!) // Card reference count: 1. Customer reference count tetap 1.john = nil // Customer reference count jadi 0, Customer di-deinit. Card juga di-deinit.

Dalam kasus ini, CreditCard nggak bisa ada tanpa Customer, jadi customer properti di CreditCard aman pakai unowned karena Customer dijamin ada selama CreditCard itu ada.

Capture Lists untuk Closures

Closures juga bisa bikin strong reference cycles. Kalau closure lo meng-capture (mengambil) instance self secara kuat, dan instance self itu juga punya strong reference ke closure-nya, maka terjadilah siklus. Ini sering terjadi di delegate pattern atau blok completion handler.

Buat ngatasinnya, kita pakai capture list di awal closure: [weak self] atau [unowned self].

swift
class HTMLElement {
    let name: String
    let text: String?// Lazy var biar closure-nya baru dibikin pas diakses pertama kali
    lazy var asHTML: () -> String = {
        // [weak self] atau [unowned self] ditaruh di sini
        [unowned self] in // Pakai unowned karena HTMLElement & closure ini punya masa hidup yang sama
        if let text = self.text {
            return "<\(self.name)>\(text)"
        } else {
            return "<\(self.name) />"
        }
    }init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
        print("\(name) is being initialized")
    }deinit {
        print("\(name) is being deinitialized")
    }
}var paragraph: HTMLElement? = HTMLElement(name: "p", text: "Hello, world")
print(paragraph!.asHTML())
  • [weak self]: Pakai kalau self mungkin jadi nil di masa depan (misal, self bisa dihapus sebelum closure selesai dijalankan). Lo harus nanganin self sebagai Optional di dalam closure.
  • [unowned self]: Pakai kalau lo yakin self akan selalu ada selama closure itu ada. Kalau self dihapus duluan, aplikasi lo bakal crash.

Tips Praktis Biar Makin Jago

  1. Selalu Inisialisasi Properti: Pastikan semua properti yang disimpan di class atau struct lo punya nilai awal yang valid. Swift akan marah kalau lo lupa.
  2. Pahami Delegasi Inisialisasi (class): Ingat aturan "up, across": designated init delegasi ke atas (super-nya), convenience init delegasi menyamping (di class yang sama) dan akhirnya harus manggil designated init.
  3. Hati-hati dengan init?: Failable initializers itu kuat, tapi pastikan lo selalu nanganin hasil Optional-nya.
  4. lazy var vs. init: Kalau properti itu mahal buat diinisialisasi dan cuma perlu pas pertama kali diakses, pake lazy var.
  5. Gunakan weak dan unowned dengan Bijak: Ini kuncinya buat mencegah memory leak. Pahami bedanya dan kapan harus pakai yang mana.

* weak: Objek yang direferensikan bisa jadi nil (misal: delegate, objek yang siklus hidupnya lebih pendek). * unowned: Objek yang direferensikan dijamin ada selama referensi ini ada (misal: properti yang wajib ada di objek yang mereferensikannya).

  1. deinit untuk Cleanup: Gunakan deinit buat membersihkan sumber daya eksternal (menutup file, membatalkan observasi, memutuskan koneksi). Jangan over-use deinit, biarkan ARC melakukan sebagian besar pekerjaan.
  2. Testing Memori: Gunakan Xcode Instruments (terutama "Leaks" dan "Allocations") buat ngecek aplikasi lo ada memory leak atau nggak. Ini penting banget buat aplikasi yang stabil.

Penutup

Memahami siklus hidup objek di Swift itu bukan cuma soal ngoding, tapi juga soal bikin aplikasi yang sehat dan stabil. Dari inisialisasi yang memastikan setiap properti punya nilai awal, sampai deinisialisasi yang membersihkan memori dengan rapi, setiap tahapan itu krusial.

Dengan menguasai init, deinit, dan terutama cara kerja ARC dengan weak dan unowned references, lo bakal bisa bikin kode Swift yang lebih kuat, lebih efisien, dan bebas dari jebakan memory leak. Ini adalah salah satu fondasi terpenting dalam pengembangan aplikasi iOS yang sukses. Terus belajar dan eksplorasi ya, gengs! Semangat ngodingnya!