Сегодня мы научимся быстро и просто управлять данными в своем приложении, с помощью Realm.

Мобильные базы данных

Ни для кого не станет секретом, если я скажу, что сегодня наши девайсы обмениваются большими объемами информации по высокоскоростным каналам мобильной связи, часть этой информации просто исчезает,  выгружаясь вместе с оперативной памятью закрываемого приложения, а другая часть оседает в перcистентных хранилищах прямо на ваших устройствах. Причем объем этих хранилищ может колебаться от нескольких мегабайт с мета-информацией до десятков гигабайт медиа-контента. IOS поддерживает несколько видов этих хранилищ, отличных по своим принципам работы и назначению, но сегодня мы поговорим именно про мобильные базы данных.

Обычно, на мобильные СУБД возлагают следующий ряд задач:

  • Поддержка функциональной возможности работы в Offline;
  • Синхронизация с централизованным хранилищем для нескольких устройств;
  • Обеспечение приемлемого времени выполнения запроса на мобльных ВУ;
  • Повышенное внимание к сохранению целостности данных на мобильных ВУ.

Realm

Итак, Realm это кроссплатформенная мобильная система управления данными, которая предполагает использование одних и тех же файлов баз данных, для всех поддерживаемых мобильных ОС. Realm создавался, как замена SQLite, что уже де-факто успел стать стандартом в индустрии разработки мобильных приложений. Технология обладает высокой производительностью и масштабируемостью, так что если ваше приложение работает с большим числом записей — возможно, это ваш кейс.

Что еще немаловажно отметить, Realm будет доступен вам для всех современных устройств Apple экосистемы:

  • iOS;
  • macOS;
  • tvOS;
  • watchOS.

На текущий момент Pod имеет отличный рейтинг на GitHub’e, лицензируется по Apache License 2.0(по принципу работы — всем известная MIT лицензия) и распространяется сразу для нескольких языков, включающих Swift(RealmSwift) и Objective-C(Realm).

API

Realm имеет простой и лаконичный API, что довольно выгодно отличает его от Core Data. Для получения экземпляра Realm мы просто пробуем создать новый объект:


import RealmSwift

let realmInstance = try! Realm()

Для создания модели мы просто субклассируем Object, который является базовым классом для всех объектов-моделей в Realm:


import RealmSwift

class Model: Object {
    dynamic var property = String()
}

Запись и чтение данных модели можно произвести с помощью нашего realmInstance:


import RealmSwift

//создаем и инициализируем модель
let model = Model()
model.property = "storedProperty"

//сохраняем модель в персистентном хранилище
try! realmInstance.write {
  realmInstance.write.add(model)
}

//читаем данные из хранилища
let model = realmInstance.objects(Model.self).first

В следующих статьях из моего цикла по Realm, мы более детально рассмотрим возможности API, а пока этого нам вполне хватит что-бы написать простое приложение, реализующее запись и отображение данных в UI.

Проектируем базу данных

Для начала давайте определимся с задачей, пускай это будет приложение, хранящее информацию о членах команды cocoa-beans. Что-бы не загромождать статью основами реляционных СУБД, сделаем простую БД из 2 сущностей:

  • Пользователь
    • Идентификатор
    • Имя
    • Специализация
    • Описание
  • Специализация
    • Идентификатор
    • Имя

Получилась вот такая элементарная схема базы данных:

Вы уже наверное успели заметить связь М:М, для тех кто забыл — напоминаю: «каждый пользователь может иметь одну и более специализаций & каждая специализация может быть изучена одним и более пользователем». Так вот, для сегодняшнего примера нам не нужен полный пул запросов, поэтому о том как построить many to many связь через инверсию отношений я расскажу в следующей статье.

Мы спроектировали нашу БД, теперь можно запустить Xcode и построить модели соответствующие нашим сущностям в схеме БД, это исключительно просто:


class User: Object {
    
    dynamic var id = Int()
    dynamic var name = String()
    dynamic var about = String()
    dynamic var articles = Int()
    let specialties = List()
    
    //устанавливаем PK
    override static func primaryKey() -> String? {
        return "id"
    }
    
    //статический метод для быстрой инициализации
    static func getUserObject(
        id: Int, name: String, specialties: [Specialty], about: String, articles: Int) -> User {
        let user = User()
        user.id = id
        user.name = name
        user.about = about
        user.articles = articles
        for specialty in specialties {
            user.specialties.append(specialty)
        }
        return user
    }
}

Проделываем то же самое для модели специализации наших пользователей:


class Specialty: Object {
    dynamic var id = Int()
    dynamic var name = String()

    //устанавливаем PK    
    override static func primaryKey() -> String? {
        return "id"
    }
    
    //статический метод для быстрой инициализации
    static func getSpecialtyObject(
        id: Int, name: String) -> Specialty {
        let specialty = Specialty()
        specialty.id = id
        specialty.name = name
        return specialty
    }
}

Довольно просто неправда-ли? Это все, что необходимо сделать для описания наших моделей.

Готовим проект

Наш проект будет иметь два контроллера:

  • UserTableViewController
  • UserViewController

Первый контроллер будет отображать общий список участников нашей команды, а второй детальную информацию по каждому из участников. Соотвественно подготовим следующую раскадровку для нашего проекта:

Теперь необходимо проставить IBOutlet соответствующим контроллерам и назначить их в InterfaceBuilder’е. Для кастомизации ячейки таблицы нам потребуется создать новый xib и привязать к нему соотвествующий UITableViewCell:

Устанавливаем БД

