Сегодня мы научимся быстро и просто управлять данными в своем приложении, с помощью 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.