のーとぶっく

学んだことをまとめておく学習帳および備忘録

ChordPicks 公開終了のお知らせ

2020年6月にリリースいたしました「ChordPicks」の公開およびサポートを2020年12月31日をもって終了いたします。


少数とはいえ、ちょっとでも触ってみてくださった方々、短い間でしたが本当にありがとうございました!


またの機会がありましたら、よろしくお願いいたします。

iOSアプリ「ChordPicks」リリースしました!

 

先日、ようやっとiOSアプリ第1作目をリリースしました。

 

「ChordPicks」

https://apps.apple.com/jp/app/chordpicks/id1517211925

f:id:hirakana:20200625174349p:plain

 

作編曲補助ツールで、画面に一覧表示されたコードをタップして和音を鳴らすアプリです。

 

これからアップデートを重ねて、よりコード進行の発見を補助できるように進化させていきたいと思ってます。

 

とりあえず「1つアプリを公開する」という目標を達成できて良かったぁー。

【Swift】非同期処理 - GCD -

動作確認環境

Xcode: 11.3.1
Swift: 5.1.3

同期処理・非同期処理

同期処理

同期処理とは、プログラムを上から順に実行していくこと。
Aの処理が終わったらBの処理。Bが終わったらCへ。しかし、Aが時間のかかる処理だった場合、Bを実行するにはAの完了を待たなければいけない。その問題を解決するのが非同期処理。

非同期処理

プログラムの実行は、CPU のスレッドという実行単位で処理を管理する。通常はメインスレッドという単一のスレッドで実行されるが、複数の処理を並列して実行する場合、メインスレッドとは別のスレッドを作成して処理を預ける。これを非同期処理という。

GCD (Grand Central Dispatch)

こういったマルチスレッドを使った非同期処理は、プログラマー自身が各スレッドを直接管理して処理を割り振ることもできるが、複雑で難しくなる。
GCD を利用すれば、スレッドを直接管理しなくても CPU のコア数や負荷の状況を見て自動的に最適なスレッドに処理を割り振ってくれる。

キュー

GCD では、キューとタスクを使って非同期処理を行う。
キューは、処理の順番待ち行列を並ばせる道のようなもので、そこに処理群のタスクを追加していく。GCD のキューは、ディスパッチキューと呼ばれ、大きく2つに分かれる。

1. 直列ディスパッチキュー (serial dispatch queue)

メインスレッドでタスクを実行するディスパッチキュー。実行中の処理が終わり次第、次の処理に移行する。

//メインキューを取得
let mainQueue = DispatchQueue.main
2. 並列ディスパッチキュー (concurrent dispatch queue)

メインスレッド以外のサブスレッドを利用してタスクを実行するディスパッチキュー。他のスレッドの実行完了を待たずに、並行して処理を行う。

//グローバルキューを取得
let globalQueue = DispatchQueue.global()

このグローバルキューでは、キューごとに優先順位をつけて処理を振り分けることができる。その優先順位は QoS (Quality of Service) といい、5種類に分かれる。上から優先度の高い順に、

//1. ユーザーの入力に即座に反応しなければならないような処理を扱う
DispatchQueue.global(qos: .userInteractive)

//2. ユーザーからの入力を受けて行う処理を扱う
DispatchQueue.global(qos: .userInitiated)

//3. 2と4の中間の優先度で、通常`default`を引数で指定することはなく、後者を使う
DispatchQueue.global(qos: .default)
DispatchQueue.global()

//4. 即座の結果を要しない処理を扱う
DispatchQueue.global(qos: .utility)

//5. 数分から数時間かかっても問題ない処理
DispatchQueue.global(qos: .background)

指定した優先順位に応じてタスクが実行される。

キューを生成

ディスパッチキューに自前の名称をつけて新しく生成することもできる。

//自前の名称はデバッグ時にも役立つ
let queue = DispatchQueue(
    label: "com.sample.sample_App.sample_queue",
    qos: .default,
    attributes: .concurrent
)

一般的に名称となる引数 label には、他のライブラリで使用されているものと被る可能性を無くすため、逆順DNSが使われる。引数 attributes は、指定がなければ serial として扱われる。

