Lynx-EyEDの電音鍵盤 新館

制御とか数学とか駄文とか

Swift Playgrounds(Swift4.0)で横スクロールするグラフを描く

単純な横スクロールのグラフだけでいい

iOSプログラミングのお仕事がちょいちょい来てるのですが、ブランクが長すぎてiPad Proでリハビリをする毎日です。Cordova/PhoneGapも最近聞かないなぁ。
最近はReactがアツいとかなんとか聞くのですが。

iOS11になってから今まで順調に動いていたPythonista3でグラフをプロットしようとすると何かの契機にアプリごとクラッシュしてしまうようになりました。
自分がmatplotlibの扱いがよくわかってないのもあるのですが…そのうち解決するでしょうということで。

Swiftでグラフを描くのはライブラリを使うのが良いようです。たとえばGitHub - core-plot/core-plot: Core Plot source code and example applications
今回は簡単な折れ線グラフが書きたいだけなのでこのライブラリの学習コストが見合わないなと思いいろいろ探していたところ、ありました。

Swiftでシンプルなグラフ描写する ... | FiNC Developers Blog

ここで紹介されているものをお借りしました。ありがとうございます。
プロット数が多くなると、UIViewサイズに合わせてx軸のマージンがどんどん小さくなるので、UIScrollViewを使って横スクロールしてしまうようにしました。またSwift 2.0の記述だったので4.0に対応するように一部直しています。
なお、プロット間のx軸の距離は80に固定してしまっています。複数の折れ線グラフも楽々。


f:id:Lynx-EyED:20171106153539p:plain

動作してるところ


Swift ScrollView plot

感想

iPad上のSwift Playgroundsで結構しっかりしたプログラム組めるのはすごいなと思いました。電車内やカフェで殴り書きするのにちょうどいい。
でも、ガチなソフト組むとなると、StoryBoardが欲しくなるしエディタとしては機能が限定されているので、MacXCodeが必要になりますね。

コード

本家
Swiftでシンプルなグラフ描写する ... | FiNC Developers Blog
のコードと比較したほうがいいかも。急いででっち上げてしまったので…

//
//  ViewController.swift
//  SimpleGraph
//
//  Created by Yoshimi Kondo on 2015/11/19.
//  Copyright © 2015年 yoshimikeisui. All rights reserved.
//
//  fixed to use UIScrollView by lynxeyed 2017/11/05


import PlaygroundSupport
import UIKit

public class ViewController: UIViewController{
    let wd = 500
    let ht = 630
override public func viewDidLoad() {
        super.viewDidLoad()
        drawLineGraph()
    //drawBarGraph()
    }
    
    func drawLineGraph() {
        let stroke1 = LineStroke(graphPoints: [1, 3, 6, 4, 9, 12, 4, 11, 15,5,1,5,10,5])
        let stroke2 = LineStroke(graphPoints: [16,3, nil, 6, 4, 8])
        let stroke3 = LineStroke(graphPoints: [24,5, 5, 6, 4, 5])
        
        stroke1.color = #colorLiteral(red: 0.239215686917305, green: 0.674509823322296, blue: 0.968627452850342, alpha: 1.0)
        stroke2.color = #colorLiteral(red: 0.854901969432831, green: 0.250980406999588, blue: 0.47843137383461, alpha: 1.0)
        stroke3.color = #colorLiteral(red: 0.584313750267029, green: 0.823529422283173, blue: 0.419607847929001, alpha: 1.0)
        let lineGraphView = UIScrollView()
        lineGraphView.frame.size = CGSize(width: wd, height: ht)
        let graphFrame = LineStrokeGraphFrame(strokes: [stroke1, stroke2, stroke3])
        lineGraphView.isPagingEnabled = false
        lineGraphView.backgroundColor = UIColor.darkGray
        view.addSubview(lineGraphView)
        lineGraphView.addSubview(graphFrame)
        let fw = graphFrame.xAxisPointsCount * Int(graphFrame.xAxisMargin)
        lineGraphView.contentSize = CGSize(width:fw, height:ht) //スクロールビュー内のコンテンツサイズ設定
       
        
    }
    
    func drawBarGraph() {
        let bars = BarStroke(graphPoints: [nil, 1, 3, 1, 4, 9, 12, 4])
        bars.color = UIColor.green
        
        let barFrame = LineStrokeGraphFrame(strokes: [bars])
        
        let barGraphView = UIView(frame: CGRect(x: 0, y: 240, width: view.frame.width, height: 200))
        barGraphView.backgroundColor = UIColor.gray
        barGraphView.addSubview(barFrame)
        
        view.addSubview(barGraphView)
    }
    
    override public func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    
    
}

protocol GraphObject {
    var view: UIView { get }
}

extension GraphObject {
    var view: UIView {
        return self as! UIView
    }
    
    func drawLine(from: CGPoint, to: CGPoint) {
        let linePath = UIBezierPath()
        
        linePath.move(to: from)
        linePath.addLine(to: to)
        linePath.lineWidth = 0.5
        
        let color = UIColor.white
        color.setStroke()
        linePath.stroke()
        linePath.close()
    }
}

protocol GraphFrame: GraphObject {
    var strokes: [GraphStroke] { get }
}

