Advertisement
  1. Code
  2. Swift

Swift dari Nol: Inisialisasi dan Delegasi Initializer

Scroll to top
Read Time: 11 min
This post is part of a series called Swift From Scratch.
Swift From Scratch: Access Control and Property Observers

() translation by (you can also view the original English article)

Dalam bagian sebelumnya Belajar Swift dari Nol, kita membuat suatu aplikasi daftar hal yang harus dilakukan secara fungsional. Namun demikian ada cinta yang bisa digunakan model datanya. Dalam tutorial ini kita akan melakukan refaktor model data dengan mengimplementasikan kelas model sesuaian (custom).

1. Model Data

Model data yang akan diimplementasikan mencakup dua kelas, suatu kelas Task dan kelas Todo yang mewarisi dari kelas Task. Sembari membuat dan mengimplementasikan kelas-kelas model ini, kita lanjut mengeksplorasi pemrograman berorientasi objek di Swift. Dalam tutorial ini, kita akan membahas tentang inisialisasi intance kelas dan peran apa yang dimainkan oleh pewarisan (inheritance) selama inisialisasi.

Kelas Task

Mari kita mulai implementasi kelas Task. Buat file Swift baru dengan memilih New > File... dari menu File Xcode. Pilih Swift File dari bagian iOS > Source. Beri nama filenya Task.swift dan tekan Create.

Implementasi dasarnya singkat dan sederhana. Kelas Task mewarisi dari NSObject, didefinisikan dalam framework Foundation, dan memiliki properti variabel name dari tipe String. Kelas tersebut mendefinisikan dua initializer, init dan init(name:). Ada sedikit perincian yang mungkin menghambat Anda, jadi akan saya jelaskan apa yang terjadi.

1
import Foundation
2
3
class Task: NSObject {
4
    var name: String
5
    
6
   convenience override init() {
7
        self.init(name: "New Task")
8
    }
9
    
10
    init(name: String) {
11
        self.name = name
12
    }
13
}

Karena metode init juga didefinisikan dalam kelas NSObject, kita harus menambahkan awalan (prefiks) initializer dengan kata kunci override. Kita membahas metode override di bagian seri ini yang lebih awal. Dalam metode init, kita memanggil metode init(name:), memasukkannya dalam "New Task" sebagai nilai untuk parameter name.

Metode init(name:) adalah initializer lain, yang menerima parameter tunggal name yang bertipe String. Dalam initializer ini, nilai parameter name diberikan ke properti name. Ini cukup mudah dipahami. Betul?

Initializer Designated dan Convenience

Ada apa dengan kata kunci convenience yang menjadi awalan metode init? Kelas bisa memiliki dua tipe initializer, yakni initializer designated dan initializer convenience. Initializer convenience memilliki prefiks kata kunci convenience, yang mengimplikasikan bahwa init(name:) adalah designated initializer. Mengapa demikian? Apa perbedaan antara initializer designated dan convenience?

Designated initializer sepenuhnya menginisialisasi intance suatu kelas, artinya setiap properti instance memiliki nilai awal setelah inisialisasi. Dengan melihat kelas Task sebagai contoh, kita melihat properti name diatur dengan nilai untuk parameter name dari initializer init(name:). Hasilnya setelah inisialisasi adalah instance Task yang sepenuhnya diinisialisasi.

Namun demikian, Convenience initializers bergantung pada designated initializer untuk memuat instance kelas yang sepenuhnya diinisialisasi. Itulah mengapa initializer init dari kelas Task memanggil initializer init(name:) dalam implementasinya. Ini disebut sebagai delegasi initializer. Initializer init mendelegasikan inisialisasi ke initializer yang ditetapkan (designated initializer) untuk membuat instance kelas Task yang sepenuhnya diinisialisasi.

Initializer convenience bersifat opsional. Tidak semua kelas memiliki convenience initializer. Designated initializer sendiri harus ada dan suatu kelas membutuhkan setidaknya satu designated initializer untuk sepenuhnya menginisialisasi instance dengan sendirinya.

Protokol NSCoding

Implementasi kelas Task belum cukup lengkap. Selanjutnya dalam artikel ini, kita akan menulis array instance ToDo ke cakram. Ini hanya mungkin jika instance kelas ToDo bisa disandikan (encoded) dan dibuka sandinya (decoded).

Tetapi jangan khawatir, ini bukan ilmu roket. Kita hanya butuh membuat kelas Task dan ToDo untuk menyesuaikan dengan protokol NSCoding. Itulah mengapa kelas Task mewarisi dari kelas NSObject saja karena protokol NSCoding hanya bisa diimplementasikan dengan kelas yang mewarisi—secara langsung atau tidak langsung—dari NSObject. Seperti kelas NSObject, protokol NSCoding didefinisikan dalam framework Foundation.

