SQLite로 데이터베이스를 활용하려다 Realm이라는 새로운 모바일 데이터베이스를 발견하고 이를 사용해 보기로 한다. 사용방법이나 튜토리얼이 모두 한글로 자세히 설명이 되어 있기에 금방 익숙해 질 수 있을 것 같다. 아래의 페이지를 참고하자.

 https://realm.io/kr/


 아직 자세히는 모르지만 직접 DB에 접속하여 스키마를 구현하는 것이 아니라, 개발 코드 내에서 Realm model의 Object를 상속받은 클래스를 정의하면 자동으로 DB에 해당 형태로 스키마를 구현하는 듯 하다. 즉 클래스의 프로퍼티를 정의하면 DB의 컬럼이 설정된다고 보면 될 것 같다. 그리고 SQL이 아니라 일반 객체의 메서드를 이용하는 것 처럼 DB의 데이터를 조작할 수 있다.


 이전에 만들었던 FoodVO를 대체하는 Realm 오브젝트 클래스를 만들어보자. 먼저 Realm을 사용하기 위한 framework를 Xcode에 추가해준다.




  Food.swift라는 오브젝트 클래스 파일을 만들고 아래와 같이 작성한다.


Food.swift

import Foundation
import RealmSwift

class Food: Object {
    
// Specify properties to ignore (Realm won't persist these)
    dynamic var name: String = ""
    dynamic var expDate: Date = Date(timeIntervalSinceNow: 0)
    private dynamic var categoryRawState = FoodCategory.Grain.rawValue
    public var category: FoodCategory {
        get {
            return FoodCategory(rawValue: categoryRawState)!
        }
        set {
            categoryRawState = newValue.rawValue
        }
    }
    
//  override static func ignoredProperties() -> [String] {
//    return []
//  }
}

enum FoodCategory : Int {
    case Grain, Potato, Sugars, Pulses, SeedsAndNuts, Vegetables, Mushrooms, Fruits, Meat, Eggs, Seafoods, Seaweed, MilkProducts, FatAndOils, Drinks, Seasoning, Etc
    static var count: Int { return FoodCategory.Etc.rawValue + 1 }
    
    var description: String {
        switch self {
        case .Grain : return "곡류"
        case .Potato : return "감자류"
        case .Sugars : return "당류"
        case .Pulses : return "두류"
        case .SeedsAndNuts : return "종실류"
        case .Vegetables : return "채소류"
        case .Mushrooms : return "버섯류"
        case .Fruits : return "과실류"
        case .Meat : return "육류"
        case .Eggs : return "난류"
        case .Seafoods : return "어패류"
        case .Seaweed : return "해조류"
        case .MilkProducts : return "유제품류"
        case .FatAndOils : return "유지류"
        case .Drinks : return "음료"
        case .Seasoning : return "조미료류"
        case .Etc : return "기타"
        }
    }
}

 데이터베이스에서는 enum 타입을 인식하지 못하므로, 실제 데이터베이스에 저장될 값은 private로 지정하여 Int 형태로 저장되게 하며, 코드 상에서만 enum타입으로 인식될 수 있도록 public 프로퍼티를 따로 설정하여 준다. enum타입인 FoodCategory를 해당 클래스에 동시에 선언해주었다. FoodCategory에는 단순 case만 입력한 것이 아니라 추가적으로 몇개의 데이터가 있는지 정적 변수를 추가하였으며, 각각에 대한 설명을 반환하는 변수 또한 추가하였다.



 이제 기존 코드를 수정하여 Realm 데이터베이스에 저장하고 값을 가져 올 수 있게 하자.


RefrigeratorTableViewController.swift

import UIKit
import RealmSwift

class RefrigeratorTableViewController : UITableViewController {
    var list = Array<Food>()
    var dateFormat = DateFormatter()
    
    override func viewDidLoad() {
        dateFormat.dateFormat = "yyyy-MM-dd"
        dateFormat.locale = Locale.init(identifier: "ko_KR")
        list.append(Food(value: ["name":"서울우유", "category":FoodCategory.MilkProducts, "expDate":dateFormat.date(from: "2017-04-20")!]))
        list.append(Food(value: ["name":"3분짜장", "category":FoodCategory.Etc, "expDate":dateFormat.date(from: "2017-07-31")!]))
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return list.count
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let row = list[indexPath.row]
        let cell = tableView.dequeueReusableCell(withIdentifier: "SokoCell")!
        cell.textLabel?.text = row.name
        cell.detailTextLabel?.text = "유통기한: \(dateFormat.string(from: row.expDate))"
        
        return cell
    }
    
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        NSLog("Touch Table Row at %d", indexPath.row)
    }
    
    @IBAction func unwindToMainViewController(segue : UIStoryboardSegue) {
        
    }
    
    @IBAction func unwindToMainViewControllerAndRefreshList(seque : UIStoryboardSegue) {
        
    }

}


위 코드는 데이터베이스에 저장하는 코드가 아닌 Realm 객체로 테이블 뷰에 표시하는 코드이다. 이 코드로 실행이 되는지 테스트하고 진행한다.



'재료 추가' View Controller를 조금 더 완성시켜서 컴포넌트에 입력한 값을 저장할 수 있도록 만든다. 먼저 Swift파일을 하나 새로 생성해서 'AddFoodViewController.swift'라 이름 짓고 아래와 같이 storyboard의 ViewController와 연결한다. 또한 카테고리를 표현하는 PickerView도 추가한다.



 값을 읽어올 컴포넌트를 아래와 같이 연결한다.



 '닫기'와 '추가' 버튼을 unwind segue로 미리 RefrigeratorTableViewController에 생성한 unwindToMainViewController(segue:) 메서드로 연결한다. 그리고 '닫기'버튼의 Identifier를 'AddCancel'로, '추가'버튼은 'AddDone'으로 설정한다.




 코드를 아래와 같이 완성한다.


