1. 加上 Table View
開啟新專案 TodoExample
選 Tabbed Application,把 View 都設定好
請照著影片上的流程做
影片一
2. Install Cocoapods & RealmSwift
Cocoapods 是幫我們管理 App 套件的程式,安裝好後就可以很方便的幫 App 加上各種 Plugin,在我們的 Todo App 中因為需要有可以存資料的地方,所以我們要安裝 RealmSwift
這個套件,安裝方式就是在 Cocoapods 產生的 Podfile
裡面加上 pod 'RealmSwift'
然後在 Command Line 打上 pod install
就可以了。
安裝 Cocoapods:
待補
CocoaPods
安裝好 Cocoapods 後要讓 Cocoapods 管理這個專案,首先打開 Command Line 進到目前的資料夾:
wayne@Waynes-MacBook-Pro ~/Downloads/TodoExample $
$ pod init
# Uncomment this line to define a global platform for your project
# platform :ios, '8.0'
# Uncomment this line if you're using Swift
# use_frameworks!
target 'TodoExample' do
use_frameworks!
pod 'RealmSwift'
end
$ pod install
把 Xcode 關掉,改成開 TodoExample.xcworkspace
而不要開 TodoExample.xcodeproj
,因為只有 workspace 的檔案會幫你載入你想要的 framework, 在此例就是幫你載入 Realm 的 framework.
3. 定義 Model
建立一個新的檔案叫做 Task.swift
import RealmSwift
class Task: Object {
dynamic var name = ""
dynamic var isCompleted = false
}
4. 讓 Table View 顯示東西,並且可以新增 Task
記得剛剛我們有兩個 Tabs 嗎? 現在我們要加上一些功能
- 點擊新增的按鈕可以跳出新增 Task 的 Popup 頁面,並可以新增 Task
- 讓 Table 可以顯示所有未完成的 Tasks
(1) 點擊新增的按鈕可以跳出新增 Task 的 Popup 頁面,並可以新增 Task
首先我們先看第一個 Tab,也就是 Todo List
的 Tab, 這個 Tab 對應的檔案 Xcode 已經幫我們產生了,打開 FirstViewController.swift
會看到現在長這樣:
import UIKit
class FirstViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
@IBAction func addNewItem(sender: AnyObject) {
}
}
記得我們在 Step1 新增 View 的時候有把新增按鈕拖動到這個檔案裡面嗎?就是下面這段:
@IBAction func addNewItem(sender: AnyObject) {
}
這段 Code 的意思就是每當我們按下新增按鈕的時候,執行這段 Code。
也就是說,裡面要執行這些動作:
- 跳出新增的視窗
- 可以輸入 Task 的 Name
- 可以新增 Task
- 可以 Cancel 取消新增
讓我們一行一行打上去變這樣:
首先因為我們要在這個 Controller 裡面使用到 Realm 的功能,所以要在這個檔案的開頭加上
import RealmSwift
然後在 FirstViewController
class 裡面定義一個 Realm 的 Object 常數
let realm = try! Realm()
之後定義使用者按了新增的按鈕後應該會發生的動作:
@IBAction func addNewItem(sender: AnyObject) {
// 定義 alertController, UIAlertController 就是之後會跳出來的 Popup View
let alertController = UIAlertController(title: "Add Task", message: nil, preferredStyle: UIAlertControllerStyle.Alert)
// 加上一個 Input 的 TextField 框框讓使用者輸入 Task Name
alertController.addTextFieldWithConfigurationHandler({ (textField) -> Void in
textField.placeholder = "type task name here.."
})
// 定義這個 Alert Popup 會有哪些動作,此例我們只需要 Cancel 和 Add
let cancelAction = UIAlertAction(title: "Cancel", style: .Default, handler: { (action: UIAlertAction!) in
// handler 裡面的 code 代表使用者按了這個 Action 以後要處理的事情。
print("Canceled")
})
let addTaskAction = UIAlertAction(title: "Add", style: .Default, handler: { (action: UIAlertAction!) in
let textField = alertController.textFields![0] as UITextField
let taskName = textField.text! as String
// 初始化 Task 的 Object
let task = Task()
task.name = taskName
// 將 Task 寫進資料庫
try! self.realm.write {
self.realm.add(task)
}
})
// 把剛剛定義好的 Action 塞到 Alert 的 Popup 裡面
alertController.addAction(cancelAction)
alertController.addAction(addTaskAction)
// 將前面設定好的 Alert View 顯示出來
presentViewController(alertController, animated: true, completion: nil)
}
整個檔案會變成這樣:
import UIKit
import RealmSwift
class FirstViewController: UIViewController {
let realm = try! Realm()
@IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
@IBAction func addNewItem(sender: AnyObject) {
// 定義 alertController, UIAlertController 就是之後會跳出來的 Popup View
let alertController = UIAlertController(title: "Add Task", message: nil, preferredStyle: UIAlertControllerStyle.Alert)
// 加上一個 Input 的 TextField 框框讓使用者輸入 Task Name
alertController.addTextFieldWithConfigurationHandler({ (textField) -> Void in
textField.placeholder = "type task name here.."
})
// 定義這個 Alert Popup 會有哪些動作,此例我們只需要 Cancel 和 Add
let cancelAction = UIAlertAction(title: "Cancel", style: .Default, handler: { (action: UIAlertAction!) in
// handler 裡面的 code 代表使用者按了這個 Action 以後要處理的事情。
print("Canceled")
})
let addTaskAction = UIAlertAction(title: "Add", style: .Default, handler: { (action: UIAlertAction!) in
let textField = alertController.textFields![0] as UITextField
let taskName = textField.text! as String
// 初始化 Task 的 Object
let task = Task()
task.name = taskName
// 將 Task 寫進資料庫
try! self.realm.write {
self.realm.add(task)
}
})
// 把剛剛定義好的 Action 塞到 Alert 的 Popup 裡面
alertController.addAction(cancelAction)
alertController.addAction(addTaskAction)
// 將前面設定好的 Alert View 顯示出來
presentViewController(alertController, animated: true, completion: nil)
}
}
(2) 讓 Table 可以顯示所有未完成的 Tasks
請照著教學影片的做法將 Table 設定好
影片解釋:
我們使用了 UITableViewDelegate
和 UITableViewDataSource
兩個 Protocal 讓這個 FristViewController 可以有操作 Table 的能力,在 Storyboard 按住 Control
拖拉 Table View 是為了讓 Story Board 中 Table 的 Object 可以跟我們寫在 Controller 中的東西連結起來,建立 Reference,若是少了這個步驟那模擬器就不會顯示任何資訊在 Table 上面。
另外由於 UITableViewDataSource
需要有以下兩個 function, 所以我在影片中快速 Demo 了一下這兩個 Function 的作用,如果想要查在這兩個 protocal 裡面還有哪些可以用的 function 可以按住 Command 然後點擊它,就如同影片中做的一樣,有空可以多看看。
// 告訴 Table View 我們的 Table 有幾格 (幾個 Cells)
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1 // 這個 Table 只有 1 個 cell
}
// 告訴 Table View 每個 Cell 的內容
tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// 建立 Cell 的物件並設個 Text 測試看看是否成功。
let cell = UITableViewCell()
cell.textLabel!.text = "Test Cell Here!!"
return cell
}
Table 設定好之後,我們就要想辦法讓真正的資料顯示在 Cell 中
方便起見,我們先定義一個變數 items
來儲存所有新增的 Tasks
var tasks :Results<Task>!
然後在 viewDidLoad()
裡面把這些 Tasks 都拿出來:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
loadTasks()
}
func loadTasks() {
tasks = realm.objects(Task).filter("isCompleted == 0")
}
viewDidLoad()
這個 function 的意思就是讓手機在 load 完這個 View 但是還沒顯示前做的事情,所以我們要趁沒顯示前先把資料準備好也是很合理的。
接下來就是把剛剛寫過的兩個 table 相關的 functions 改成以下的樣子:
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// 有多少 Tasks 就有多少 Cell
return tasks.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = UITableViewCell()
// 因為 tasks 是一個 List,所以我們可以從 cell 的順序來取得每一個 Task,按照順序顯示出來
cell.textLabel!.text = tasks[indexPath.row].name
return cell
}
最後還有一件事情要做,就是在每次 Task 有變動的時候記得要重新 refresh table,目前我們會變動的時候只有新增 Task 的時候,所以要加兩行 Code 在 addNewItem()
的裡面,分別是重新讀取 Tasks 的資料以及 Refresh table
self.loadTasks()
self.tableView.reloadData()
addNewItem
的 function 變成這樣:
@IBAction func addNewItem(sender: AnyObject) {
// 定義 alertController, UIAlertController 就是之後會跳出來的 Popup View
let alertController = UIAlertController(title: "Add Task", message: nil, preferredStyle: UIAlertControllerStyle.Alert)
// 加上一個 Input 的 TextField 框框讓使用者輸入 Task Name
alertController.addTextFieldWithConfigurationHandler({ (textField) -> Void in
textField.placeholder = "type task name here.."
})
// 定義這個 Alert Popup 會有哪些動作,此例我們只需要 Cancel 和 Add
let cancelAction = UIAlertAction(title: "Cancel", style: .Default, handler: { (action: UIAlertAction!) in
// handler 裡面的 code 代表使用者按了這個 Action 以後要處理的事情。
print("Canceled")
})
let addTaskAction = UIAlertAction(title: "Add", style: .Default, handler: { (action: UIAlertAction!) in
let textField = alertController.textFields![0] as UITextField
let taskName = textField.text! as String
// 初始化 Task 的 Object
let task = Task()
task.name = taskName
// 將 Task 寫進資料庫
try! self.realm.write {
self.realm.add(task)
}
self.loadTasks()
self.tableView.reloadData()
})
// 把剛剛定義好的 Action 塞到 Alert 的 Popup 裡面
alertController.addAction(cancelAction)
alertController.addAction(addTaskAction)
// 將前面設定好的 Alert View 顯示出來
presentViewController(alertController, animated: true, completion: nil)
}
5. 可以將每個 Task Mark as completed 和 Delete
接下來要做滑動 Cell 就可以把 Task 變成完成和刪除的功能,在 UITableViewDataSource
裡面有提供 editActionsForRowAtIndexPath
的 API 給我們用,所以只要寫以下的 Code 就可以實作:
func tableView(tableView: UITableView, editActionsForRowAtIndexPath indexPath: NSIndexPath) -> [UITableViewRowAction]? {
let doneAction = UITableViewRowAction(style: .Normal, title: "Mark as completed") { action, index in
let task = self.tasks[indexPath.row]
try! self.realm.write {
task.isCompleted = true
}
self.loadTasks()
self.tableView.reloadData()
}
doneAction.backgroundColor = UIColor.greenColor()
let deleteAction = UITableViewRowAction(style: .Default, title: "Delete") { action, index in
let task = self.tasks[indexPath.row]
try! self.realm.write {
self.realm.delete(task)
}
self.loadTasks()
self.tableView.reloadData()
}
return [deleteAction, doneAction]
}
6. 完成 Completed 的 Tab
以上就大致完成了這個 Todo App, 剩下的就是把一樣的功能也寫進 SecondViewController
裡面,大致上就把 FirstViewController
的 Code 貼過去,大概會長這樣:
import UIKit
import RealmSwift
class SecondViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
let realm = try! Realm()
var tasks :Results<Task>!
@IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
loadTasks()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tasks.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.textLabel!.text = tasks[indexPath.row].name
return cell
}
@IBAction func addNewItem(sender: AnyObject) {
let alertController = UIAlertController(title: "Add Task", message: nil, preferredStyle: UIAlertControllerStyle.Alert)
alertController.addTextFieldWithConfigurationHandler({ (textField) -> Void in
textField.placeholder = "type task name here.."
})
let cancelAction = UIAlertAction(title: "Cancel", style: .Default, handler: { (action: UIAlertAction!) in
print("Canceled")
})
let addTaskAction = UIAlertAction(title: "Add", style: .Default, handler: { (action: UIAlertAction!) in
let textField = alertController.textFields![0] as UITextField
let taskName = textField.text! as String
let task = Task()
task.name = taskName
try! self.realm.write {
self.realm.add(task)
}
})
alertController.addAction(cancelAction)
alertController.addAction(addTaskAction)
presentViewController(alertController, animated: true, completion: nil)
}
func tableView(tableView: UITableView, editActionsForRowAtIndexPath indexPath: NSIndexPath) -> [UITableViewRowAction]? {
let doneAction = UITableViewRowAction(style: .Normal, title: "Mark as uncompleted") { action, index in
let task = self.tasks[indexPath.row]
try! self.realm.write {
task.isCompleted = false
}
self.loadTasks()
self.tableView.reloadData()
}
doneAction.backgroundColor = UIColor.greenColor()
let deleteAction = UITableViewRowAction(style: .Default, title: "Delete") { action, index in
let task = self.tasks[indexPath.row]
try! self.realm.write {
self.realm.delete(task)
}
self.loadTasks()
self.tableView.reloadData()
}
return [deleteAction, doneAction]
}
func loadTasks() {
tasks = realm.objects(Task).filter("isCompleted == 1")
}
}
幾個注意的點
- 記得把 table view 跟 delegate 和 datasource reference 起來(storyboard)
- 把 mark as completed 改成 mark as uncompleted
- Load
isCompleted == 1
的 tasks - 新增完 task 不用 reload tasks 跟 table view
做完後其實會有一個小 Bug,就是 mark as completed/uncompleted 後再切換到另外一個 Tab 發現剛剛 Mark 的 Task 並沒有出現在上面,原因是因為我們只有在 viewDidLoad 的時候更新 Tasks,但是切換 Tab 的時候 View 早就 Loaded 好了,所以並不會再度觸發 viewDidLoad
裡面的 loadTasks()
,所以其實我們應該要在這兩個 Tab 的 Controller 裡面再加上一個 function:
override func viewDidAppear(animated: Bool) {
loadTasks()
tableView.reloadData()
}
這段就會讓我們在切換 Tab 但是 View 還沒顯示出來的時候運行,以上。
最後成品:Github
待續
- 整理 Code
- Edit Task
- 批次刪除 Tasks
參考資料
Realm
CocoaPods
http://www.appcoda.com/realm-database-swift/