Mengadopsi protokol adalah sesuatu yang telah kita bahas dalam seri ini, tetapi ada beberapa gotcha yang ingin saya tegaskan. Mari memulai dengan memberitahu compiler bahwa kelas Task sesuai dengan protokol NSCoding.

1
class Task: NSObject, NSCoding {
2
    var name: String
3
    
4
    ...
5
}

Berikutnya, kita butuh untuk mengimplementasikan dua metode yang dideklarasikan dalam protokol NSCoding, init(coder:) dan encodeWithCoder(_:). Implementasinya langsung ke tujuan jika Anda akrab dengan protokol NSCoding.

1
import Foundation
2
3
class Task: NSObject, NSCoding {
4
    var name: String
5
    
6
    @objc required init(coder aDecoder: NSCoder) {
7
        name = aDecoder.decodeObjectForKey("name") as! String
8
    }
9
    
10
    @objc func encodeWithCoder(aCoder: NSCoder) {
11
        aCoder.encodeObject(name, forKey: "name")
12
    }
13
    
14
    convenience override init() {
15
        self.init(name: "New Task")
16
    }
17
    
18
    init(name: String) {
19
        self.name = name
20
    }
21
}

Initializer init(coder:) adalah designated initializer yang menginisialisasi instance Task. Meskipun kita mengimplementasikan metode init(coder:) untuk menyesuaikan dengan protokol NSCoding, Anda tidak akan butuh lagi untuk memanggil metode ini secara langsung. Hal yang sama juga berlaku untuk encodeWithCoder(_:), yang menyandikan instance kelas Task.

Kata kunci required yang menjadi prefiks metode init(coder:) mengindikasikan bahwa tiap subkelas dari kelas Task harus mengimplementasikan metode ini. Kata kunci yang required hanya berlaku untuk initializer, itulah mengapa kita tidak perlu menambahkannya ke metode encodeWithCoder(_:).

Sebelum kita lanjut, perlu dibahas tentang atribut @objc. Karena protokol NSCoding adalah protokol Objective-C, protocol conformance hanya bisa dicek dengan menambahkan atribut @objc. Di Swift, tidak ada yang namanya kesesuaian protokol atau metode protokol pilihan. Dengan kata lain, jika suatu kelas melekat ke protokol tertentu, compilernya memverifikasi dan mengekspektasikan bahwa tiap metode protokolnya diimplementasikan.

Kelas ToDo

Dengan diimplementasikannya kelas Task, tiba saatnya mengimplementasikan kelas ToDo. Buat file Swift baru dan beri nama ToDo.swift. Mari kita lihat implementasi kelas ToDo.

1
import Foundation
2
3
class ToDo: Task {
4
    var done: Bool
5
    
6
    @objc required init(coder aDecoder: NSCoder) {
7
        self.done = aDecoder.decodeObjectForKey("done") as! Bool
8
        super.init(coder: aDecoder)
9
    }
10
    
11
    @objc override func encodeWithCoder(aCoder: NSCoder) {
12
        aCoder.encodeObject(done, forKey: "done")
13
        super.encodeWithCoder(aCoder)
14
    }
15
    
16
    init(name: String, done: Bool) {
17
        self.done = done
18
        super.init(name: name)
19
    }
20
}

Kelas ToDo mewarisi dari kelas Task dan mendeklarasikan properti variabel done dari tipe Bool. Sebagai tambahan terhadap dua metode yang disyaratkan protokol NSCoding yang diwarisinya dari kelas Task, dideklarasikan juga suatu designated initializer init(name:done:).

Sebagaimana di Objective-C, kata kunci super merujuk pada superkelasnya, dalam contoh ini adalah kelas Task. Ada satu perincian penting yang butuh perhatian. Sebelum Anda memanggil metode init(name:) di superkelas, tiap properti yang dideklarasikan oleh kelas ToDo harus diinisialisasi. Dengan kata lain, sebelum kelas ToDo mendelegasikan inisialisasi ke superkelasnya, tiap properti yang dideklarasikan kelas ToDo, harus memiliki nilai awal yang valid. Anda bisa memverifikasi ini dengan mengganti urutan statement dan menginspeksi kesalahan yang muncul.

Hal yang sama berlaku untuk metode init(coder:). Mula-mula kita menginisialisasi properti done sebelum memanggin init(coder:) pada superkelasnya. Perhatikan juga bahwa kita men-downcast dan memaksa unwrap hasil decodeObjectForKey(_:) ke suatu Bool dengan menggunakan as!.

Initializer dan Inheritance (Pewarisan)