AddFoodViewController.swift

import UIKit

class AddFoodViewController : UIViewController, UIPickerViewDataSource, UIPickerViewDelegate {
    @IBOutlet var name: UITextField!
    @IBOutlet var expDate: UIDatePicker!
    @IBOutlet var category: UIPickerView!
    
    override func viewDidLoad() {
        
        // textField에 Custom디자인을 적용한 코드. 밑줄이 생성된다.
        let border = CALayer()
        let width = CGFloat(0.3)
        border.borderColor = UIColor.gray.cgColor
        border.frame = CGRect(x: 0, y: name.frame.size.height - width, width: name.frame.size.width, height: name.frame.size.height)
        
        border.borderWidth = width
        name.layer.addSublayer(border)
        name.layer.masksToBounds = true
    }
    
    // UIPickerView 설정 메서드
    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 1
    }
    
    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return FoodCategory.count
    }
    
    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        return FoodCategory(rawValue: row)?.description
    }
    
    // 새 Food객체 생성 메서드
    func newFood() -> Food? {
        let food = Food(value: ["name":name.text!, "expDate":expDate.date])
        food.category = FoodCategory(rawValue: category.selectedRow(inComponent: 0))!
        return food
    }
    
    // unwind 하기 전 실행되는 메서드.
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "AddDone" {
            guard let food = newFood(), let refrigeratorTableViewController = segue.destination as? RefrigeratorTableViewController else {
                return
            }
            
            let realm = refrigeratorTableViewController.realm!
            try! realm.write {
                realm.add(food)
            }
        }
    }
}


 '추가' 버튼을 누를 때 컴포넌트에 설정된 값들이 저장되어야 한다. 따라서 unwind 하기 전에 prepare 메서드에서 DB에 저장하고, RefrigeratorTableViewController에서 DB가 수정됨을 확인하면 자동으로 화면을 새로고침한다. 이제 RefrigeratorTableViewController를 완성시키자.


RefrigeratorTableViewController.swift

import UIKit
import RealmSwift

class RefrigeratorTableViewController : UITableViewController {
    var list: Results<Food>?
    var dateFormat = DateFormatter()
    var notificationToken: NotificationToken!
    var realm: Realm!
    
    override func viewDidLoad() {
        dateFormat.dateFormat = "yyyy-MM-dd"
        dateFormat.locale = Locale.init(identifier: "ko_KR")
        setupRealm()
    }
    
    func setupRealm() {
        try! realm = Realm()
        
        func updateList() {
            if list == nil {
                list = self.realm.objects(Food.self)
            }
            self.tableView.reloadData()
        }
        updateList()
        
        self.notificationToken = self.realm.addNotificationBlock { _ in
            updateList()
        }
    }
    
    deinit {
        notificationToken.stop()
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return list!.count
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let row = list![indexPath.row]
        let cell = tableView.dequeueReusableCell(withIdentifier: "SokoCell")!
        cell.textLabel?.text = row.name
        cell.detailTextLabel?.text = "유통기한: \(dateFormat.string(from: row.expDate))"
        
        return cell
    }
    
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        NSLog("Touch Table Row at %d", indexPath.row)
    }
    
    @IBAction func unwindToMainViewController(segue : UIStoryboardSegue) {
    }
}


 list타입을 원래의 Array<Food>타입이 아닌 Results<Food>타입으로 변경하였다. Results타입은 Realm 프레임워크에서 제공하는 타입으로 사용자가 Realm에서 가져온 데이터들의 배열로 Array와 유사합니다. 단 실제 DB데이터를 가지고 있는것과 같아서 이 배열에 값을 추가하므로써 DB에 값을 저장하는 것과 동일한 효과를 갖는다.

 setupRealm()에서 Realm 객체를 할당하고 Realm으로 부터 저장된 데이터들을 가져오는 updateList()메서드를 수행한다. 또한 notificationToken을 이용하여 Realm의 변화 여부를 감지한다.


 앱을 실행하여 Realm에 값이 저장되어 앱이 꺼지고 켜져도 값이 유지되는지 확인해보자.




 앱이 종료되었다가 다시 실행되어도 값이 유지되고 있기에 메모리에 유지된 것이 아닌 별도의 데이터베이스에 값이 저장되고 있는 것을 알 수 있다. Simulator의 앱 경로를 찾아 Realm Browser를 이용하여 Realm에 저장되고 있는지 확인 할 수 있다. (Realm Browser는 macOS에서만 제공된다.)


 Realm 파일의 경로는 아래와 같다. 아래의 파일을 Realm 브라우저로 실행하면 위와 같이 내용을 확인 할 수 있다.

/Users/<username>/Library/Developer/CoreSimulator/Devices/<simulator-uuid>/data/Containers/Data/Application/<application-uuid>/Documents/default.realm

 Xcode의 Debug 환경에서 아래의 LLDB 명령어를 입력하여 앱의 경로를 바로 찾을 수 있다.

(lldb) po Realm.Configuration.defaultConfiguration.fileURL

자세한 내용은 아래의 링크를 참고하자.

https://stackoverflow.com/questions/28465706/how-to-find-my-realm-file/28465803#28465803

+ Recent posts