しめ鯖日記

swift, iPhoneアプリ開発, ruby on rails等のTipsや入門記事書いてます

ダイクストラ法で最短経路を求めてみる

ダイクストラ法という最短経路を求めるアルゴリズムをSwiftで試してみました。
参考にさせて頂いたのはこちらの記事です。

ダイクストラ法(最短経路問題)

ダイクストラ法とは

ダイクストラ法(だいくすとらほう、英: Dijkstra’s algorithm)はグラフ理論における辺の重みが非負数の場合の単一始点最短経路問題を解くための最良優先探索によるアルゴリズムである。

Wikipediaによると上記のようなものになります。

ダイクストラ法 - Wikipedia

今回はこれをSwiftで実装してみます。

実装

まずは画面上に点を設置します。

import UIKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        (0...5).forEach { _ in
            let dot = UIView(frame: CGRect(
                x: CGFloat(arc4random_uniform(UInt32(Int32(view.frame.width)))),
                y: CGFloat(arc4random_uniform(UInt32(Int32(view.frame.height)))),
                width: 20, height: 20))
            dot.backgroundColor = UIColor.darkGray
            dot.layer.cornerRadius = dot.frame.width / 2
            view.addSubview(dot)
        }
    }
}

f:id:llcc:20170519000635p:plain

次にStartとGoalを決めます。

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        var dots: [UIView] = []
        (0...5).forEach { _ in
            let dot = UIView(frame: CGRect(
                x: CGFloat(arc4random_uniform(UInt32(Int32(view.frame.width)))),
                y: CGFloat(arc4random_uniform(UInt32(Int32(view.frame.height)))),
                width: 20, height: 20))
            dot.backgroundColor = UIColor.darkGray
            dot.layer.cornerRadius = dot.frame.width / 2
            dots.append(dot)
            view.addSubview(dot)
        }
        
        let startLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
        startLabel.text = "S"
        startLabel.textColor = UIColor.white
        startLabel.textAlignment = .center
        dots.first?.addSubview(startLabel)
        let goalLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
        goalLabel.text = "G"
        goalLabel.textColor = UIColor.white
        goalLabel.textAlignment = .center
        dots.last?.addSubview(goalLabel)
    }
}

f:id:llcc:20170519002227p:plain

続けて線同士を繋げて経路を作ろうと思います。

各点に他経路への参照を持たせるためにDotViewというクラスを作って各点をUIViewからDotViewにします。
下だと場合によっては循環参照が起こったり場合によってゴールまで着けない事もあるのですが、今回は「ダイクストラ法」の勉強という事で無視します。

import UIKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        var dots: [DotView] = [] // UIViewをDotViewに変更
        (0...5).forEach { _ in
            let dot = DotView(frame: CGRect( // UIViewをDotViewに変更
                x: CGFloat(arc4random_uniform(UInt32(Int32(view.frame.width)))),
                y: CGFloat(arc4random_uniform(UInt32(Int32(view.frame.height)))),
                width: 20, height: 20))
            dot.backgroundColor = UIColor.darkGray
            dot.layer.cornerRadius = dot.frame.width / 2
            dots.append(dot)
            view.addSubview(dot)
        }
        
        let startLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
        startLabel.text = "S"
        startLabel.textColor = UIColor.white
        startLabel.textAlignment = .center
        dots.first?.addSubview(startLabel)
        let goalLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
        goalLabel.text = "G"
        goalLabel.textColor = UIColor.white
        goalLabel.textAlignment = .center
        dots.last?.addSubview(goalLabel)
        
        // 以下今回追加
        dots.forEach { dot in
            var otherDots = dots.filter { $0 != dot }
            dot.otherDots.append(otherDots.remove(at: Int(arc4random_uniform(UInt32(otherDots.count)))))
            dot.otherDots.append(otherDots.remove(at: Int(arc4random_uniform(UInt32(otherDots.count)))))
        }
    }
}

// 以下が今回追加分
class DotView: UIView {
    var otherDots: [UIView] = []
}

上で参照を持たせたので、次は線を引きます。

import UIKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        var dots: [DotView] = []
        (0...5).forEach { _ in
            let dot = DotView(frame: CGRect(
                x: CGFloat(arc4random_uniform(UInt32(Int32(view.frame.width)))),
                y: CGFloat(arc4random_uniform(UInt32(Int32(view.frame.height)))),
                width: 20, height: 20))
            dot.backgroundColor = UIColor.darkGray
            dot.layer.cornerRadius = dot.frame.width / 2
            dots.append(dot)
            view.addSubview(dot)
        }
        
        let startLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
        startLabel.text = "S"
        startLabel.textColor = UIColor.white
        startLabel.textAlignment = .center
        dots.first?.addSubview(startLabel)
        let goalLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
        goalLabel.text = "G"
        goalLabel.textColor = UIColor.white
        goalLabel.textAlignment = .center
        dots.last?.addSubview(goalLabel)
        
        dots.forEach { dot in
            var otherDots = dots.filter { $0 != dot }
            dot.otherDots.append(otherDots.remove(at: Int(arc4random_uniform(UInt32(otherDots.count)))))
            dot.otherDots.append(otherDots.remove(at: Int(arc4random_uniform(UInt32(otherDots.count)))))
        }
        // 以下は今回追加
        dots.forEach { dot in
            dot.otherDots.forEach { otherDot in
                let lineView = LineView(start: dot.frame.origin, end: otherDot.frame.origin)
                view.addSubview(lineView)
                view.sendSubview(toBack: lineView)
            }
        }
    }
}

class DotView: UIView {
    var otherDots: [DotView] = []
}

// 以下は今回追加
class LineView: UIView {
    let start: CGPoint
    let end: CGPoint
    
    init(start: CGPoint, end: CGPoint) {
        self.start = start
        self.end   = end
        
        super.init(frame: CGRect(
            x: ([start.x, end.x].min() ?? 0) + 10,
            y: ([start.y, end.y].min() ?? 0) + 10,
            width: abs(start.x - end.x),
            height: abs(start.y - end.y)))
        
        backgroundColor = UIColor.clear
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func draw(_ rect: CGRect) {
        let path = UIBezierPath()
        path.move(to: CGPoint(x: start.x - frame.origin.x + 10, y: start.y - frame.origin.y + 10))
        path.addLine(to: CGPoint(x: end.x - frame.origin.x + 10, y: end.y - frame.origin.y + 10))
        path.lineWidth = 4.0
        UIColor.darkGray.setStroke()
        path.stroke()
    }
}

これで経路を作る事ができました。

f:id:llcc:20170520130216p:plain

ここからダイクストラ法を実装していきます。
まずはDotViewにtypeとscore(スタートからの距離)を追加して、viewDidLoadの中でtypeを設定します。

import UIKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        var dots: [DotView] = []
        (0...5).forEach { _ in
            let dot = DotView(frame: CGRect(
                x: CGFloat(arc4random_uniform(UInt32(Int32(view.frame.width)))),
                y: CGFloat(arc4random_uniform(UInt32(Int32(view.frame.height)))),
                width: 20, height: 20))
            dot.backgroundColor = UIColor.darkGray
            dot.layer.cornerRadius = dot.frame.width / 2
            dots.append(dot)
            view.addSubview(dot)
        }
        
        let startLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
        startLabel.text = "S"
        startLabel.textColor = UIColor.white
        startLabel.textAlignment = .center
        dots.first?.addSubview(startLabel)
        dots.first?.type = .start // 追加
        let goalLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
        goalLabel.text = "G"
        goalLabel.textColor = UIColor.white
        goalLabel.textAlignment = .center
        dots.last?.addSubview(goalLabel)
        dots.last?.type = .goal // 追加
        
        dots.forEach { dot in
            var otherDots = dots.filter { $0 != dot }
            dot.otherDots.append(otherDots.remove(at: Int(arc4random_uniform(UInt32(otherDots.count)))))
            dot.otherDots.append(otherDots.remove(at: Int(arc4random_uniform(UInt32(otherDots.count)))))
        }
        dots.forEach { dot in
            dot.otherDots.forEach { otherDot in
                let lineView = LineView(start: dot.frame.origin, end: otherDot.frame.origin)
                view.addSubview(lineView)
                view.sendSubview(toBack: lineView)
            }
        }
    }
}