Ketika berurusan dengan pewarisan dan inisialisasi, ada beberapa aturan yang perlu untuk dicamkan dalam pikiran. Aturan untuk designated initializer cukup sederhana.

  • Designated initializer harus memanggil suatu designated initializer dari superkelasnya. Misalnya dalam kelas ToDo, metode init(coder:) memanggil metode init(coder:) dari superkelasnya. Ini juga disebut sebagai delegating up.

Aturan untuk convenience initializer sedikit lebih rumit. Ada dua aturan yang harus dicamkan dalam pikiran.

  • Convenience initializer selalu butuh untuk memanggil initializer lain kelas yang mendefinisikannya. Misalnya dalam kelas Task, metode init adalah convenience initializer dan mendelegasikan inisialisasi ke initializer lainnya, misalnya init(name:). Ini disebut sebagai delegating across.
  • Meskipun suatu convenience initializer tidak harus mendelagasikan inisialisasi ke suatu designated initializer, convenience initializer harus memanggil designated initializer pada titik tertentu. Hal ini penting supaya menginisialisasi sepenuhnya instance yang sedang diinisialisasi.

Dengan kedua kelas model yang diimplementasikan, sekarang tiba saatnya refaktor kelas ViewController dan AddItemViewController. Mari mulai dengan yang belakang.

2. Me-Refaktor AddItemViewController

Langkah 1: Memperbarui Protokol AddItemViewControllerDelegate

Perubahan yang perlu dilakukan di kelas AddItemViewController hanyalah yang berhubungan dengan protokol AddItemViewControllerDelegate. Dalam deklarasi protokol, ubah tipe didAddItem dari String ke ToDo, kelas model yang kita implementasikan sebelumnya.

1
protocol AddItemViewControllerDelegate {
2
    func controller(controller: AddItemViewController, didAddItem: ToDo)
3
}

Langkah 2: Perbarui Action create

Ini berarti kita juga harus memperbarui action create yang di situ kita memanggil metode delegasi. Dalam impelentasi yang diperbarui, kita membuat instance ToDo dan memasukkannya ke metode delegasi.

1
@IBAction func create(sender: AnyObject) {
2
    let name = self.textField.text
3
    
4
    let item = ToDo(name: name, done: false)
5
    
6
    if let delegate = self.delegate {
7
        delegate.controller(self, didAddItem: item)
8
    }
9
}

3. Me-Refaktor ViewController

Langkah 1: Memperbarui Properti items

Kelas ViewController membutuhkan kerja yang sedikit lebih banyak. Pertama kita harus mengubah tipe properti items ke [ToDo], suatu array instance ToDo.

1
var items: [ToDo] = [] {
2
    didSet {
3
        let hasItems = items.count > 0
4
        self.tableView.hidden = !hasItems
5
        self.messageLabel.hidden = hasItems
6
    }
7
}

Langkah 2: Metode Sumber Table View Data

Hal ini juga berarti kita harus merefaktor sejumlah metode lainnya, seperti metode cellForRowAtIndexPath(_:) yang ditunjukkan di bawah ini. Karena array items sekarang berisi beberapa instance ToDo, memeriksa apakah suatu item sudah ditandai sebagai done akan jauh lebih mudah. Kita menggunakan operator Swift ternary conditional untuk memperbarui tipe aksoris tampilan sel tabel.

1
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
2
    // Fetch Item

3
    let item = self.items[indexPath.row]
4
    
5
    // Dequeue Table View Cell

6
    let tableViewCell = tableView.dequeueReusableCellWithIdentifier("TableViewCell", forIndexPath: indexPath) as! UITableViewCell
7
    
8
    // Configure Table View Cell

9
    tableViewCell.textLabel?.text = item.name
10
    tableViewCell.accessoryType = item.done ? .Checkmark : .None
11
    
12
    return tableViewCell
13
}

Ketika pengguna menghapus suatu item, kita hanya perlu memperbarui properti items dengan menghapus instance ToDo yang berkaitan. Hal tersebut direfleksikan dalam implementasi metode tableView(_:commitEditingStyle:forRowAtIndexPath:) yang ditunjukkan di bawah ini.

1
func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
2
    if editingStyle == .Delete {
3
        // Fetch Item

4
        let item = self.items[indexPath.row]
5
        
6
        // Update Items

7
        self.items.removeAtIndex(indexPath.row)
8
        
9
        // Save State

10
        self.saveItems()
11
        
12
        // Update Table View

13
        tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Right)
14
    }
15
}

Langkah 3: Metode Delegasi Table View

Memperbarui keadaan suatu item ketika pengguna menyentuh baris tertentu ditangani dalam metode tableView(_:didSelectRowAtIndexPath:). Implementasi metode UITableViewDelegate ini jauh lebih mudah berkat kelas ToDo.

1
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
2
    tableView.deselectRowAtIndexPath(indexPath, animated: true)
3
    
4
    // Fetch Item

5
    let item = self.items[indexPath.row]
6
    
