Swift: Add, Edit, Delete — part1

Jru
彼得潘的 Swift iOS / Flutter App 開發教室
6 min readDec 17, 2022

What if there’s no diners but have a convenience store nearby, you should be thankful, even though the food in the store tasted not as good as diners.

Let’s use this app to record what you bought there and comment how your feeling.

App features

part1:

  • add, edit, delete content → to record items in document directory or UserDefaults

part2:

part3:

Storyboard

ListTableViewController is in the middle of picture and EditTableViewController is right side of picture.

User can press + which is on the upper right corner of ListTableViewController and add or edit the item on the EditTableViewController. Then press Done after finishing which is on the upper right corner of EditTableViewController. And the item that user save will appear on the ListTableViewController.

Model

Define struct

import Foundation

struct Item:Codable{ //要 Codable or Encodable 才能轉成 Data
let photoName:String?
let store:String
let item:String
let date:Date
let price:Int
let discount:Bool
let comment:String
var photoURL:URL {
Item.documentsDirectory.appending(path: photoName ?? "")
}

.....Save/Load data function......
}

Save/ Load Data

There are two ways can save and load the item data.

I. UserDefaults

import Foundation

struct Item:Codable{ //要 Codable or Encodable 才能轉成 Data

......Define struct......

static func loadItems() -> [Item]?{
let userDefaults = UserDefaults.standard
guard let data = userDefaults.data(forKey: "items") else { return nil}
let decoder = JSONDecoder()
return try? decoder.decode([Item].self, from: data)
}


static func saveItems(_ items: [Item]){
let encoder = JSONEncoder()
guard let data = try? encoder.encode(items) else { return }
let userDefaults = UserDefaults.standard
userDefaults.set(data, forKey: "items")
}

}

II. documentsDirectory

struct Item:Codable{ //要 Codable or Encodable 才能轉成 Data

......Define struct......


static let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!

static func loadItems() -> [Self]?{ // "?" 有可能讀到東西也可能沒讀到
let decoder = JSONDecoder()
let url = documentsDirectory.appendingPathExtension("items")
guard let data = try? Data(contentsOf: url) else {return nil}
return try? decoder.decode([Self].self, from: data)

}

static func saveItems(_ items:[Self]){ //Self (大寫的 S) 代表型別 Item
let encoder = JSONEncoder()
let data = try? encoder.encode(items)
let url = documentsDirectory.appendingPathExtension("items")
try? data?.write(to: url)
}

}

Controller — Add / Edit/ Delete

I. Add / Edit

prepare(for segue:sender:) to deliver the data from EditTableViewController to ListTableViewController.

EditTableViewController

    var thing:Item?
var isSelectedPhoto = false //是否選擇相片

//準備傳遞資料
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
var photoName:String?
let store = storeTextField.text ?? ""
let item = itemTextField.text ?? ""
let date = datePicker.date
let price = Int(priceTextField.text ?? "0") ?? 0
let discount = discountSwitch.isOn
let comment = commentTextField.text ?? ""
//如果選擇照片
if isSelectedPhoto{
if let item = thing{ //修改:改照片就用原本名字
photoName = item.photoName
}
if photoName == nil{ //新增:取新名字
photoName = UUID().uuidString
}
//圖片呼叫data,透過data存檔到指定的路徑
//compressionQuality(0~1)來減少圖片容量
let photoData = photoImageView.image?.jpegData(compressionQuality: 0.7)
//圖片路徑:先讀出「資料夾」加上「圖片名稱」加上「副檔名」
let photoURL = Item.documentsDirectory.appending(path: photoName!).appendingPathExtension("jpg")
//將圖片存入路徑位置=>write是複寫,存入後之前的會被覆蓋掉
//<方法一>try? photoData?.write(to: photoURL)
//<方法二>
do{
let _ = try photoData?.write(to: photoURL)
}catch{
print("can't get photoURL")
}

}

thing = Item(photoName:photoName,store: store, item: item, date: date, price: price, discount: discount, comment: comment)
}

var thing:Item? is a stored property prepare to send new data to ListTableViewController. And it also store the data from ListTableViewController to EditTableViewController when editing.

var isSelectedPhoto = false is a way to know if there is photo be selected. If there’s a new photo, UUID().uuidString give this photo a new name and save it with data format.

jpegData(compressionQuality:) that contains the image in JPEG format. And this method can compress the image as a value from 0.0 to 1.0.

Alert message

EditTableViewController

