0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Macアプリ初心者:Realm で更新されたコレクションに対して自動でUIを描画

Posted at

以前作成したプロジェクトにRealmを使ってみましたが、値が更新された時にUIも連動して更新されるようにしてみました。

完成イメージ

3秒に1回 Realm のデータを更新してます。
完成イメージ

環境

  • macOS Mojave:10.14.6
  • Xcode:10.3

データの更新イメージ

Kobito.c6K0zH.png

デバッグ用にデータ更新処理を作成

登録されている情報に対してランダムに株価の変更を行います。
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
            }
        }
    }
}
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?