以前作成したプロジェクトにRealmを使ってみましたが、値が更新された時にUIも連動して更新されるようにしてみました。
完成イメージ
環境
- macOS Mojave:10.14.6
- Xcode:10.3
データの更新イメージ
デバッグ用にデータ更新処理を作成
登録されている情報に対してランダムに株価の変更を行います。
3秒に1回最大で5%くらい更新されます。
Realm データの更新処理
StockInfo.swift
// MARK: - debug
extension StockInfo {
/// データ更新
class func debugRandomUpdate(stockValues: Results<StockInfo>) {
let realm = try! Realm()
try! realm.write {
for stockValue in stockValues {
let status = StockStatus(rawValue: Int.random(in: 0..<3))!
stockValue.stockStatus = status
var value = Int(Double(stockValue.price) * 0.2)
if value == 0 {
value = 1
}
switch status {
case .down:
stockValue.price += -Int.random(in: 0..<value)
if stockValue.price < 0 {
stockValue.price = 1
}
stockValue.updatedAt = Date()
case .up:
stockValue.price += Int.random(in: 0..<value)
stockValue.updatedAt = Date()
case .flat:
break
}
}
}
}
}
データ更新用のタイマーを作成
これで Realm に登録されているデータが3秒に1回更新されるようになります。
ViewController.swift
class ViewController: NSViewController {
// MARK: - private variables
private var timer: Timer? = nil
override func viewDidLoad() {
super.viewDidLoad()
self.timer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true, block: { (timer) in
StockInfo.debugRandomUpdate(stockValues: self.stockValues)
})
}
}
deinit {
self.timer?.invalidate()
}
自動でUIを描画する処理を作成
NotificationToken を利用して、Realm オブジェクトのコレクションを監視
削除、登録、更新されたインデックスが通知されるため、その該当行に対して適切な処理を実行します。
アニメーションは気に入ったものを適当に適用してください。
また、件数と最終更新日時を表示するように修正しています。
ViewController.swift
class ViewController: NSViewController {
// MARK: - private variables
private var stockValues = StockInfo.objects()
private var notificationToken: NotificationToken? = nil
override func viewDidLoad() {
super.viewDidLoad()
// 株価情報をViewに反映
self.updateStockValuesCount()
// Resultsの通知を監視します
notificationToken = self.stockValues.observe { [weak self] (changes: RealmCollectionChange) in
guard let tableView = self?.tableView else { return }
switch changes {
case .initial:
// Results are now populated and can be accessed without blocking the UI
tableView.reloadData()
case .update(let collectionType, let deletions, let insertions, let modifications):
// Resultsに変更があったので、UITableViewに変更を適用します
print(collectionType)
tableView.beginUpdates()
tableView.insertRows(at: IndexSet(insertions), withAnimation: .effectFade)
tableView.removeRows(at: IndexSet(deletions), withAnimation: .effectFade)
tableView.reloadData(forRowIndexes: IndexSet(modifications), columnIndexes: IndexSet(integer: 0))
tableView.endUpdates()
case .error(let error):
// バックグラウンドのワーカースレッドがRealmファイルを開く際にエラーが起きました
fatalError("\(error)")
}
// 件数を更新
self?.updateStockValuesCount()
}
}
deinit {
self.notificationToken?.invalidate()
}
// MARK: - private func
private func updateStockValuesCount() {
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .decimal
let commaCount = numberFormatter.string(from: NSNumber(value: self.stockValues.count))!
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "HH:mm:ss"
let lastUpdatedString = dateFormatter.string(from: Date())
self.countTextField.stringValue = "\(commaCount)件 (last updated:\(lastUpdatedString))"
}
ソースコード
展開して確認
ViewController.swift
import Cocoa
import RealmSwift
class ViewController: NSViewController {
// MARK: - IBOutlet
@IBOutlet private weak var tableView: NSTableView! {
didSet {
self.tableView.dataSource = self
self.tableView.delegate = self
self.tableView.headerView = nil
self.tableView.rowHeight = 88
}
}
@IBOutlet private weak var countTextField: NSTextField!
// MARK: - private variables
private var stockValues = StockInfo.objects()
private var timer: Timer? = nil
private var notificationToken: NotificationToken? = nil
// MARK: - override variables
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
// MARK: - override func
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
// デバッグ用にデータ作成
StockInfo.debugInitData()
// 株価情報をViewに反映
self.updateStockValuesCount()
// Resultsの通知を監視します
notificationToken = self.stockValues.observe { [weak self] (changes: RealmCollectionChange) in
guard let tableView = self?.tableView else { return }
switch changes {
case .initial:
// Results are now populated and can be accessed without blocking the UI
tableView.reloadData()
case .update(let collectionType, let deletions, let insertions, let modifications):
// Resultsに変更があったので、UITableViewに変更を適用します
print(collectionType)
tableView.beginUpdates()
tableView.insertRows(at: IndexSet(insertions), withAnimation: .effectFade)
tableView.removeRows(at: IndexSet(deletions), withAnimation: .effectFade)
tableView.reloadData(forRowIndexes: IndexSet(modifications), columnIndexes: IndexSet(integer: 0))
tableView.endUpdates()
case .error(let error):
// バックグラウンドのワーカースレッドがRealmファイルを開く際にエラーが起きました
fatalError("\(error)")
}
// 件数を更新
self?.updateStockValuesCount()
}
self.timer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true, block: { (timer) in
StockInfo.debugRandomUpdate(stockValues: self.stockValues)
})
}
deinit {
self.notificationToken?.invalidate()
self.timer?.invalidate()
}
// MARK: - private func
private func updateStockValuesCount() {
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .decimal
let commaCount = numberFormatter.string(from: NSNumber(value: self.stockValues.count))!
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "HH:mm:ss"
let lastUpdatedString = dateFormatter.string(from: Date())
self.countTextField.stringValue = "\(commaCount)件 (last updated:\(lastUpdatedString))"
}
// MARK: - actions
@IBAction func actionSearchField(_ sender: NSSearchField) {
self.stockValues = StockInfo.objects(searchValue: sender.stringValue)
self.tableView.reloadData()
self.updateStockValuesCount()
}
}
extension ViewController: NSTableViewDataSource, NSTableViewDelegate {
// MARK: - NSTableViewDataSource
func numberOfRows(in tableView: NSTableView) -> Int {
return self.stockValues.count
}
// MARK: - NSTableViewDelegate
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
let result = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "MyView"), owner: self) as! CustomNSTableCellView
// Set the stringValue of the cell's text field to the nameArray value at row
let stock = self.stockValues[row]
result.image = stock.stockStatus.image
result.name = stock.name
result.color = stock.stockStatus.color
result.price = stock.price
// Return the result
return result
}
}
CustomNSTableCellView.swift
import Cocoa
class CustomNSTableCellView: NSTableCellView {
// MARK: - IBOutlet
@IBOutlet private weak var stockImage: NSImageView!
@IBOutlet private weak var companyName: NSTextField!
@IBOutlet private weak var stockPrice: NSTextField!
// MARK: - public variables
var name: String {
get { return self.companyName.stringValue }
set { self.companyName.stringValue = newValue }
}
var image: NSImage? {
get { return self.stockImage.image }
set { self.stockImage.image = newValue }
}
var color: NSColor? {
get {
return self.companyName.textColor
}
set {
self.companyName.textColor = newValue
self.stockImage.contentTintColor = newValue
}
}
var price: Int = -1 {
didSet {
guard self.price > 0 else {
self.stockPrice.stringValue = "-"
return
}
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
let commaString = formatter.string(from: NSNumber(value: self.price))!
self.stockPrice.stringValue = commaString
}
}
}
StockInfo.swift
import Cocoa
import RealmSwift
class StockInfo: Object {
@objc dynamic var name = ""
@objc dynamic var price = -1
@objc dynamic var status = StockStatus.flat.rawValue
@objc dynamic var createdAt = Date()
@objc dynamic var updatedAt = Date()
}
extension StockInfo {
/// 状態をEnum形式で返却
var stockStatus: StockStatus {
get {
return StockStatus(rawValue: self.status) ?? StockStatus.flat
}
set {
self.status = newValue.rawValue
}
}
/// StockInfo オブジェクトを作成
class func create(realm: Realm, name: String, price: Int, status: StockStatus) -> StockInfo {
let stockInfo = realm.create(StockInfo.self)
stockInfo.name = name
stockInfo.price = price
stockInfo.stockStatus = status
return stockInfo
}
/// 企業名で部分一致検索(空の場合は全件取得)
class func objects(searchValue: String = "") -> Results<StockInfo> {
var result = try! Realm().objects(StockInfo.self)
let searchValue = searchValue.trimmingCharacters(in: .whitespaces)
if searchValue != "" {
result = result.filter("name CONTAINS %@", searchValue)
}
return result.sorted(byKeyPath: "createdAt", ascending: true)
}
}
// MARK: - debug
extension StockInfo {
/// データ初期化
class func debugInitData() {
let realm = try! Realm()
try! realm.write {
// データクリア
realm.deleteAll()
// サンプルデータ作成
let _ = StockInfo.create(realm: realm, name: "○○株式会社", price: 1000, status: .up)
let _ = StockInfo.create(realm: realm, name: "○×水産", price: 12345, status: .down)
let _ = StockInfo.create(realm: realm, name: "株式会社□○", price: 345, status: .flat)
let _ = StockInfo.create(realm: realm, name: "×△ホールディングス", price: 321, status: .up)
let _ = StockInfo.create(realm: realm, name: "ABC BANK", price: 20, status: .down)
let _ = StockInfo.create(realm: realm, name: "▼○重工", price: 98000, status: .up)
// サンプル用に1,000 件データを作成
// for i in 1...1_000 {
// let status = StockStatus.init(rawValue: Int.random(in: 0..<3))!
// let price = Int.random(in: -1..<10_000_000)
// let _ = StockInfo.create(realm: realm, name: "○△×株式会社 \(i)", price: price, status: status)
// }
}
}
/// データ更新
class func debugRandomUpdate(stockValues: Results<StockInfo>) {
let realm = try! Realm()
try! realm.write {
for stockValue in stockValues {
let status = StockStatus(rawValue: Int.random(in: 0..<3))!
stockValue.stockStatus = status
var value = Int(Double(stockValue.price) * 0.05)
if value == 0 {
value = 1
}
switch status {
case .down:
stockValue.price += -Int.random(in: 0..<value)
if stockValue.price < 0 {
stockValue.price = 1
}
stockValue.updatedAt = Date()
case .up:
stockValue.price += Int.random(in: 0..<value)
stockValue.updatedAt = Date()
case .flat:
break
}
}
}
}
}
StockStatus.swift
import Cocoa
enum StockStatus: Int {
case flat = 0
case up
case down
/// 状態ごとのImageオブジェクト
var image: NSImage {
get {
switch self {
case .flat:
return NSImage(named: NSImage.Name("flat"))!
case .up:
return NSImage(named: NSImage.Name("up"))!
case .down:
return NSImage(named: NSImage.Name("down"))!
}
}
}
/// 状態ごとのカラーオブジェクト
var color: NSColor {
get {
switch self {
case .flat:
return .gray
case .up:
return .green
case .down:
return .red
}
}
}
}