タスクの追加

引数にクロージャを渡してタスクを追加する。
追加する方法はいくつかある。

同期 sync(execute:)

渡したタスクの処理が完了するまでの間、sync(execute:) を実行したスレッドの処理を止める追加方法。
※スレッドの処理を止める働きがあるので、2つ以上のスレッドが互いの処理待ちで何も動かなくなる"デッドロック"が起きるコードを書いてしまわないよう、注意が必要。

queue.sync {
    //code
}
非同期 async(execute:)

渡したタスクの完了を待たずに async(execute:) を実行したスレッドの処理を進める追加方法。

queue.async {
    //code
}
その他

同一のグローバルキューに直列のように順番に処理してほしい場合

queue.async(flags: .barrier) {
    print(1)
}

queue.async(flags: .barrier) {
    print(2)
}

queue.async(flags: .barrier) {
    print(3)
}

タスクの実行開始に遅延を持たせたい場合

//2秒の遅延
queue.asyncAfter(deadline: .now() + 2.0) {
    //code
}

実行中のタスクのスレットがメインスレッドか否かを確認する方法

Thread クラスの isMainThread プロパティで Bool 値を確認できる。

Thread.isMainThread

【Swift】クロージャの基礎

動作確認環境

Xcode: 11.3.1
Swift: 5.1.3

クロージャとは?

クロージャとは、ひとまとまりになった処理のこと。このひとまとまりごと変数や定数に入れたり、関数の引数に渡して扱う。

//「猪木!ボンバイエ!」を出力するひとまとまりの処理
{
    let name = "猪木"
    let word = "ボンバイエ"
    print("\(name)\(word)!")
}

クロージャの書式は、

{ (引数名1: 型, 引数名2:...) -> 戻り値の型 in
    処理
}

実行は、

let sayDomo = {
    print("どーも")
}

sayDomo()

文字列 String や整数 Int と同じようにクロージャ自身にも型があり、引数と戻り値の型がクロージャ自身の型になる。
戻り値がない場合、戻り値の型は「Void」

{ (num: Int) -> String in
    return "\(num)回転サルコウ"
}

//この場合の型は...
(Int) -> String

なのでこの型の制約を持った変数に入れることもできるし、

var jump: (Int) -> String

型推論も行われる。

var jump = { (num: Int) -> String in
    "\(num)回転サルコウ"
}

print(type(of: jump)) // (Int) -> String

型を省略

型推論によりクロージャ内の型の記述を省略できる。
下の場合、引数は Int 、戻り値は String が変数の型指定を推論するので両方省略している。
(※戻り値はあるが処理が一行の場合は return を省略できる)

var jump: (Int) -> String

jump = { num in
    "\(num)回転サルコウ"
}

print(jump(42)) // 42回転サルコウ
var jump: () -> String

jump = {
    "半回転サルコウ"
}

print(jump())

引数

クロージャでは、外部引数名、初期値を設定するデフォルト引数、スコープ内で引数を書き換えるインアウト引数は、使えない。

//コンパイルエラー
{ (number num: Int) in
    /**/
}

//コンパイルエラー
{ (num: Int = 5) in
    /**/
}

//コンパイルエラー
{ (num: Int) in
    num = 5
}

引数名を簡略化する

引数名をつけず、第1引数を「$0」、第2引数を「$1」というように、$ と引数の番号を使って書くことができる。

let skill: (String, Int) -> Void = {
    print("\($0)\($1)倍!")
}

skill("界王拳",4)

ただ、簡略化をして何の引数を扱ってるかが解りにくくなるような記述は避けた方がいい。
(なので↑の例は良くない...)

クロージャを引数にする

クロージャ自身を関数の引数として扱うことができる。

//受け取る関数
func myFunc(closure: () -> Void) {
    //...
}

//値を渡す
myFunc(closure: {
    //...
})
escaping

関数でクロージャを引数として受け取り、そのクロージャを関数外へ反映させる際には、「この引数のクロージャを関数外に持ち出しますよ」ということを明示するため、引数の型の前に @escaping という属性を宣言する。宣言しないとコンパイルエラーになる。

var myClosure: () -> Void = {
    print("入学おめでとう!")
}

