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に固定してしまっています。複数の折れ線グラフも楽々。
動作してるところ
感想
iPad上のSwift Playgroundsで結構しっかりしたプログラム組めるのはすごいなと思いました。電車内やカフェで殴り書きするのにちょうどいい。
でも、ガチなソフト組むとなると、StoryBoardが欲しくなるしエディタとしては機能が限定されているので、MacとXCodeが必要になりますね。
コード
本家
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()