7
    // Fetch Table View Cell

8
    let tableViewCell = tableView.cellForRowAtIndexPath(indexPath)
9
    
10
    // Update Item

11
    item.done = !item.done
12
    
13
    // Update Table View Cell

14
    tableViewCell?.accessoryType = item.done ? .Checkmark : .None
15
    
16
    // Save State

17
    self.saveItems()
18
}

Instance ToDo yang berkaitan akan diperbarui dan perubahannya direfleksikan dengan tampilan tabel (table view). Untuk menyimpan state tersebut, kita memanggil saveItems dan bukannya saveCheckedItems.

Langkah 4: Tambahkan Metode Delegasi Add Item View Controller

Karena kita memperbarui protokol AddItemViewControllerDelegate, kita juga harus memperbarui implementasi ViewController protokol ini. Meskipun demikian perubahannya sederhana saja. Kita hanya perlu memperbarui signature metodenya.

1
func controller(controller: AddItemViewController, didAddItem: ToDo) {
2
    // Update Data Source

3
    self.items.append(didAddItem)
4
    
5
    // Save State

6
    self.saveItems()
7
    
8
    // Reload Table View

9
    self.tableView.reloadData()
10
    
11
    // Dismiss Add Item View Controller

12
    self.dismissViewControllerAnimated(true, completion: nil)
13
}

Langkah 5: Menyimpan Item-Itemnya

pathForItems

Alih-alih menyimpan item-item dalam database default pengguna, kita akan menyimannya dalam direktori dokumen aplikasinya. Sebelum memperbarui metode loadItems dan saveItems, kita akan mengimplementasikan metode pembantu yang dinamai pathForItems. Metode ini bersifat privasi dan akan mengembalikan suatu path, lokasi item dalam direktori dokumen.

1
private func pathForItems() -> String {
2
    let documentsDirectory = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true).first as! String
3
    return documentsDirectory.stringByAppendingPathComponent("items")
4
}

Pertama kita mengambil direktori dokumen dalam kotak pasir aplikasi dengan memanggil NSSearchPathForDirectoriesInDomains(_:_:_:). Karena metode ini mengembalikan sekelompok string, kita ambil item pertama, force unwrap, dan downcast ke suatu String. Nilai yang kita kembalikan untuk pathForItems disusun oleh path ke direktori dokumen dengan tambahan string "items".

loadItems

Metode loadItems mengubah sedikit. Pertama kita menyimpan hasil pathForItems dalam konstanta yang dinamai path. Lalu kita membuka arsip objek yang diarsipkan di path tersebut dan men-downcast-nya ke suatu array opsional instance ToDo. Kita menggunakan optional binding untuk membuka opsionalnya dan memberikannya ke konstanta yang bernama items. Dalam klausa if, kita menetapkan nilai yang disimpan dalam items ke properti items.

1
private func loadItems() {
2
    let path = self.pathForItems()
3
    
4
    if let items = NSKeyedUnarchiver.unarchiveObjectWithFile(path) as? [ToDo] {
5
        self.items = items
6
    }
7
}

saveItems

Metode saveItems ini singkat dan sederhana. Kita menyimpan hasil pathForItems dalam suatu konstanta yang dinamai path dan memanggil archiveRootObject(_:toFile:) pada NSKeyedArchiver, memasukkan properti items dan path. Kita menampilkan hasil operasinya ke konsol.

1
private func saveItems() {
2
    let path = self.pathForItems()
3
    
4
    if NSKeyedArchiver.archiveRootObject(self.items, toFile: path) {
5
        println("Successfully Saved")
6
    } else {
7
        println("Saving Failed")
8
    }
9
}

Langkah 6: Bersih-Bersih

Mari mulai dengan bagian yang menyenangkan, menghapus kode. Mulailah dengan menghapus properti checkedItems di bagian teratas karena tak lagi dibutuhkan. Sebagai hasilnya, kita juga bisa menghapus metode loadCheckedItems dan saveCheckedItems, serta setiap referensi ke metode ini dalam kelas ViewController.

Buat dan jalankan aplikasinya untuk melihat semuanya masih bekerja dengan baik. Model data membuat kode aplikasi jauh lebih mudah dan bisa diandalkan. Berkat kelas ToDo, mengelola item dalam daftar kita sekarang jauh lebih mudah dan lebih tidak rentan pada kesalahan.

Kesimpulan

Dalam tutorial ini kita merefaktor model data aplikasi kita. Anda belajar lebih banyak tentang pemrograman berorientasi objek dan pewarisan (inheritance). Inisialisasi instance adalah konsep penting dalam Swift, jadi pastikan Anda memahami apa yang dibahas dalam tutorial ini. Anda bisa membaca lebih banyak tentang inisialisasi dan delegasi initializer dalam The Swift Programming Language.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.