// 今回修正
class DotView: UIView {
    enum DotType {
        case start
        case goal
        case normal
    }
    
    var otherDots: [DotView] = []
    var score: CGFloat = 0
    var type: DotType = .normal
    var routeDots: [DotView] = []
}

// 省略

経路を作ったので、どれが最短経路かを計算します。
ViewControllerを以下のように修正します。

import UIKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        var dots: [DotView] = []
        (0...5).forEach { _ in
            let dot = DotView(frame: CGRect(
                x: CGFloat(arc4random_uniform(UInt32(Int32(view.frame.width)))),
                y: CGFloat(arc4random_uniform(UInt32(Int32(view.frame.height)))),
                width: 20, height: 20))
            dot.backgroundColor = UIColor.darkGray
            dot.layer.cornerRadius = dot.frame.width / 2
            dots.append(dot)
            view.addSubview(dot)
        }
        
        let startLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
        startLabel.text = "S"
        startLabel.textColor = UIColor.white
        startLabel.textAlignment = .center
        dots.first?.addSubview(startLabel)
        dots.first?.type = .start
        let goalLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
        goalLabel.text = "G"
        goalLabel.textColor = UIColor.white
        goalLabel.textAlignment = .center
        dots.last?.addSubview(goalLabel)
        dots.last?.type = .goal
        
        dots.forEach { dot in
            var otherDots = dots.filter { $0 != dot }
            dot.otherDots.append(otherDots.remove(at: Int(arc4random_uniform(UInt32(otherDots.count)))))
            dot.otherDots.append(otherDots.remove(at: Int(arc4random_uniform(UInt32(otherDots.count)))))
        }
        dots.forEach { dot in
            dot.otherDots.forEach { otherDot in
                let lineView = LineView(start: dot.frame.origin, end: otherDot.frame.origin)
                view.addSubview(lineView)
                view.sendSubview(toBack: lineView)
            }
        }
        
        if let start = dots.first {
            var targetDots = [start]
            while targetDots.count != 0 {
                let targetDot = targetDots.remove(at: 0)
                targetDot.otherDots.forEach {
                    let dx = targetDot.frame.origin.x - $0.frame.origin.x
                    let dy = targetDot.frame.origin.y - $0.frame.origin.y
                    if $0.score == 0 || targetDot.score + sqrt(dx*dx + dy*dy) < $0.score {
                        $0.score = targetDot.score + sqrt(dx*dx + dy*dy)
                        targetDots.append($0)
                        $0.routeDots = targetDot.routeDots + [targetDot]
                    }
                }
            }
        }
        if let last = dots.last, last.routeDots.count != 0 {
            last.routeDots.append(last)
            (0...(last.routeDots.count - 2)).forEach {
                let lineView = LineView(
                    start: last.routeDots[$0].frame.origin,
                    end: last.routeDots[$0 + 1].frame.origin,
                    color: UIColor.red,
                    lineWidth: 2)
                view.addSubview(lineView)
            }
        }
    }
}

class DotView: UIView {
    enum DotType {
        case start
        case goal
        case normal
    }
    
    var otherDots: [DotView] = []
    var score: CGFloat = 0
    var type: DotType = .normal
    var routeDots: [DotView] = []
}

class LineView: UIView {
    let start: CGPoint
    let end: CGPoint
    let color: UIColor
    let lineWidth: CGFloat
    
