() 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
, metodeinit(coder:)
memanggil metodeinit(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
, metodeinit
adalah convenience initializer dan mendelegasikan inisialisasi ke initializer lainnya, misalnyainit(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.