myClosure() //入学おめでとう!

func changeClosure(closure: @escaping () -> Void) {
    myClosure = closure
}

changeClosure(closure: {
    print("卒業おめでとう!")
})

myClosure() //卒業おめでとう!
autoclosure

例として、論理和(2つのうちどちらか一方でも真なら真、どちらも偽なら偽)を返す関数があるとする。2つの引数はそれぞれ真偽値を返す関数から受け取る場合、このor(_:_:)関数の実行時には、2つの関数が働くことになる。

func or(_ a: Bool, _ b: Bool) -> Bool {
    
    if a {
        return true
    } else {
        return b
    }
    
}

func returnTrue() -> Bool {
    return true
}

func returnFalse() -> Bool {
    return false
}

or(returnTrue(), returnFalse()) // true

しかし、↑の if 文だと、引数 a が true なら b をチェックするまでもなく true になるので、b を受け取る関数を実行する必要がない。なので b をクロージャで受け取って or(_:_:) 関数の実行時に働く関数は a だけにしようとするには、↓のようにすれば解決する。

func or(a: Bool, b: () -> Bool) -> Bool {
    
    if a {
        return true
    } else {
        let result = b()
        return result
    }
    
}

func returnTrue() -> Bool {
    return true
}

func returnFalse() -> Bool {
    return false
}

or(a: returnTrue(), b: { return returnFalse() })

こうすることで、a が false の場合にのみ、b のクロージャを通じて処理が実行されて万々歳かと思いきや、or(_:_:) 関数実行の引数の部分が煩雑な記述になってしまう。実行を遅らせたいが、実行時の記述の煩雑さを解消したいという願いを一手に叶えてくれるのが autoclosure 。

func or(a: Bool, b: @autoclosure () -> Bool) -> Bool {
    //省略
}

//省略

or(a: returnFalse(), b: returnTrue())

これで綺麗さっぱり可読性の高いコードを書くことができる。

トレイリングクロージャ

引数にクロージャを渡す関数実行の記述では、最後の引数がクロージャの場合、() の外にクロージャを書いて見やすくすることができる。

func myFunc(num: Int, closure: (String) -> Void) {
    closure("今なら\(num)ポイント還元")
}

myFunc(num: 2) { string in
    print(string)
}
func myFunc(closure: ()-> Void) {
    closure()
}

myFunc {
    print("スッキリ")
}

キャプチャ

通常、変数定数は、宣言されたスコープの外側からアクセスすることができない。

... {

    var count = 0

}

count = 1 //×

しかしクロージャは、参照している変数定数をキャプチャするので、その変数定数の定義されているスコープの外側でクロージャを実行しても利用することができる。

var jump: () -> Void

... {
    
    var count = 0
    
    jump = {
        count += 1
        print("\(count)回転サルコウ")
    }
    
}

jump() //1回転サルコウ
jump() //2回転サルコウ

関数をクロージャとして扱う

関数の一連の処理をクロージャとして扱うこともできる。

func addDesuwayo(_ string: String) -> String {
    return string + "ですわよ!"
}

//変数定数名 = 関数名(引数名:)
let desuwayo = addDesuwayo(_:)

print(desuwayo("遅刻")) // 邪魔ですわよ!

【Swift/UIKit】ドラッグ移動で形合わせパズル

f:id:hirakana:20200223214956g:plain

hirakana.hatenablog.jp

↑ UIView をドラッグ移動させる方法の練習がてら簡単質素なパズルを作った。

コード

import UIKit

class ViewController: UIViewController {
    
    @IBOutlet weak var bookImage: UIImageView!
    @IBOutlet weak var cloudImage: UIImageView!
    @IBOutlet weak var personImage: UIImageView!
    
    var imageViews = [UIImageView()]
    
    var piece: UIImageView!
    
    //初期位置
    var defaultX = CGFloat()
    var defaultY = CGFloat()
    
    let imageNames = ["book.fill","cloud.fill","person.fill"]
    var num = Int()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        imageViews = [bookImage,cloudImage,personImage]
        