    init(start: CGPoint, end: CGPoint, color: UIColor = UIColor.darkGray, lineWidth: CGFloat = 4) {
        self.start = start
        self.end   = end
        self.color = color
        self.lineWidth = lineWidth
        
        super.init(frame: CGRect(
            x: ([start.x, end.x].min() ?? 0) + 10,
            y: ([start.y, end.y].min() ?? 0) + 10,
            width: abs(start.x - end.x),
            height: abs(start.y - end.y)))
        
        backgroundColor = UIColor.clear
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func draw(_ rect: CGRect) {
        let path = UIBezierPath()
        path.lineWidth = lineWidth
        color.set()
        path.move(to: CGPoint(x: start.x - frame.origin.x + 10, y: start.y - frame.origin.y + 10))
        path.addLine(to: CGPoint(x: end.x - frame.origin.x + 10, y: end.y - frame.origin.y + 10))
        path.stroke()
    }
}

実際の計算は下の部分です。
スタートから順番に点を辿り、その順路が最短なら更に探索、最短でなければそのルートは探索終了しています。

if let start = dots.first {
    var targetDots = [start]
    while targetDots.count != 0 {
        let targetDot = targetDots.remove(at: 0)
        targetDot.otherDots.forEach {
            let dx = targetDot.frame.origin.x - $0.frame.origin.x
            let dy = targetDot.frame.origin.y - $0.frame.origin.y
            if $0.score == 0 || targetDot.score + sqrt(dx*dx + dy*dy) < $0.score {
                $0.score = targetDot.score + sqrt(dx*dx + dy*dy)
                targetDots.append($0)
                $0.routeDots = targetDot.routeDots + [targetDot]
            }
        }
    }
}

これを実行すると、以下のように最短経路を表示できた事が分かります。

f:id:llcc:20170520191211p:plain

rspec-retryでfeature specを安定させる

rspec-retryという、失敗したテストを再実行するGemを使ってみました。

GitHub - NoRedInk/rspec-retry: retry randomly failing rspec example

まずはRailsのプロジェクトを作ってfeature specを書きます。

require 'rails_helper'

feature 'test' do
  scenario 'ページが表示される' do
    visit users_path

    expect(current_path).to eq users_path
  end
end

ここに一回目だけ失敗する処理を追加します。

require 'rails_helper'

i = 0
feature 'test' do
  scenario 'ページが表示される' do
    i += 1
    raise if i == 1
    visit users_path

    expect(current_path).to eq users_path
  end
end

これを実行するとエラーになります。

f:id:llcc:20170510000358p:plain

次はrspec-retryでエラーが出たら何回かリトライするようにします。
まずはGemfileにrspec-retryを追加します。

group :development, :test do
  gem 'rspec-retry'
end

次はspec_helper.rbにrspec-retry関連の設定を追加します。

require 'rspec/retry'

RSpec.configure do |config|
  config.verbose_retry = true
  config.display_try_failure_messages = true
end

最後にscenarioにretry回数を追加します。

require 'rails_helper'

i = 0
feature 'test' do
  scenario 'ページが表示される', retry: 3 do
    i += 1
    raise if i == 1
    visit users_path

    expect(current_path).to eq users_path
  end
end

実行すると、1回目で失敗したけど2回目で成功したことが分かります。

f:id:llcc:20170510000835p:plain

retryはscenarioではなくfeatureに付ける事もできます。

require 'rails_helper'

i = 0
feature 'test', retry: 3 do
  scenario 'ページが表示される' do
    i += 1
    raise if i == 1
    visit users_path

    expect(current_path).to eq users_path
  end
end

feature spec以外で使う事もできます。

require 'rails_helper'

i = 0
RSpec.describe User, type: :model, retry: 3 do
  it do
    i += 1
    raise if i == 1
    expect(1).to eq 1
  end
end

f:id:llcc:20170510001152p:plain

全feature specを対象にしたい場合は、spec_helper.rbに以下に設定を追加します。

require 'rspec/retry'

RSpec.configure do |config|
  config.verbose_retry = true
  config.display_try_failure_messages = true
  config.around :each, type: :feature do |ex|
    ex.run_with_retry retry: 3
  end
end

モンテカルロ法で円周率を求める

囲碁AIで有名なモンテカルロ法で円周率を計算できるようなので試してみました。

モンテカルロ法とは、Wikipediaによるとシミュレーションや数値計算を乱数を用いて行う手法の総称とのことです。

モンテカルロ法 - Wikipedia

実装方法

計算の手順は下の通りです。

  1. 四角形とそれに内接する円を作成する
  2. 四角形の中に多数の点を配置する
  3. 円の内部にある点の数を円の面積だと仮定して円周率を計算する

まずは四角形と内接する円を作成します。

import UIKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let v = UIView(frame: CGRect(x: 100, y: 100, width: 200, height: 200))
        v.backgroundColor = UIColor.darkGray
        view.addSubview(v)
        let circle = UIView(frame: v.bounds)
        circle.backgroundColor = UIColor.lightGray
        circle.layer.cornerRadius = circle.frame.width / 2
        v.addSubview(circle)
    }
}

