1
2

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 を使って株価のウォッチリストを作る(まとめ)

Last updated at Posted at 2019-11-06

今まで作成してきたサンプルのまとめとして Realm の機能を作って株価(※)のウォッチリストを作ります。
※ 実際のデータは使わないで、データをデバッグ用に仮で作ります。

細かい解説は以下の記事を参考にしてください。

一部リファクタリングしてるのでソースコードなどでわかりづらい点あるかもです。。。

完成イメージ

完成イメージ

環境

  • macOS Mojave:10.14.6
  • Xcode:11.1

全体の構成

View

Watchリスト用と検索用のViewControllerを用意して、Watchリスト用のViewControllerはWindowControllerから初期表示用のViewControllerとして設定し、検索用のViewControllerは「+」ボタンがクリックされた場合に表示されるViewControllerとして設定しています。
Kobito.CmJ0aq.png

Kobito.Y9ofv2.png

Kobito.kOpmwL.png

データ構造

  • 企業名
  • 価格
  • ステータス(株価が上がった、下がった、変わらない)
  • Watchリストの表示/非表示
  • データ作成日時
  • データ更新日時

ソースコード

AppDelegate.swift
import Cocoa

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

    /// update stock info
    private var timer: Timer? = nil

    func applicationWillFinishLaunching(_ notification: Notification) {
        
        // debug
        StockInfo.debugInitData()
        self.timer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true, block: { (timer) in
            StockInfo.debugRandomUpdate()
        })
    }

    func applicationDidFinishLaunching(_ aNotification: Notification) {
        // Insert code here to initialize your application
    }

    func applicationWillTerminate(_ aNotification: Notification) {
        // Insert code here to tear down your application
    }


}
WatchListViewController.swift
import Cocoa
import RealmSwift

class WatchListViewController: 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
            
            let name = String(describing: CustomNSTableCellView.self)
            if let nib = NSNib(nibNamed: name, bundle: Bundle(for: type(of: self))) {
                nib.instantiate(withOwner: self, topLevelObjects: nil)
                self.tableView.register(nib, forIdentifier: NSUserInterfaceItemIdentifier.init("MyView"))
            }
        }
    }
    @IBOutlet private weak var countTextField: NSTextField!
    
    
    // MARK: - private variables
    private let stockValues = StockInfo.objects(isWatchList: true)
    private lazy var notificationToken = StockInfo.notificationToken(stockValues: self.stockValues, tableView: self.tableView) {
        self.updateStockValuesCount()
    }

    // 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.
        print(self.notificationToken.description)
        // 株価情報をViewに反映
        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))"
    }
    
}

extension WatchListViewController: 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]
        result.isHiddenAddArea = true
        result.isHiddenDeleteArea = false

        // Return the result
        return result
    }
}
SearchStockViewController.swift
import Cocoa

class SearchStockViewController: 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
            
            let name = String(describing: CustomNSTableCellView.self)
            if let nib = NSNib(nibNamed: name, bundle: Bundle(for: type(of: self))) {
                nib.instantiate(withOwner: self, topLevelObjects: nil)
                self.tableView.register(nib, forIdentifier: NSUserInterfaceItemIdentifier.init("MyView"))
            }
        }
    }
    @IBOutlet private weak var countTextField: NSTextField!
    
    
    // MARK: - private variables
    private var stockValues = StockInfo.objects(isWatchList: false)
    private lazy var notificationToken = StockInfo.notificationToken(stockValues: self.stockValues, tableView: self.tableView) {
        self.updateStockValuesCount()
    }
    
    // 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.
        print(self.notificationToken.description)
        // 株価情報をViewに反映
        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))"
    }
    

    // MARK: - actions
    
    @IBAction func actionSearchField(_ sender: NSSearchField) {
        self.stockValues = StockInfo.objects(isWatchList: false, searchValue: sender.stringValue)
        self.notificationToken.invalidate()
        self.notificationToken = StockInfo.notificationToken(stockValues: self.stockValues, tableView: self.tableView) {
            self.updateStockValuesCount()
        }
        self.tableView.reloadData()
        self.updateStockValuesCount()
    }
    
}

extension SearchStockViewController: 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]
        result.isHiddenAddArea = false
        result.isHiddenDeleteArea = true

        // 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!
    @IBOutlet private weak var addArea: NSView!
    @IBOutlet private weak var deleteArea: NSView!

    
    // 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
        }
    }
    
    var isHiddenDeleteArea: Bool = true {
        didSet { self.deleteArea.isHidden = self.isHiddenDeleteArea }
    }
    
    var isHiddenAddArea: Bool = true {
        didSet { self.addArea.isHidden = self.isHiddenAddArea }
    }

    // MARK: - IBAction
    @IBAction func actionDelete(_ sender: NSButton) {
        self.stockInfo?.update(isWatchList: false)
    }
    
    @IBAction func actionAdd(_ sender: NSButton) {
        self.stockInfo?.update(isWatchList: true)
    }

}
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, isWatchList: Bool) -> StockInfo {
        let stockInfo = realm.create(StockInfo.self)
        stockInfo.name = name
        stockInfo.price = price
        stockInfo.stockStatus = status
        stockInfo.isWatchList = isWatchList
        
        return stockInfo
    }
    
    /// 企業名で部分一致検索(空の場合は全件取得)
    class func objects(realm: Realm = try! Realm(),isWatchList: Bool? = true, searchValue: String = "") -> Results<StockInfo> {
        var result = 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)
    }
    
    /// 監視用処理
    class func notificationToken(stockValues: Results<StockInfo>, tableView: NSTableView, completion: (() -> Void)?) -> NotificationToken {
        return stockValues.observe { [weak tableView, completion] (changes: RealmCollectionChange) in
            guard let tableView = 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)")
            }
            // 件数を更新
            completion?()
        }
    }
}

// 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, isWatchList: false)
            let _ = StockInfo.create(realm: realm, name: "○×水産", price: 12345, status: .down, isWatchList: false)
            let _ = StockInfo.create(realm: realm, name: "株式会社□○", price: 345, status: .flat, isWatchList: false)
            let _ = StockInfo.create(realm: realm, name: "×△ホールディングス", price: 321, status: .up, isWatchList: false)
            let _ = StockInfo.create(realm: realm, name: "ABC BANK", price: 20, status: .down, isWatchList: false)
            let _ = StockInfo.create(realm: realm, name: "▼○重工", price: 98000, status: .up, isWatchList: false)
            
            // サンプル用に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, isWatchList: false)
            }
        }
    }
    
    /// データ更新
    class func debugRandomUpdate() {
        let realm = try! Realm()
        try! realm.write {
            let stockValues = StockInfo.objects(realm: realm, isWatchList: nil)
            print("update stock values \(stockValues.count)")
            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 <= 1 {
                    value = 2
                }
                switch status {
                case .down:
                    stockValue.price += -Int.random(in: 1..<value)
                    if stockValue.price < 0 {
                        stockValue.price = 1
                    }
                    stockValue.updatedAt = Date()
                case .up:
                    stockValue.price += Int.random(in: 1..<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
            }
        }
    }
}
1
2
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
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?