        //移動ピース
        piece = UIImageView()
        piece.frame = CGRect(x: 0, y: 0, width: 80, height: 80)
        piece.center = CGPoint(x: self.view.frame.size.width/2, y: self.view.frame.size.height-240)
        piece.contentMode = .scaleAspectFit
        piece.tintColor = .brown
        piece.isUserInteractionEnabled = true
        piece.layer.shadowOffset = CGSize(width: 1.0, height: 2.0)
        piece.layer.shadowColor = UIColor.black.cgColor
        piece.layer.shadowOpacity = 0.3
        piece.layer.shadowRadius = 2
        self.view.addSubview(piece)
        
        //画像セット
        setImage()
        
        //初期位置
        defaultX = piece.frame.origin.x
        defaultY = piece.frame.origin.y
        
    }
    
    //画像セット
    func setImage() {
        num = Int.random(in: 0...2)
        piece.image = UIImage(systemName: imageNames[num])
    }

    //タップ開始
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        
        if touches.first?.view == piece {
            
            UIView.animate(withDuration: 0.06, animations: {
                
                self.piece.transform = CGAffineTransform(scaleX: 0.8, y: 0.8)
                
            }) { (Bool) in
                
            }
            
        }
        
    }
    
    //ドラッグ移動
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        
        let aTouch = touches.first!
        
        if aTouch.view == piece {
            
            let destLocation = aTouch.location(in: self.view)
            let prevLocation = aTouch.previousLocation(in: self.view)
            
            var frame = piece.frame
            
            let deltaX = destLocation.x - prevLocation.x
            let deltaY = destLocation.y - prevLocation.y
            
            frame.origin.x += deltaX
            frame.origin.y += deltaY
            
            piece.frame = frame
            
        }
        
    }
    
    //タップ終了
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        
        if touches.first?.view == piece {
            
            //指を離した時のアニメーション
            UIView.animate(withDuration: 0.1, animations: {
                
                self.piece.transform = CGAffineTransform(scaleX: 0.4, y: 0.4)
                self.piece.transform = CGAffineTransform(scaleX: 1.0, y: 1.0)
                
            }) { (Bool) in
                
            }
            
            //離した位置
            let dropPointX = Float(piece.center.x)
            let dropPointY = Float(piece.center.y)
            
            //正解の UIImageView
            let correctImage = imageViews[num]
            
            //正解の UIImageView の X,Y の範囲
            let xRangeMin = Float(correctImage.frame.origin.x)
            let xRangeMax = xRangeMin + Float(correctImage.frame.size.width)
            let yRangeMin = Float(correctImage.frame.origin.y)
            let yRangeMax = yRangeMin + Float(correctImage.frame.size.height)
            
            //離した位置が正解の UIImageView の範囲内なら
            if dropPointX > xRangeMin && dropPointX < xRangeMax ,
                dropPointY > yRangeMin && dropPointY < yRangeMax {
                
                //ピタラベル
                let pitaLabel = UILabel()
                pitaLabel.frame = CGRect(x: 0, y: 0, width: 100, height: 40)
                pitaLabel.center = CGPoint(x: correctImage.center.x, y: correctImage.center.y-55)
                pitaLabel.textAlignment = .center
                pitaLabel.textColor = .black
                pitaLabel.font = UIFont.boldSystemFont(ofSize: 18.0)
                pitaLabel.text = "\ピタッ/"
                self.view.addSubview(pitaLabel)
                
                //ピース非表示
                piece.isHidden = true
                
                correctImage.tintColor = .brown
                
                //0.8秒後
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
                    
                    //ピタラベル削除
                    pitaLabel.removeFromSuperview()
                    
                    //ピース表示
                    self.piece.isHidden = false
                    
                    correctImage.tintColor = .lightGray
                    
                    //元の場所へ
                    self.piece.frame.origin.x = self.defaultX
                    self.piece.frame.origin.y = self.defaultY
                    
                    //画像セット
                    self.setImage()
                    
                }
                
            } else {
                
                //元の場所へ
                piece.frame.origin.x = defaultX
                piece.frame.origin.y = defaultY
                
            }
            
        }
        
    }

}


【Swift/UIKit】ドラッグで UIView を動かす