これを実行すると、四角形と円が描画されます。

f:id:llcc:20170508235355p:plain

次は四角形の上に点を配置していきます。

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let v = UIView(frame: CGRect(x: 100, y: 100, width: 200, height: 200))
        v.backgroundColor = UIColor.darkGray
        view.addSubview(v)
        let circle = UIView(frame: v.bounds)
        circle.backgroundColor = UIColor.lightGray
        circle.layer.cornerRadius = circle.frame.width / 2
        v.addSubview(circle)
        
        // ここから今回追加
        var dots: [UIView] = []
        (0...40000).forEach { _ in
            let x = CGFloat(arc4random_uniform(201))
            let y = CGFloat(arc4random_uniform(201))
            let dot = UIView(frame: CGRect(x: x, y: y, width: 1, height: 1))
            dot.backgroundColor = UIColor.white
            v.addSubview(dot)
            dots.append(dot)
        }
    }
}

実行すると、点が配置されているのが分かります。

f:id:llcc:20170509000427p:plain

次に円の中の点の数を計算します。

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let v = UIView(frame: CGRect(x: 100, y: 100, width: 200, height: 200))
        v.backgroundColor = UIColor.darkGray
        view.addSubview(v)
        let circle = UIView(frame: v.bounds)
        circle.backgroundColor = UIColor.lightGray
        circle.layer.cornerRadius = circle.frame.width / 2
        v.addSubview(circle)
        
        var dots: [UIView] = []
        (0...40000).forEach { _ in
            let x = CGFloat(arc4random_uniform(201))
            let y = CGFloat(arc4random_uniform(201))
            let dot = UIView(frame: CGRect(x: x, y: y, width: 1, height: 1))
            dot.backgroundColor = UIColor.white
            v.addSubview(dot)
            dots.append(dot)
        }
        
        // ここから今回追加
        let count = dots.filter {
            let dx = $0.frame.origin.x - circle.center.x
            let dy = $0.frame.origin.y - circle.center.y
            let radius = circle.frame.size.width / 2
            return dx*dx + dy*dy < radius*radius
        }.count
        print(count) // → 74
    }
}

最後に円の中の点(円の面積)を元に円周率を求めます。
結果は、3.1111と3.14に近い数字となりました。

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let v = UIView(frame: CGRect(x: 100, y: 100, width: 200, height: 200))
        v.backgroundColor = UIColor.darkGray
        view.addSubview(v)
        let circle = UIView(frame: v.bounds)
        circle.backgroundColor = UIColor.lightGray
        circle.layer.cornerRadius = circle.frame.width / 2
        v.addSubview(circle)
        
        var dots: [UIView] = []
        (0...40000).forEach { _ in
            let x = CGFloat(arc4random_uniform(201))
            let y = CGFloat(arc4random_uniform(201))
            let dot = UIView(frame: CGRect(x: x, y: y, width: 1, height: 1))
            dot.backgroundColor = UIColor.white
            v.addSubview(dot)
            dots.append(dot)
        }
        
        let count = dots.filter {
            let dx = $0.frame.origin.x - circle.center.x
            let dy = $0.frame.origin.y - circle.center.y
            let radius = circle.frame.size.width / 2
            return dx*dx + dy*dy < radius*radius
        }.count
        
        let radius = circle.frame.size.width / 2
        print(CGFloat(count) / radius / radius) // 3.1111
    }
}

【GameplayKit】GKRuleSystemで複雑な条件を管理する

GKRuleSystemという複数の条件を管理する機能を使ってみました。

使い方は下の通りです。

  1. GKRuleSystemにGKRuleを追加する
  2. GKRuleSystemにパラメータをセットする
  3. GKRuleSystemの評価メソッド(evaluate)を呼ぶ
  4. 評価の結果を取得する

コードは下の通りです。
評価の結果は、GKRuleの引数のgradeの数字が返ってきます。
system.addがルールの追加、system.state["value1"] = 0がパラメータのセット、system.evaluate()が評価メソッドの呼び出し、system.grade(forFact: NSString(string: "fact1"))が結果の取得になります。

