前回作成したプロジェクトではRealmで更新された情報を自動的にアップデートするための処理を実装してみましたが、今回はにRealmを使ってみましたが、値が更新された時にUIも連動して更新されるようにしてみました。
Watchリスト風にしてみました。次回は追加機能を作ります。
完成イメージ
環境
- macOS Mojave:10.14.6
- Xcode:11.0
データの更新イメージ
Watchリスト風にするために、前回作成した時からデータ構造を変更しています。
株価情報をマスターデータにしてWatchリストへの表示/非表示をフラグで管理します。
データクラスの変更
データ構造を変更
Watchリストの表示/非表示をフラグとして、isWatchList
を追加しています。
StockInfo.swift
class StockInfo: Object {
@objc dynamic var name = ""
@objc dynamic var price = -1
@objc dynamic var status = StockStatus.flat.rawValue
@objc dynamic var isWatchList = true
@objc dynamic var createdAt = Date()
@objc dynamic var updatedAt = Date()
}
データ構造変化に伴い処理の追加と変更
isWatchList
の更新用の関数を追加
StockInfo.swift
extension StockInfo {
/// watch list の表示/非表示を切り替える
func update(isWatchList: Bool) {
let realm = try! Realm()
try! realm.write {
self.isWatchList = isWatchList
self.updatedAt = Date()
}
}
検索用の関数にWatchリストの表示/非表示をフラグを指定できるように修正
StockInfo.swift
/// 企業名で部分一致検索(空の場合は全件取得)
class func objects(isWatchList: Bool? = true, searchValue: String = "") -> Results<StockInfo> {
var result = try! Realm().objects(StockInfo.self)
let searchValue = searchValue.trimmingCharacters(in: .whitespaces)
if let isWatchList = isWatchList {
result = result.filter("isWatchList == %@", isWatchList)
}
if searchValue != "" {
result = result.filter("name CONTAINS %@", searchValue)
}
return result.sorted(byKeyPath: "createdAt", ascending: true)
}
}
Watchリストからの削除用UIを作成
カスタムセルのイメージ
以前に作成したセルの右側に新しくカスタムViewとNSButtonを配置してます。
×ボタンクリック時に表示フラグをオフ
以前のプロジェクトからリファクタリングして、株価情報Objectをカスタムセルに渡して株価情報の描画と更新処理を行うようにしてます。
データオブジェクトをUIクラスで保持するのはどうなんだって話もありますが、わかりやすいのでこんな感じで作ってしまってます。
CustomNSTableCellView.swift
class CustomNSTableCellView: NSTableCellView {
// MARK: - public variables
var stockInfo: StockInfo? {
didSet {
guard let stockInfo = self.stockInfo else {
return
}
self.companyName.stringValue = stockInfo.name
self.stockImage.image = stockInfo.stockStatus.image
self.companyName.textColor = stockInfo.stockStatus.color
self.stockImage.contentTintColor = stockInfo.stockStatus.color
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
let commaString = formatter.string(from: NSNumber(value: stockInfo.price))!
self.stockPrice.stringValue = commaString
}
}
// MARK: - IBAction
@IBAction func actionDelete(_ sender: NSButton) {
self.stockInfo?.update(isWatchList: false)
}
}
カスタムセルのI/Fを変更したのでそれに合わせて、ViewController 側も変更
ViewController.swift
extension ViewController: NSTableViewDataSource, NSTableViewDelegate {
// 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
result.stockInfo = self.stockValues[row]
// Return the result
return result
}
}
ソースコード
展開して確認
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
result.stockInfo = self.stockValues[row]
// 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 stockInfo: StockInfo? {
didSet {
guard let stockInfo = self.stockInfo else {
return
}
self.companyName.stringValue = stockInfo.name
self.stockImage.image = stockInfo.stockStatus.image
self.companyName.textColor = stockInfo.stockStatus.color
self.stockImage.contentTintColor = stockInfo.stockStatus.color
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
let commaString = formatter.string(from: NSNumber(value: stockInfo.price))!
self.stockPrice.stringValue = commaString
}
}
// MARK: - IBAction
@IBAction func actionDelete(_ sender: NSButton) {
self.stockInfo?.update(isWatchList: false)
}
}
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 isWatchList = true
@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
}
}
/// watch list の表示/非表示を切り替える
func update(isWatchList: Bool) {
let realm = try! Realm()
try! realm.write {
self.isWatchList = isWatchList
self.updatedAt = Date()
}
}
/// 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(isWatchList: Bool? = true, searchValue: String = "") -> Results<StockInfo> {
var result = try! Realm().objects(StockInfo.self)
let searchValue = searchValue.trimmingCharacters(in: .whitespaces)
if let isWatchList = isWatchList {
result = result.filter("isWatchList == %@", isWatchList)
}
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
}
}
}
}
}