スーッと指で UIView を動かす処理を書き置き。
ドラッグ中に細かく何度も呼び出されるメソッド touchesMoved(_:with:) を使用する。

f:id:hirakana:20200221224443p:plain

イメージ

メソッドで行う処理のイメージはこんな感じ

  1. 指でドラッグを開始した位置(座標)と終了した(動かしてる途中でも一区切りの中で)位置を取得
  2. 開始位置と終了位置から移動距離を出す
  3. 移動した分を UIView の元の位置に足して反映

指を動かしている間、メソッドが呼ばれるたびにこの流れを繰り返す

コード

UIView を用意。ここではラベルを。

var sampleLabel: UILabel!

isUserInteractionEnabled を忘れずに。これでタッチを感知することを許可する。

sampleLabel = UILabel()
sampleLabel.frame = CGRect(x: 0, y: 0, width: 100, height: 50)
sampleLabel.center = self.view.center
sampleLabel.textAlignment = .center
sampleLabel.backgroundColor = .black
sampleLabel.textColor = .white
sampleLabel.text = "Move me!"

//タッチを受け付ける(デフォルトは false)
sampleLabel.isUserInteractionEnabled = true

self.view.addSubview(sampleLabel)

何度も呼ばれるメソッド内の処理。動かしたいものに触れた時だけ実行したいので、タッチイベントの情報を使って条件分岐。

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
    
    //タッチイベント
    let aTouch = touches.first!
    
    //タッチされたものがラベルの場合
    if aTouch.view == sampleLabel {
        
        //指の移動先の座標
        let destLocation = aTouch.location(in: self.view)

        //指の移動前の座標
        let prevLocation = aTouch.previousLocation(in: self.view)

        //取得した座標を格納する変数
        var myFrame = sampleLabel.frame

        //X,Yの移動距離
        let deltaX = destLocation.x - prevLocation.x
        let deltaY = destLocation.y - prevLocation.y

        //移動距離分を足す
        myFrame.origin.x += deltaX
        myFrame.origin.y += deltaY

        //反映
        sampleLabel.frame = myFrame
        
    }
    
}

好みでタッチ開始、終了時にアニメーション

//タッチを感知した時に呼ばれるメソッド
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    
    if touches.first?.view == sampleLabel {
        
        UIView.animate(withDuration: 0.06, animations: {
            
            self.sampleLabel.transform = CGAffineTransform(scaleX: 0.8, y: 0.8)
            
        }) { (Bool) in
            
        }
        
    }
    
}
//指を離した時に呼ばれるメソッド
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
    
    if touches.first?.view == sampleLabel {
        
        UIView.animate(withDuration: 0.1, animations: {
        
            self.sampleLabel.transform = CGAffineTransform(scaleX: 0.4, y: 0.4)
            self.sampleLabel.transform = CGAffineTransform(scaleX: 1.0, y: 1.0)
            
        }) { (Bool) in
            
        }
        
    }
    
}

【Swift/UIKit】TableView のセル並び替えでセクション跨ぎを防ぐ

使いどきはわからないが、セルを並び替える際にセクションを跨いで並び替えられないようにする方法をちょいメモ。(並び替え可能にする工程は割愛)

//セルに当てはめる配列
var cellTitles = [
    ["アイテム1", "アイテム2", "アイテム3"],
    ["アイテム1", "アイテム2"],
    ["アイテム1", "アイテム2", "アイテム3", "アイテム4"]
]
//掴んだセルを離した時に呼び出されるメソッド
override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {

    /*
    sourceIndexPath : 移動前の情報
    destinationIndexPath : 移動先の情報
    */
    
    //移動前と移動先のセクションが一致した場合
    if sourceIndexPath.section == destinationIndexPath.section {
        
        //文字列を取得
        let cellTitle = cellTitles[sourceIndexPath.section][sourceIndexPath.row]
        
        //掴んだセルの番号から配列の値を削除
        cellTitles[sourceIndexPath.section].remove(at: sourceIndexPath.row)

        //取得した文字列を移動先に挿入
        cellTitles[sourceIndexPath.section].insert(cellTitle, at: destinationIndexPath.row)
        
    } else {
        
        self.tableView.reloadData()
        
    }
    
}