let system = GKRuleSystem()
system.add([
    GKRule(predicate: NSPredicate(format: "$value1 = 0"), assertingFact: NSString(string: "fact1"), grade: 0.9),
    ])
system.state["value1"] = 0
system.reset()
system.evaluate()
print(system.grade(forFact: NSString(string: "fact1"))) // → 0.9

条件式がfalseの場合、結果は0になります。

let system = GKRuleSystem()
system.add([
    GKRule(predicate: NSPredicate(format: "$value1 = 0"), assertingFact: NSString(string: "fact1"), grade: 0.9),
    ])
system.state["value1"] = 1 // → 今回は1をセット
system.reset()
system.evaluate()
print(system.grade(forFact: NSString(string: "fact1"))) // → 0.0

GKRuleは複数渡す事ができます。
複数の条件を満たす場合、結果は全ての条件の和になります。

let system = GKRuleSystem()
system.add([
    GKRule(predicate: NSPredicate(format: "$value1 = 0"), assertingFact: NSString(string: "fact1"), grade: 0.1),
    GKRule(predicate: NSPredicate(format: "$value2 = 1"), assertingFact: NSString(string: "fact1"), grade: 0.2),
    ])
system.state["value1"] = 0
system.state["value2"] = 1
system.reset()
system.evaluate()
print(system.grade(forFact: NSString(string: "fact1"))) // → 0.3

しかし結果は1.0以上にはならないので注意が必要です。
和が1.0以上の場合は、1.0が返ってきます。

let system = GKRuleSystem()
system.add([
    GKRule(predicate: NSPredicate(format: "$value1 = 0"), assertingFact: NSString(string: "fact1"), grade: 1.1),
    GKRule(predicate: NSPredicate(format: "$value2 = 1"), assertingFact: NSString(string: "fact1"), grade: 2.2),
    ])
system.state["value1"] = 0
system.state["value2"] = 1
system.reset()
system.evaluate()
print(system.grade(forFact: NSString(string: "fact1"))) // → 1.0

GKRuleの初期化のassertingFactをretractingFactに変えると、条件を満たした時にgradeを減算するようになります。
これも和の時と同じように0.0以下にはなりません。

let system = GKRuleSystem()
system.add([
    GKRule(predicate: NSPredicate(format: "$value1 = 0"), assertingFact: NSString(string: "fact1"), grade: 0.5),
    GKRule(predicate: NSPredicate(format: "$value2 = 1"), retractingFact: NSString(string: "fact1"), grade: 0.2),
    ])
system.state["value1"] = 0
system.state["value2"] = 1
system.reset()
system.evaluate()
print(system.grade(forFact: NSString(string: "fact1"))) // → 0.3

GKRuleはNSPredicateではなくブロックで評価をする事もできます。

GKRule(blockPredicate: { system in return true }, action: { system in print("条件を満たした") })

GKRuleのサブクラスを作って、そこに評価式を書くこともできます。
複雑な条件の時はこれを使うのも良さそうです。

class MyRule: GKRule {
    override func evaluatePredicate(in system: GKRuleSystem) -> Bool {
        return true
    }
    
    override func performAction(in system: GKRuleSystem) {
        print("条件を満たした")
    }
}

【Swift】ベジェ曲線を自前で描いてみる

ベジェ曲線って良く聞くんですが、イマイチ理解できてなかったので自前で描いてみました。
具体的にはベジェ曲線の座標を自分で計算して描画をしてみました。

ベジェ曲線の座標の求め方

ベジェ曲線の座標は制御点を使って求められます。
今回は下のように制御点が3つの場合のベジェ曲線を求めます。

f:id:llcc:20170504212742p:plain

まずはP1とP2、P2とP3の2点間をつなぎます。

f:id:llcc:20170504213146p:plain

次にP1-P2間で少しだけP1から離れた点P4とP2-P3で少しだけP2から離れたP5を決めて、それらをつなぎます。

f:id:llcc:20170504213445p:plain

そしてP4-P5間で少しだけP4から離れた点P6がベジェ曲線の座標になります。

f:id:llcc:20170504213602p:plain

段々とP4・P5をP2・P3に近づけて行きつつ、それぞれのP6を算出します。

f:id:llcc:20170504213755p:plain

