カテゴリー
iOS Swift

iOS & Swift 102 Threadについて

これまで見てきたコードは、全体がひとつの流れになっていて、初めから終わりまでが順番に処理されていくものになっていました。

このひとつの流れのことをThread(スレッド)といいます。

以下の例はボタンをタップすると1秒ごとにコンソールに変数iの値がプリントされるプログラムです。

import UIKit

class ViewController: UIViewController {
  
  lazy var button: UIButton = {
    let button = UIButton()
    button.setTitle("Tap", for: .normal)
    button.setTitleColor(.white, for: .normal)
    button.backgroundColor = .red
    button.addTarget(self, action: #selector(onButtonTapped), for: .touchUpInside)
    return button
  }()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    view.addSubview(button)
  }
  
  override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()
    button.frame = CGRect(x: view.bounds.width/2 - 50, y: view.bounds.height/2 - 50, width: 100, height: 100)
  }
  
  private func loop () {
    for i in 0..<100 {
      Thread.sleep(forTimeInterval: 1)
      print(Thread.current)
      print(i)
    }
  }
  
  @objc private func onButtonTapped() {
    loop()
    print("button tapped")
  }
}

これをビルドし、ボタンをタップすると、コンソールに1秒ごとに変数iのカウントがプリントされます。

[1] 現在のThreadオブジェクトをコンソールにプリントしています。<_NSMainThread: ….>とプリントされているのがメイン・スレッドです。

Swiftだけではなく、他の色々なプログラミング言語・OSにおいても同じですが、UI(ユーザーインターフェース)はmain thread(メイン・スレッド)で動作しています。

カウント中にボタンをタップしても、コンソールには”button tapped”とプリントされないことに気が付くと思います。

これはThread.sleepがメイン・スレッドにおいて行われているため、UIがブロック(フリーズ)しているからです。

プログラミングでは、UIが動作するメインスレッドでは時間のかかるタスクを処理させてはいけないという大前提があります。理論上プログラムはクラッシュせずに正しく動いているのですが、UIが固まって見えるようでは、ユーザー満足度はとても低くなるからです。時間のかかるタスクはUIとは別のスレッド(バックグラウンド・スレッド)で処理する必要があります。

実際のアプリではひとつのThreadだけで動作していることはあまりなく、複数のThreadが起動しています。これをMulti Thread(マルチ・スレッド)といいます。

iOSではこのマルチ・スレッドを簡単に取り扱えるGrand Central Dispatch(GCD)という仕組みがあります。

loop( )関数を以下のように書き換えてみましょう。

private func loop () {
  // バックグラウンド・キュー(Thread)を作成
  DispatchQueue.global().async {
    for i in 0..<10 {
      Thread.sleep(forTimeInterval: 1)
    // NS Thread number = 5 などとプリントされる
      print(Thread.current)
      print(i)        
    }
  }
}

ボタンをタップするとバックグラウンド・スレッドが起動され、そのすぐ後に”button tapped”とプリントされ、カウントが始まるのがわかると思います。

もう一度ボタンをタップすると、また”button tapped”とプリントされ、先ほどとは違うカウントが「並列(concurrent)に」実行されているのがわかると思います。これは、もうひとつ別のバックグラウンド・スレッドを起動した、ということです。

次に、ひとつラベルを用意して、変数iの値をスクリーンに表示するようにしてみましょう。太字部分が先ほどのコードからの変更点です。

class ViewController: UIViewController {
  
  // labelを用意
  lazy var label: UILabel = {
    let label = UILabel()
    label.backgroundColor = .blue
    label.textColor = .white
    label.textAlignment = .center
    return label
  }()
  
  lazy var button: UIButton = {
    let button = UIButton()
    button.setTitle("On / Off", for: .normal)
    button.setTitleColor(.white, for: .normal)
    button.backgroundColor = .red
    button.addTarget(self, action: #selector(onButtonTapped), for: .touchUpInside)
    return button
  }()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    view.addSubview(button)
  // labelをviewに追加
    view.addSubview(label)
  }
  
  override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()
    button.frame = CGRect(x: view.bounds.width/2 - 50, y: view.bounds.height/2 - 50, width: 100, height: 100)
  // labelをレイアウト
    label.frame = CGRect(x: view.bounds.width/2 - 50, y: view.bounds.height/4 - 25, width: 100, height: 50)
  }
  
  
  private func loop () {
    DispatchQueue.global().async {
      for i in 0..<10 {
        Thread.sleep(forTimeInterval: 1)
        print(Thread.current)
     // labelテキストにiを代入
        self.label.text = "\(i)"
      }
    }
  }
  
  @objc private func onButtonTapped() {
    loop()
    print("button tapped")
  }
}

ビルドし、ボタンをタップしてみましょう。

実はこのコードはうまく動作しません。

UI関連のアップデートはバックグラウンド・スレッドから行ってはいけないというルールがあるからです。

loop( )関数を以下のように変えるとうまく動作します。

private func loop () {
  DispatchQueue.global().async {
    for i in 0..<10 {
      Thread.sleep(forTimeInterval: 1)
      print(Thread.current)
      // バックグラウンド・スレッドから
    // メイン・スレッドに処理を戻している
      DispatchQueue.main.async {
        self.label.text = "\(i)"          
      }
    }
  }
}