RealmSwift 사용해보기 - 걸음수 저장하기
Realm 은 모바일에 최적화된 오픈소스 데이터베이스다.
CoreData보다 가볍고 실시간 데이터처리 + 간단한 모델을 사용할때 더 적합하다고하여
프로젝트에 적용하고자 공부해보고자 한다.
https://github.com/realm/realm-swift
GitHub - realm/realm-swift: Realm is a mobile database: a replacement for Core Data & SQLite
Realm is a mobile database: a replacement for Core Data & SQLite - realm/realm-swift
github.com
공식 사이트를 통해 알아보았다.
Realm의 장점
1. 직관적이고 객체 지향적이며 ORM이 필요없다.
2. 가볍고, 메모리, 디스크 공간, 배터리를 효율적으로 사용한다.
3. 오프라인을 위해 설계됨
4. 사용자, 기기 및 백엔드에서 실시간으로 데이터를 동기화하는걸 간편하게 해준다.
설치
SPM을 사용했다.
프로젝트 선택 - Package Dependecies - 깃허브주소 붙여넣기 로 설치해주었다.
객체 지향적인 모델
// Define your models like regular Swift classes
class Dog: Object {
@Persisted var name: String
@Persisted var age: Int
}
class Person: Object {
@Persisted(primaryKey: true) var _id: String
@Persisted var name: String
@Persisted var age: Int
// Create relationships by pointing an Object field to another Class
@Persisted var dogs: List<Dog>
}
// Use them like regular Swift objects
let dog = Dog()
dog.name = "Rex"
dog.age = 1
print("name of dog: \(dog.name)")
// Get the default Realm
let realm = try! Realm()
// Persist your data easily with a write transaction
try! realm.write {
realm.add(dog)
}
여기서 @Persisted는 데이터를 영구 저장할 속성을 표시하는 프로퍼티 래퍼다.
해당 래퍼없이 var name : String으로 정의하면 Realm에 저장되지 않는다.
라이브 객체: 반응형 앱 빌드
// Open the default realm.
let realm = try! Realm()
var token: NotificationToken?
let dog = Dog()
dog.name = "Max"
// Create a dog in the realm.
try! realm.write {
realm.add(dog)
}
// Set up the listener & observe object notifications.
token = dog.observe { change in
switch change {
case .change(let properties):
for property in properties {
print("Property '\(property.name)' changed to '\(property.newValue!)'");
}
case .error(let error):
print("An error occurred: (error)")
case .deleted:
print("The object was deleted.")
}
}
// Update the dog's name to see the effect.
try! realm.write {
dog.name = "Wolfie"
}
observe 메서드가 dog객체의 모든 변경사항을 감시하고
변경이 발생할 때마다 클로저가 실행된다.
그래서 아래의 dog.name을 Wolfie로 바꾸면
Property 'name' changed to 'Wolfie'
이렇게 출력된다.
추가로 관찰이 필요없어졌을떄는 invalidate를 통해 메모리 누수를 방지했어야했다.
// 관찰 중단하기 (중요!)
token?.invalidate()
하지만 @ObservedResults가 나온 이후로는 간단하게 관리가 가능해졌다
struct ContactsView: View {
@ObservedResults(Person.self) var persons
var body: some View {
List {
ForEach(persons) { person in
Text(person.name)
}
.onMove(perform: $persons.move)
.onDelete(perform: $persons.remove)
}.navigationBarItems(trailing:
Button("Add") {
$persons.append(Person())
}
)
}
}
삭제나 정렬또한 내장 메서드 호출로 간단히 구현 가능하다.
암호화
// Generate a random encryption key
var key = Data(count: 64)
_ = key.withUnsafeMutableBytes { (pointer: UnsafeMutableRawBufferPointer) in
guard let baseAddress = pointer.baseAddress else {
fatalError("Failed to obtain base address")
}
SecRandomCopyBytes(kSecRandomDefault, 64, baseAddress)
}
// Add the encryption key to the config and open the realm
let config = Realm.Configuration(encryptionKey: key)
let realm = try Realm(configuration: config)
// Use the Realm as normal
let dogs = realm.objects(Dog.self).filter("name contains 'Fido'")
전송 및 저장 중에 데이터를 암호화할 수도 있다고한다.
하지만 토큰의 경우 iOS에서 가장 안전한 KeyChain과 UserDefaults를 사용하고
이번 개인 프로젝트의 경우 전송과정이 없기때문에 깊이 공부하지않고 넘어가겠다.
그럼 한번 만들어보자!
실시간 걸음수는 CPU를 비교적 덜 차지하는 CMPedometer 공식문서를 보고 만들었다.
https://developer.apple.com/documentation/coremotion/cmpedometer
CMPedometer | Apple Developer Documentation
An object for fetching the system-generated live walking data.
developer.apple.com
걸음수 측정 클래스 내부 함수에서 RealmSwift객체 업데이트 로직을 작성하고
뷰에서는 @ObservedObject 래퍼를 활용해서 뷰를 업데이트 하는 구성으로 만들어보았습니다.
객체 모델링
import RealmSwift
import Foundation
class DailySteps : Object,ObjectKeyIdentifiable {
@Persisted(primaryKey: true) private var _id: String = ""
@Persisted var date: Date
@Persisted var steps: Int = 0
@Persisted var snowmanName: String
@Persisted var measurementStartTime: Date
@Persisted var targetSteps: Int // 목표 걸음 수
@Persisted var daysSpent: Int = 0 // 만드는데 걸린 날짜
override init() {
super.init()
self.date = Calendar.current.startOfDay(for: Date())
self.snowmanName = DailySteps.generateSnowmanName()
self._id = self.snowmanName
self.measurementStartTime = Date()
self.targetSteps = Self.generateRandomTarget() // 랜덤 목표 설정
}
걸음수 확인 클래스
class StepCounter: ObservableObject {
private let pedometer = CMPedometer()
private let realm: Realm
Realm.Configuration.defaultConfiguration = config
realm = try! Realm()
private func updateLatestSteps(_ newSteps: Int) {
try? realm.write {
if let latestSteps = realm.objects(DailySteps.self).sorted(byKeyPath: "date", ascending: false).first {
latestSteps.steps = newSteps
}
}
}
func startUpdates(from: Date, withHandler: CMPedometerHandler)
최근 보행자 관련 데이터를 앱으로 전송하기 시작합니다.
해당 함수를 사용해서 걸음수를 측정했는데 처음에는 해당일로 계산했다가.
같은날 두개 이상의 눈사람이 만들어졌을때 값이 중복으로 할당되어서 현재시간으로 바꾸었다.
하지만 현재시간으로 바꾸니 앱을 껏다 켤떄마다 초기화가 돼
각 눈사람 별로 측정시작시간을 저장해 해당 시간을 기준으로 걸음수를 가져왔다.
그래서 컬럼이 하나 추가되어 마이그레이션이 필요해졌다.
마이그레이션
init() {
// Realm 마이그레이션 설정
let config = Realm.Configuration(
schemaVersion: 2, // 스키마 버전 증가
migrationBlock: { migration, oldSchemaVersion in
if oldSchemaVersion < 1 {
migration.enumerateObjects(ofType: DailySteps.className()) { oldObject, newObject in
newObject!["measurementStartTime"] = Date()
}
}
if oldSchemaVersion < 2 {
migration.enumerateObjects(ofType: DailySteps.className()) { oldObject, newObject in
// 새로운 필드들에 기본값 설정
newObject!["targetSteps"] = DailySteps.generateRandomTarget()
newObject!["daysSpent"] = 0
}
}
}
)
이미 만들어진 데이터베이스에서 컬럼을 추가하면 마이그레이션을 해주어야한다.
init 부분 코드가 그 부분이다.
실시간 변경 뷰
struct WalkCountView: View {
@ObservedResults(DailySteps.self) var dailySteps
private let stepCounter = StepCounter()
var todaySteps: Int {
dailySteps.last?.steps ?? 0
}
var snowmanName: String {
dailySteps.last?.snowmanName ?? "스!노우맨"
}
var body: some View {
VStack {
Text(snowmanName)
Text("현재 걸음수 \(todaySteps)")
Button("새로운 시작") {
stepCounter.startNewCount() // 새로운 시작 함수 호출
}
.foregroundColor(.blue)
.padding()
List {
ForEach(dailySteps.sorted(by: { $0.date > $1.date })) { step in
VStack(alignment: .leading) {
Text("이름: \(step.snowmanName)")
Text("걸음수: \(step.steps)")
Text("날짜: \(step.date.formatted())")
Text("측정시간 : \(step.measurementStartTime)")
Text("최고 가속도")
Text("만드는데 걸린 날짜 : \(step.daysSpent)")
Text("목표 걸음수 : \(step.targetSteps)")
}
}
}
}
.onAppear {
stepCounter.startCounting() // 일반 시작
}
.padding()
}
}
자원 사용량
이전에 비해서 월등하게 낮아진 것을 볼 수 있다.
시연
Core Data보다 훨씬 간단하고 최적화가 잘되어있는 것 같다. ( 왜 Apple공식 라이브러리 상태는 항상... )
사용방법도 간단하고 자주 활용할 수 있는 라이브러리같다..!
실시간 걸음수도 나름 정확하게 계산되고 메모리도 많이 잡아먹지 않는 걸 보아 Unity를 굳이 도입할 필요가 없을 것 같기도하다.
내일은 걷는 가속도를 계산해서 눈이 굴러가는 이팩트를 주는 기능을 만들어봐야겠다.