Save data

บันทึกให้ด้วยสิ

ถ้าเราไม่ทำให้บันทึกค่าเก็บไว้ปกติ ถ้าเรา kill แอพทิ้งแล้วรันใหม่ข้อมูลเราจะหายไป

ขั้นแรกเราต้องทำให้สามารถ encode และ decode คลาส Todo ของเราให้ได้ก่อน เพราะเราจะได้แปลงเป็น data ที่จะบันทึกได้ต่อไป

วิธีการก็แสนจะง่าย ให้เราเพิ่ม conform Codable ที่ Todo และ TodoItem เป็นอันเสร็จ

Todo.swift
import Foundation

class Todo: Codable {
    private var items = [TodoItem]()

    var totalItems: Int {
        return items.count
    }

    func item(at index: Int) -> TodoItem {
        return items[index]
    }

    func add(item: TodoItem) {
        items.insert(item, at: 0)
    }

    func remove(at index: Int) {
        items.remove(at: index)
    }

    func index(of item: TodoItem) -> Int? {
        return items.firstIndex(where: { (todoItem) -> Bool in
            return todoItem === item
        })
    }

    func move(from sourceIndex: Int, to destinationIndex: Int) {
        let item = items.remove(at: sourceIndex)
        items.insert(item, at: destinationIndex)
    }
}
TodoItem.swift
import Foundation

class TodoItem: Codable {
    var title: String
    var isDone: Bool

    init(title: String, isDone: Bool = false) {
        self.title = title
        self.isDone = isDone
    }
}

บันทึกลงไฟล์

เราใช้ FileManager ในการสร้าง url ไปยังไฟล์ที่เรากำหนด ใช้ PropertyListEncoder ในการ encode ข้อมูลของเรา

TodoListViewController
func saveTodo() {
    do {
        let encoder = PropertyListEncoder()
        encoder.outputFormat = .xml
        let data = try encoder.encode(todo)

        let fileManager = FileManager.default
        var destinationURL = try fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
        destinationURL.appendPathComponent("todo")
        destinationURL.appendPathExtension("plist")
        
        try data.write(to: destinationURL)
    } catch {
        print("Cannot save todo to file, Error: \(error)")
    }
}

เสร็จแล้วให้เพิ่มเรียก saveTodo() ที่ delegate, dataSource method ที่ใช้ในการ Add, Edit และ Delete TodoItem และ todoItemTableViewCellDidTapCheckboxButton รวมทั้งตอน dropItem

โดย default PropertyListEncoder จะ encode เป็น binary แต่เราจะใช้ xml (บรรทัดที่ 4) ก่อนเพื่อที่จะได้อ่านได้

เราสามารถ print(destinationURL.path) ออกมาดูได้ว่าไฟล์อยู่ที่ไหนและลองเปิดดูได้

อ่านจากไฟล์

เหมือนบันทึกลงไฟล์ แต่คราวนี้ใช้ PropertyListDecoder ในการ decode และถ้าไม่มีไฟล์ให้อ่านเราจะข้ามไป

TodoListViewController.swift
func loadTodo() {
    do {
        let fileManager = FileManager.default
        var destinationURL = try fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
        destinationURL.appendPathComponent("todo")
        destinationURL.appendPathExtension("plist")

        if fileManager.fileExists(atPath: destinationURL.path) {
            let data = try Data(contentsOf: destinationURL)
            let decoder = PropertyListDecoder()
            todo = try decoder.decode(Todo.self, from: data)
            tableView?.reloadData()
        }

    } catch {
        print("Cannot load todo from file, Error: \(error)")
    }
}

เพิ่มโค้ดให้อ่านไฟล์ตอนโหลด

TodoListViewController.swift
override func viewDidLoad() {
    super.viewDidLoad()
    tableView?.dragDelegate = self
    tableView?.dragInteractionEnabled = true

    tableView?.dropDelegate = self

    loadTodo()
}

จากนั้นลองรันแอพ เพิ่มรายการ kill แอพ แล้วเปิดดูใหม่

รายละเอียดเพิ่มเติมเกี่ยวกับเรื่องนี้ได้ที่ Archives and Serialization

Refactor Code หน่อยยย

จะเห็นว่าโค้ดที่ใช้หา Path ของไฟล์ที่กำหนดมันใช้ซ้ำกัน เราก็สร้างฟังก์ชั่นใหม่มาครอบดีกว่า

TodoListViewController.swift
func loadTodo() {
    do {
        let fileManager = FileManager.default
        let destinationURL = try makeTodoFileURL(fileManager: fileManager)

        if fileManager.fileExists(atPath: destinationURL.path) {
            let data = try Data(contentsOf: destinationURL)
            let decoder = PropertyListDecoder()
            todo = try decoder.decode(Todo.self, from: data)
            tableView?.reloadData()
        }
    } catch {
        print("Cannot load todo from file, Error: \(error)")
    }
}

func saveTodo() {
    do {
        let destinationURL = try makeTodoFileURL(fileManager: FileManager.default)

        let encoder = PropertyListEncoder()
        let data = try encoder.encode(todo)
        
        try data.write(to: destinationURL)
    } catch {
        print("Cannot save todo to file, Error: \(error)")
    }
}

func makeTodoFileURL(fileManager: FileManager) throws -> URL {
    var destinationURL = try fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
    destinationURL.appendPathComponent("todo")
    destinationURL.appendPathExtension("plist")
    return destinationURL
}

Last updated