f:id:llcc:20170504213911p:plain

ここで算出したP6を全てつなげればベジェ曲線になります。
今回はこれをSwiftで実装してみようと思います。

ベジェ曲線をSwiftで描画

今回はこの3つの制御点を使ったベジェ曲線を求めます。

f:id:llcc:20170504214739p:plain

現状のコードは下の通りです。

import UIKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let myView = MyView(frame: view.bounds)
        myView.backgroundColor = UIColor.white
        view.addSubview(myView)
    }
}

class MyView: UIView {
    let point1 = CGPoint(x: 100, y: 200)
    let point2 = CGPoint(x: 200, y: 100)
    let point3 = CGPoint(x: 300, y: 200)
    
    override func draw(_ rect: CGRect) {
        UIColor.darkGray.setFill()
        UIBezierPath(roundedRect: CGRect(origin: point1, size: CGSize(width: 5, height: 5)), cornerRadius: 2).fill()
        UIBezierPath(roundedRect: CGRect(origin: point2, size: CGSize(width: 5, height: 5)), cornerRadius: 2).fill()
        UIBezierPath(roundedRect: CGRect(origin: point3, size: CGSize(width: 5, height: 5)), cornerRadius: 2).fill()
    }
}

先程描いたように、point1・point2・point3を元に、point6を求めてそれを繋いでいきます。

class MyView: UIView {
    let point1 = CGPoint(x: 100, y: 200)
    let point2 = CGPoint(x: 200, y: 100)
    let point3 = CGPoint(x: 300, y: 200)
    
    override func draw(_ rect: CGRect) {
        let count = 100
        let path = UIBezierPath()
        path.move(to: point1)
        (0...count).forEach {
            let point4 = CGPoint(
                x: point1.x + (point2.x - point1.x) * CGFloat($0) / CGFloat(count),
                y: point1.y + (point2.y - point1.y) * CGFloat($0) / CGFloat(count)
            )
            let point5 = CGPoint(
                x: point2.x + (point3.x - point2.x) * CGFloat($0) / CGFloat(count),
                y: point2.y + (point3.y - point2.y) * CGFloat($0) / CGFloat(count)
            )
            let point6 = CGPoint(
                x: point4.x + (point5.x - point4.x) * CGFloat($0) / CGFloat(count),
                y: point4.y + (point5.y - point4.y) * CGFloat($0) / CGFloat(count)
            )
            path.addLine(to: point6)
        }
        path.addLine(to: point3)
        path.lineWidth = 5.0
        UIColor.brown.setStroke()
        path.stroke()
    }
}

これを実行すると以下のようになります。
無事にきれいな曲線を引くことができました。

f:id:llcc:20170504215426p:plain

【Swift3】UIViewのdrawの中で線や文字や画像を描画する

UIViewのdrawメソッド中では線・文字・矩形など様々なものを描画できます。
今回はそれらの描画を試してみました。

検証用コードは下の通りです。

import UIKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let myView = MyView(frame: view.bounds)
        myView.backgroundColor = UIColor.white
        view.addSubview(myView)
    }
}

class MyView: UIView {
    override func draw(_ rect: CGRect) {
    }
}

線を引く

線の描画は以下の通りです。
線の太さや色を変える事ができます。

class MyView: UIView {
    override func draw(_ rect: CGRect) {
        let path = UIBezierPath()
        path.move(to: CGPoint(x: 100, y: 100))
        path.addLine(to: CGPoint(x: 200, y: 200))
        path.lineWidth = 5.0 // 線の太さ
        UIColor.brown.setStroke() // 色をセット
        path.stroke()
    }
}

実行結果は以下の通りです。

f:id:llcc:20170503235019p:plain

addLineを追加すれば複雑な線も作る事ができます。

class MyView: UIView {
    override func draw(_ rect: CGRect) {
        let path = UIBezierPath()
        path.move(to: CGPoint(x: 100, y: 100))
        path.addLine(to: CGPoint(x: 200, y: 200))
        path.addLine(to: CGPoint(x: 300, y: 100)) // 今回追加
        path.lineWidth = 5.0 // 線の太さ
        UIColor.brown.setStroke() // 色をセット
        path.stroke()
    }
}