shouldPerformSegue(withIdentifier:sender:) to check if textfield is empty before sending the data to ListTableViewController. If user don’t finish the textfield, it will show alert message that tell user to finish the empty textfield, otherwise the data can’t show on the ListTableViewController.

 override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool {
if storeTextField.text?.isEmpty == false, itemTextField.text?.isEmpty == false, priceTextField.text?.isEmpty == false, commentTextField.text?.isEmpty == false{
return true
}else if storeTextField.text?.isEmpty == true{
let alertController = UIAlertController(title: "\(Message.store.rawValue)", message: "Where you bought the item?", preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "OK", style: .default))
present(alertController, animated: true)
}else if itemTextField.text?.isEmpty == true{
let alertController = UIAlertController(title: "\(Message.item.rawValue)", message: "What did you buy?", preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "OK", style: .default))
present(alertController, animated: true)
}else if priceTextField.text?.isEmpty == true{
let alertController = UIAlertController(title: "\(Message.price.rawValue)", message: "How much it is?", preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "OK", style: .default))
present(alertController, animated: true)
}else if commentTextField.text?.isEmpty == true{
let alertController = UIAlertController(title: "\(Message.comment.rawValue)", message: "How about it?", preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "OK", style: .default))
present(alertController, animated: true)

}
return false
}

Leave EditTableViewController

EditTableViewController

Done button connect with Exit, then select unwind segue method wrote in the ListTableViewController.

dismiss EditTableViewController

ListTableViewController

unwindTo___(unwindSegue:) unwind segue method can dismiss from EditTableViewController and back to ListTableViewController. This method should write on the ListTableViewController.

unwind function

Through this unwind method can also send the data back to EditTableViewController while edit the data.

Add new or edit previous data

ListTableViewController

Add new:

to use property observer to save data automatically while data have any change.

var items = [Item](){ //property observer應用儲存資料
didSet{ //array 有變動時存檔
Item.saveItems(items)
}
}

indexPathForSelectedRow : to add a new item or edit previous item.

//退掉編輯頁面回到前一頁(此頁)
@IBAction func unwindToListTableViewControllerWithSegue(_ unwindSegue: UIStoryboardSegue) {
//修改
if let sourceViewController = unwindSegue.source as? EditTableViewController, let thing = sourceViewController.thing
{
//當 indexPathForSelectedRow 有值時表示修改,否則為新增
if let indexPath = tableView.indexPathForSelectedRow{
//從編輯頁傳回此頁
items[indexPath.row] = thing
tableView.reloadRows(at: [indexPath], with: UITableView.RowAnimation.automatic)
}else{
//新增
items.insert(thing,at: 0) //加入陣列

let NewIndexPath = IndexPath(row: 0, section: 0)
//列表頁新增加入的動畫
tableView.insertRows(at: [NewIndexPath], with: .fade)
tableView.reloadData()
}
}

//<補充>新增時 indexPathForSelectedRow 是 nil,點選 cell 修改時 indexPathForSelectedRow 才會有值
}

Edit previous data:

And load the previous data form UserDefaults or documentDirectory.

override func viewDidLoad() {
super.viewDidLoad()

//讀檔得到之前儲存的資料並存入陣列中
if let things = Item.loadItems(){
self.items = things
}
}

to send data back to EditTableViewController in order to edit data.

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
//修改資料傳到下一頁
if let controller = segue.destination as? EditTableViewController, let row = tableView.indexPathForSelectedRow?.row{
controller.thing = items[row]
}
}

Load previous saved data

EditTableViewController

viewDidLoad() with loading previous saved data .

var thing:Item? // thing is a property to save the data form ListTableViewController

override func viewDidLoad() {
super.viewDidLoad()

//顯示要修改的資料
editorUpdateUI()
}

//修改資料傳到編輯頁,顯示之前的記錄(讀檔)
func editorUpdateUI(){
if thing != nil{
storeTextField.text = thing?.store
itemTextField.text = thing?.item
datePicker.date = thing!.date
priceTextField.text = thing?.price.description
commentTextField.text = thing?.comment
//有圖片名字才去讀出url
if let imageName = thing?.photoName{
let photoURL = Item.documentsDirectory.appending(path: imageName).appendingPathExtension("jpg")
photoImageView.image = UIImage(named: photoURL.path)
}

}
}

II. Delete

ListTableViewController

 //刪除。屬於UITableViewDataSource protocol 的 function
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
//先從記憶體刪除,在刪除陣列裡資料
try? FileManager.default.removeItem(at: items[indexPath.row].photoURL)

items.remove(at: indexPath.row) //左滑 cell 將出現 delete 的 button
Item.saveItems(items)
//畫面更新,刪除cell上的資料
//tableView.reloadData() //要點選delete按鈕才能刪除,滑得無法刪除
tableView.deleteRows(at: [indexPath], with: .middle) //->滑掉或按刪除按鈕都可刪除。tableView.reloadData()二選一寫
}

//<補充>東⻄從 array 移除後,才呼叫deleteRowsAtIndexPaths: withRowAnimation:,否則會閃退。因為 numberOfRows(inSection:) 回傳的 cell 數量要與東⻄刪除後的數量相同

--

--