extension GraphFrame {
    // 保持しているstrokesの中で最大値
    var yAxisMax: CGFloat {
        return strokes.map{ $0.graphPoints }.flatMap{ $0 }.flatMap{ $0 }.max()!
    }
    
    // 保持しているstrokesの中でいちばん長い配列の長さ
    var xAxisPointsCount: Int {
        return strokes.map{ $0.graphPoints.count }.max()!
    }
    
    // X軸の点と点の幅
    var xAxisMargin: CGFloat {
        //return view.frame.size /CGFloat(xAxisPointsCount)
        return 80.0 //    }
}

class LineStrokeGraphFrame: UIView, GraphFrame {
    
    var strokes = [GraphStroke]()
    convenience init(strokes: [GraphStroke]) {
        self.init()
        self.strokes = strokes
    }
    
    override func didMoveToSuperview() {
        if self.superview == nil { return }
        //self.frame.size = self.superview!.frame.size
        let uisview = ViewController()
        
        //var frameWidth = gs.
        self.frame = CGRect(x: 0, y: 0, width: 3000, height: uisview.ht )
        self.view.backgroundColor = UIColor.clear
        
        strokeLines()
    }
    
    func strokeLines() {
        for stroke in strokes {
            self.addSubview(stroke as! UIView)
        }
    }
    
    override func draw(_ rect: CGRect) {
        drawTopLine()
        drawBottomLine()
        drawVerticalLines()
    }
    
    func drawTopLine() {
        self.drawLine(
            from: CGPoint(x: 0, y: frame.height),
            to: CGPoint(x: frame.width, y: frame.height)
        )
    }
    
    func drawBottomLine() {
        self.drawLine(
            from: CGPoint(x: 0, y: 0),
            to: CGPoint(x: frame.width, y: 0)
        )
    }
    
    func drawVerticalLines() {
        for i in 1..<xAxisPointsCount {
            let x = xAxisMargin*CGFloat(i)
            self.drawLine(
                from: CGPoint(x: x, y: 0),
                to: CGPoint(x: x, y: frame.height)
            )
        }
    }
}


protocol GraphStroke: GraphObject {
    var graphPoints: [CGFloat?] { get }
}

extension GraphStroke {
    var graphFrame: GraphFrame? {
        return ((self as! UIView).superview as? GraphFrame)
    }
    
    var graphHeight: CGFloat {
        return view.frame.height
    }
    
    var xAxisMargin: CGFloat {
        return graphFrame!.xAxisMargin
    }
    
    var yAxisMax: CGFloat {
        return graphFrame!.yAxisMax
    }
    
    // indexからX座標を取る
    func getXPoint(index: Int) -> CGFloat {
        return CGFloat(index) * xAxisMargin
    }
    
    // 値からY座標を取る
    func getYPoint(yOrigin: CGFloat) -> CGFloat {
        let y: CGFloat = yOrigin/yAxisMax * graphHeight
        return graphHeight - y
    }
}


class LineStroke: UIView, GraphStroke {
    var graphPoints = [CGFloat?]()
    var color = UIColor.white
    
    convenience init(graphPoints: [CGFloat?]) {
        self.init()
        self.graphPoints = graphPoints
    }
    
    override func didMoveToSuperview() {
        if self.graphFrame == nil { return }
        self.frame.size = self.graphFrame!.view.frame.size
        self.view.backgroundColor = UIColor.clear
    }
    
    override func draw(_ rect: CGRect) {
        let graphPath = UIBezierPath()
        
        graphPath.move(
            to: CGPoint(x: getXPoint(index: 0), y: getYPoint(yOrigin: graphPoints[0] ?? 0))
        )
        
        for graphPoint in graphPoints.enumerated() {
            if graphPoint.element == nil { continue }
            let nextPoint = CGPoint(x: getXPoint(index: graphPoint.offset),
                                    y: getYPoint(yOrigin: graphPoint.element!))
            graphPath.addLine(to: nextPoint)
        }
        
        graphPath.lineWidth = 5.0
        color.setStroke()
        graphPath.stroke()
        graphPath.close()
    }
}

class BarStroke: UIView, GraphStroke {
    var graphPoints = [CGFloat?]()
    var color = UIColor.white
    
    convenience init(graphPoints: [CGFloat?]) {
        self.init()
        self.graphPoints = graphPoints
    }
    
    override func didMoveToSuperview() {
        if self.graphFrame == nil { return }
        self.frame.size = self.graphFrame!.view.frame.size
        self.view.backgroundColor = UIColor.clear
    }
    
    override func draw(_ rect: CGRect) {
        for graphPoint in graphPoints.enumerated() {
            let graphPath = UIBezierPath()
            
            let xPoint = getXPoint(index: graphPoint.offset)
            graphPath.move(
                to: CGPoint(x: xPoint, y: getYPoint(yOrigin: 0))
            )
            
            if graphPoint.element == nil { continue }
            let nextPoint = CGPoint(x: xPoint, y: getYPoint(yOrigin: graphPoint.element!))
            graphPath.addLine(to: nextPoint)
            
            graphPath.lineWidth = 30
            color.setStroke()
            graphPath.stroke()
            graphPath.close()
        }
    }
}

PlaygroundPage.current.liveView = ViewController()