Для установки нашей БД добавим функцию инициализации в AppDelegate:


    func loadDatabase() {
        //создаем объекты специальностей
        let swift = Specialty.getSpecialtyObject(
            id: 0,
            name: "swift"
        )
        
        let objc = Specialty.getSpecialtyObject(
            id: 1,
            name: "objective-c"
        )
        
        let backend = Specialty.getSpecialtyObject(
            id: 2,
            name: "backend"
        )
        
        let specialties = [swift, objc, backend]
        
        //создаем объекты пользователей
        let alex = User.getUserObject(
            id: 0,
            name: "Алексей Каратаев",
            specialties: [swift],
            about: "Saint-petersburg, Russia",
            articles: 5  
        )
        
        let georguy = User.getUserObject(
            id: 1,
            name: "Георгий Емельянов",
            specialties: [swift, objc],
            about: "Saint-petersburg, Russia",
            articles: 3
        )
        
        let dmitry = User.getUserObject(
            id: 2,
            name: "Дмитрий Никулин",
            specialties: [backend],
            about: "Saint-petersburg, Russia",
            articles: 4
        )
        
        let albert = User.getUserObject(
            id: 3,
            name: "Альберт Хафизов",
            specialties: [objc],
            about: "Saint-petersburg, Russia",
            articles: 3
        )
        
        let members = [alex, georguy, dmitry, albert]
        
        //сохраняем наши объекты в хранилище
        let realmInstance = try! Realm()
        try! realmInstance.write {
            for specialty in specialties {
                realmInstance.add(specialty)
            }
            for member in members {
                realmInstance.add(member)
            }
        }
        
        //помечаем в Defaults что БД была установлена
        UserDefaults.standard.set(
            true,
            forKey: "db_install"
        )
    }

Осталось внести пару изменений в точку входа делегата:


func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        if !UserDefaults.standard.bool(forKey: "db_install") {
           self.loadDatabase()
        }
        
        return true
    }

Готово, при первом запуске приложения — функция loadDatabase() установит данные для нашего приложения.

Пишем табличный контроллер

Думаю, тут уже всем все знакомо, мы просто подгружаем данные в контроллер и отображаем их переопределяя метод tableView:


import UIKit
import RealmSwift

class UserTableViewController: UITableViewController {
    //наш data source
    var users = [User]()

    override func viewDidLoad() {
        super.viewDidLoad()
        self.loadUsersData()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    override func tableView(
        _ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.users.count
    }

    //отдаем данные из data source в UI
    override func tableView(
        _ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        let user = self.users[indexPath.row]
        
        let cell = Bundle.main.loadNibNamed(
                "UserTableViewCell",
                owner: nil,
                options: nil
            )?.first as! UserTableViewCell
        
        cell.nameLabel.text = user.name
        cell.articlesLabel.text = "\(user.articles) articles"
        cell.specialtyLabel.text = { () -> String in
            return "\(user.specialties.map{ $0.name }.joined(separator: ", ")) engineer"
        }()
        
        return cell
    }
    
    override func tableView(
        _ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return CGFloat(68)
    }
    
    //производим транзакцию на наш UserViewController
    override func tableView(
        _ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        self.performSegue(withIdentifier: "toUserDetailSegue", sender: self)
    }
    
    //перед переходом на UserViewController устанавливаем ему индекс пользователя, соответствующего нажатой ячейке
    override func prepare(
        for segue: UIStoryboardSegue, sender: Any?) {
        let destinationViewController = segue.destination as! UserViewController
        destinationViewController.userIndex = self.tableView?.indexPathForSelectedRow?.row
    }
    
    //загружаем данные из Realm в наш data source
    private func loadUsersData() {
        let realmInstance = try! Realm()
        var users = [User]()
        for user in realmInstance.objects(User.self) {
            users.append(user)
        }
        self.users = users
    }
}

Пишем detail контроллер

Тут все еще проще, при переходе UserTableViewContoller установил нам индекс пользователя, соотвествующий ячейке таблицы по которой было произведено нажатие, наша задача найти пользователя с таким id в Realm и отобразить его данные в UI:


import UIKit
import RealmSwift

class UserViewController: UIViewController {
    
    @IBOutlet weak var nameLabel: UILabel!
    @IBOutlet weak var specialtyLabel: UILabel!
    @IBOutlet weak var aboutTextView: UITextView!

    //индекс пользователя
    public var userIndex: Int!
    //наш data source
    private var user: User?

    override func viewDidLoad() {
        super.viewDidLoad()
        self.setupAboutTextView()
        self.loadUserData()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
    
    private func setupAboutTextView() {
        self.aboutTextView.layer.cornerRadius = 5
    }

    //запрос пользователя из Realm и установка data source
    private func loadUserData() {
        let realmInstance = try! Realm()
        self.user = realmInstance.objects(User.self)
            .filter("id == \(self.userIndex!)").first
        self.updateViewData()
    }
    
    //обновление данных на View
    private func updateViewData() {
        if let user = self.user {
            self.nameLabel.text = user.name
            self.aboutTextView.text = user.about
            self.specialtyLabel.text = { () -> String in
                return "\(user.specialties.map{ $0.name }.joined(separator: ", ")) engineer"
            }()
        }
    }
}

Формально, это все что нужно сделать, для того что-бы увидеть результат, давай-те запускаться:

 

UserTableViewController

 

UserViewController

 

Заключение

Сегодня мы рассмотрели основы взаимодействия с Realm, в следующей статье мы будем дорабатывать это проект (github.com/hol0d/CBRealmDatabase)  добавляя к нему новые функциональные возможности, не забывайте подписываться на нас в соц. сетях и давать фидбэк, всем пока.

 

Автор снимка: @madeawkward

Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.