f:id:llcc:20170503235142p:plain

曲線を描画する

addCurveメソッドを使う事でベジェ曲線を描画する事ができます。

class MyView: UIView {
    override func draw(_ rect: CGRect) {
        let path = UIBezierPath()
        path.move(to: CGPoint(x: 100, y: 100))
        path.addCurve(to: CGPoint(x: 200, y: 200), controlPoint1: CGPoint(x: 150, y: 100), controlPoint2: CGPoint(x: 200, y: 150))
        path.lineWidth = 5.0 // 線の太さ
        UIColor.brown.setStroke() // 色をセット
        path.stroke()
    }
}

f:id:llcc:20170503235903p:plain

曲線の描画はaddArcでも行う事ができます。

class MyView: UIView {
    override func draw(_ rect: CGRect) {
        let path = UIBezierPath()
        path.move(to: CGPoint(x: 100, y: 100))
        path.addArc(withCenter: CGPoint(x: 200, y: 100), radius: 100, startAngle: 180 * CGFloat.pi/180, endAngle: 90 * CGFloat.pi/180, clockwise: false)
        path.lineWidth = 5.0 // 線の太さ
        UIColor.brown.setStroke() // 色をセット
        path.stroke()
    }
}

f:id:llcc:20170504000320p:plain

四角形の描画

四角形の描画は以下のように行います。

class MyView: UIView {
    override func draw(_ rect: CGRect) {
        let path = UIBezierPath(roundedRect: CGRect(x: 100, y: 100, width: 100, height: 100), cornerRadius: 10)
        UIColor.darkGray.setFill() // 色をセット
        path.fill()
    }
}

f:id:llcc:20170504000650p:plain

addLineを使って描画する事もできます。

class MyView: UIView {
    override func draw(_ rect: CGRect) {
        let path = UIBezierPath()
        path.move(to: CGPoint(x: 100, y: 100))
        path.addLine(to: CGPoint(x: 200, y: 100))
        path.addLine(to: CGPoint(x: 200, y: 200))
        path.addLine(to: CGPoint(x: 100, y: 200))
        path.addLine(to: CGPoint(x: 100, y: 100))
        UIColor.darkGray.setFill() // 色をセット
        path.fill()
    }
}

文字の描画

文字の描画は以下のように行います。
第2引数でフォントや文字色の指定もできます。

class MyView: UIView {
    override func draw(_ rect: CGRect) {
        "MyText".draw(at: CGPoint(x: 100, y: 100), withAttributes: [
            NSForegroundColorAttributeName : UIColor.blue,
            NSFontAttributeName : UIFont.systemFont(ofSize: 50),
            ])
    }
}

f:id:llcc:20170504001145p:plain

画像の描画

画像もテキスト同様にdrawメソッドを使います。

class MyView: UIView {
    override func draw(_ rect: CGRect) {
        UIImage(named: "sample")?.draw(at: CGPoint(x: 100, y: 100))
    }
}

f:id:llcc:20170504001341p:plain

WKWebViewのキャッシュなどをクリアする

個人で出しているブラウザアプリの容量がかなりの大きさになっていたので調査しました。

キャッシュ等の削除方法は下の通りです。
これでアプリ容量が500MB → 60MBまで減りました。

WKWebsiteDataStore.default().removeData(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), modifiedSince: Date(timeIntervalSince1970: 0), completionHandler: {})

「キャッシュ・cookieだけ」のように削除対象を絞りたい場合は第一引数に削除したい対象をセットします。

WKWebsiteDataStore.default().removeData(ofTypes: [WKWebsiteDataTypeDiskCache], modifiedSince: Date(timeIntervalSince1970: 0), completionHandler: {})

キーとして使えるものは以下の通りです。

public let WKWebsiteDataTypeDiskCache: String
public let WKWebsiteDataTypeMemoryCache: String
public let WKWebsiteDataTypeOfflineWebApplicationCache: String
public let WKWebsiteDataTypeCookies: String
public let WKWebsiteDataTypeSessionStorage: String
public let WKWebsiteDataTypeLocalStorage: String
public let WKWebsiteDataTypeWebSQLDatabases: String
public let WKWebsiteDataTypeIndexedDBDatabases: String