カテゴリー
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)"          
      }
    }
  }
}
カテゴリー
iOS Swift

iOS & Swift 102 callbackとdelegate

処理の中には、一定の時間がかかるものがあり(インターネット越しにあるAPIサーバーからレスポンスを待つ、など)、その処理が終わった後にプログラムに「これをやっといてね」と「頼み」たいことがよくあります。一般的にはそれらの処理を行う関数をcallback関数と呼びますが、Swiftではclosureを使ったパターン、delegateを使ったパターンがよく見られます。

まず closureで実装した例を説明します。

// PlaySound.swift

class PlaySound {

  // [1] 処理に1秒かかる作業を10回実行しているシミュレーション。1秒ごとにcallbackを呼ぶ。callbackはclosure型で指定
  public func play(callback: (Int) -> Void) {
    for i in 0..<10 {
      Thread.sleep(forTimeInterval: 1)
      callback(i)
    }
  }
}

// ViewController.swift

class ViewController: UIViewController {
 // PlaySoundクラスのインスタンス
  let playSound = PlaySound()

  override func viewDidLoad() {
    super.viewDidLoad()
    // [2] インスタンスメソッドをコールする時に関数を渡している
    playSound.play(callback: printNum)
  }
  
  // コールバック関数の実装をここで記述出来る
  private func printNum(num: Int) {
    print(num)
  }
}

[1] 関数 play() はclosure型の引数を取ります。

closureとは、名前の無い関数のことで、以下のように( ) -> ( )のような形で書くことができます。最初の( )にはそのclosureの引数を、->(矢印)の後の( )には返り値を記述します。

( ) はtuple(タプル)という型で、複数の異なる型を入れられます。

ここで面白いのは、play( )関数の引数として渡すclosureの型は指定しますが、closureでどのような処理をするのかは、play( )関数を定義する時には決めなくてもよく、その関数を呼ぶ時に決められる、ということです。

[2] 上記の例ではplay( )に渡すためのprintNum(num: Int)という関数をViewControllerクラスで定義し、play(callback: printNum)という形で渡しています。

(Int) -> Void というclosureの型と printNum(num: Int)は合っている必要があります。

func printNum(num: Int) -> Void { // 返り値がない場合 -> Voidは省略可

}
// closureの書き方の例

// 引数なし、返り値なし
() -> ()
() -> Void

// 引数はInt型、返り値はなし
(Int) -> ()
(Int) -> Void

// 引数はIntとFloat、返り値はIntとBool
(Int, Float) -> (Int, Bool)

次に、Delegateパターンの説明をしますが、その前にProtocolについて理解する必要があります。

Protocolとは、この処理を担当するなら、これらの関数(や変数)は備えといてね、という「規約」を決めるものです。

// PlaySound.swift

// [1] Protocolを定義
protocol PlaySoundDelegate {
  func printNum(num: Int)
}

class PlaySound {

 // [1] Protocol型の変数を宣言
  var playDelegate: PlaySoundDelegate!

  public func play() {
    for i in 0..<10 {
      Thread.sleep(forTimeInterval: 1)
   // ここでProtocolの関数を使う
      playSoundDelegate.printNum(num: i)
    }
  }
}

// ViewController.swift

class ViewController: UIViewController,
                        PlaySoundDelegate // [2]-b   Protocolに準拠
{

  let playSound = PlaySound()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    // [2]-a   delegateをself(ViewControllerクラス)に指定
    playSound.playSoundDelegate = self
    playSound.play()
  
  }
  // [2]-c   Protocolの関数を定義
  func printNum(num: Int) {
    print(num)
  }
}

[1] delegateは「任せる」という意味です。PlaySoundクラスの色々な処理が完了したら、その次に任せたい処理(関数)をProtocolに記述します。Protocolには関数の宣言だけをし、定義はProtocolに準拠するクラスに記述します。

[2] 上記の例ではViewControllerクラスをPlaySoundDelegate Protocolに準拠させることにします。

[2]-a playSound.playSoundDelegate = self はViewControllerクラスがdelegateを担当しますよ、と指定しています。

[2]-b その際、クラスがProtocolに準拠していないとエラーになります。

[2]-c また、Protocolの必須関数を定義していないとエラーになります。

どちらの実装方法でも同じようなことは実現出来るので、まずは一つのやり方にじっくり慣れると、より理解が深まると思います。

カテゴリー
iOS Swift

iOS & Swift 101(初歩)Part 3

ViewController内でUIエレメントの配置に慣れてきたら、次は簡単なclassを作ってみることをおすすめします。

プロジェクトナビゲーターのプロジェクトファイルを右クリック->New File->Swift Fileを選択、ファイル名(class名)を記入して新規ファイルを作成します。

PlaySoundという名前のclassを作ることにし、その中のmember variable(メンバ変数)として、拍子を表すmeterを定義してみます。

class PlaySound {
  
  public var meter: Int = 4
  
}

これをViewControllerで使うには、以下のようにします。

// ViewController.swift

class ViewController: UIViewController {
  
  let playSound = PlaySound()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    print(playSound.meter)
 
  }
}

class PlaySound内に関数を作り、それを介してmeterにアクセスすることも可能です。

変数や関数はAccess Controlを定義することが出来ます。publicは自身以外のclassからもアクセスが可能、privateはそのclass自身以外からのアクセスが不可になります。これは将来的により安全でわかりやすいコードを書く時に役に立ちますが、いまはそれほど気にしなくても良いと思います

// PlaySound.swift

class PlaySound {
  
  private var meter: Int = 4
  
  public func printMeter() {
    print(meter)
  } 

  public func getMeter() -> Int {
    return meter
  }
}
// ViewController.swift

class ViewController: UIViewController {
  
  let playSound = PlaySound()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    // playSoundで定義したprintする関数
    playSound.printMeter()
    // playSoundからInt型の返り値をprintする処理
    print(playSound.getMeter())
 
  }
}
カテゴリー
iOS Swift

Core Data Stackを理解する

Cocoacastの良記事を勉強することでCore Dataについてより理解をしたので、忘れないように記録しておきます。

Core Dataはデータをdisk(デバイスのストレージ)に記録(persist)することが出来ますが、それはCore Dataのひとつの機能であって全てではなく、Core DataはあくまでObject Graphを管理するものです。

XcodeでUse Core Dataをチェックしてプロジェクトを作成すると、AppDelegateに最低限のコードテンプレートが用意されますが、それが逆にCore Dataを分かりにくくしている、とも言えます。

Core Data Stackの「構成部品」をひとつひとつ自分で組んでみると、全体像がより見えてくると言えます。

Core Data Stackは以下の三つから構成されます。

  • NSManagedObjectModel
  • NSPersistentStore (NSPersistentStoreCoordinator)
  • NSManagedObjectContext

これらを順序立てて組み上げて使うためのclass CoreDataManagerを用意し、必要な場面で使用します。

import UIKit
import CoreData

class CoreDataManager {
  
  private let modelName: String

  // [3]
  public typealias CoreDataManagerCompletion = () -> ()
  private let completion: CoreDataManagerCompletion
  
  // [4]
  public func privateChildManagedObjectContext() -> NSManagedObjectContext {
    let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
    managedObjectContext.parent = mainManagedObjectContext
    managedObjectContext.undoManager = UndoManager()
    return managedObjectContext
  }
  
  // [5]
  public func saveChanges() {
    mainManagedObjectContext.performAndWait{
      do {
        if self.mainManagedObjectContext.hasChanges {
          try self.mainManagedObjectContext.save()
        }
      } catch {
        print("Unable to save changes of Main Managed Object Context")
        print("\(error), \(error.localizedDescription)")
      }
    }
    
    privateManagedObjectContext.perform {
      do {
        if self.privateManagedObjectContext.hasChanges {
          try self.privateManagedObjectContext.save()
        }
      } catch {
        print("Unable to save changes of Private Managed Object Context")
        print("\(error), \(error.localizedDescription)")
      }
    }
  }
  
  public init(modelName: String, completion: @escaping CoreDataManagerCompletion) {
    self.modelName = modelName
    self.completion = completion
    setupCoreDataStack()
    setupNotification()
  }
  
  // [1]
  private lazy var managedObjectModel: NSManagedObjectModel = {
    guard let modelURL = Bundle.main.url(forResource: modelName, withExtension: "momd") else {
      fatalError("can't get momd url")
    }
    guard let mom = NSManagedObjectModel(contentsOf: modelURL) else {
      fatalError("failed to create model from file: \(modelURL)")
    }
    return mom
  }()
  
  private lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = {
    let psc = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel)
    // adding persistentStore in a background thread in addPersistentStore() below
    return psc
  }()

  // [3]
  private func addPersistentStore(to persistentStoreCoordinator: NSPersistentStoreCoordinator) {
    let dirURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last
    let fileURL = URL(string: "\(modelName).sqlite", relativeTo: dirURL)
    do {
      try persistentStoreCoordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: fileURL, options: [
        NSMigratePersistentStoresAutomaticallyOption: true,
        NSInferMappingModelAutomaticallyOption: true
      ])
    } catch {
      fatalError("error configuring persistent store: \(error)")
    }
  }
  
  private func setupCoreDataStack() {
    guard let persistentStoreCoordinator = mainManagedObjectContext.persistentStoreCoordinator else {
      fatalError("unable to set up Core Data Stack")
    }
    // [3]
    DispatchQueue.global().async {
      self.addPersistentStore(to: persistentStoreCoordinator)
      DispatchQueue.main.async {
        self.completion()
      }
    }
  }
  
  private func setupNotification() {
    let notificationCenter = NotificationCenter.default
    notificationCenter.addObserver(self, selector: #selector(onNotifyBackground), name: UIApplication.willResignActiveNotification, object: nil)
    notificationCenter.addObserver(self, selector: #selector(onNotifyBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
  }
  
  @objc private func onNotifyBackground(_ notification: Notification) {
    saveChanges()
  }

  // [2]
  private lazy var privateManagedObjectContext: NSManagedObjectContext = {
    let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
    managedObjectContext.persistentStoreCoordinator = persistentStoreCoordinator
    return managedObjectContext
  }()
  
  // [2]
  public private(set) lazy var mainManagedObjectContext: NSManagedObjectContext = {
    let managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
    managedObjectContext.parent = privateManagedObjectContext
    return managedObjectContext
  }()
}

[1] extensionがmomdとなっていますが、これはxcdatamodeldを元にコンパイルされた、より小さな(効率の良い)ファイルです。

[2] NSManagedObjectContextは動作するスレッド(メインスレッドもしくはプライベート(バックグラウンド))を指定出来ます。stack上にcontextは複数あってもよく、例えばdiskに記録するなどの作業はメインスレッドで行われるとUIをブロックしてしまう可能性があるため、バックグラウンドで行うようにするなどの目的別に分けることが出来ます。

[3] persistentStoreCoordinatorにpersistentStoreを追加する際は、状況により少し時間がかかる場合があるため、処理をバックグラウンドで行い、処理完了後の作業をコールバック関数内で行えるようにしています。

[4] mainManagedObjectContextをparentとするprivateChileManagedObjectContextを用意し、UIViewControllerなで使用するためのもの。

[5] 各contextはsave( )する事に、自身とparentに保存内容をpushします。mainManagedObjectContextがsave( )し終わるのを待ち、その後privateManagedObjectContextがsave( )し、diskに記録されるという流れです。

カテゴリー
iOS Swift

UIViewをドラッグしRectangleを描く

よくある、指のドラッグに応じて「選択中」の四角いviewを画面に表示する方法。

指のドラッグに応じてシェイプを描く方法はこのSO記事、DashedUIView(UIViewのBorderを破線 dashed にする)についてはこちらのSO記事を参考にしました。

class ViewController: UIViewController {
  
  var initialPoint: CGPoint = .zero
  
  lazy var overlay: CustomDashedView = {
    let view = CustomDashedView()
    view.dashColor = .systemPink
    view.dashWidth = 1
    view.dashLength = 5
    view.betweenDashesSpace = 2
    return view
  }()
  
  override func viewDidLoad() {
    super.viewDidLoad()

    view.addSubview(overlay)
    let panGesture = UIPanGestureRecognizer(target: self, action: #selector(onViewSelected))
    view.addGestureRecognizer(panGesture)
  }
  
  @objc func onViewSelected(_ sender: UIPanGestureRecognizer) {
    let location = sender.location(in: view)
    if sender.state == .began {
      initialPoint = location
    }
    if sender.state == .changed {
    // comparing the initialPoint and the current location, and get the smaller value for the x and y coordinate, set it as the origin of the rect
   // abs() gives the absolute value
    let rect = CGRect(x: min(initialPoint.x, location.x), y: min(initialPoint.y, location.y), width: abs(initialPoint.x - location.x), height: abs(initialPoint.y - location.y))
      print(rect)
      overlay.frame = rect
    }
    if sender.state == .ended {
      overlay.frame.size = .zero
    }
  }
}


class CustomDashedView: UIView {

    var cornerRadius: CGFloat = 0 {
        didSet {
            layer.cornerRadius = cornerRadius
            layer.masksToBounds = cornerRadius > 0
        }
    }
    var dashWidth: CGFloat = 0
    var dashColor: UIColor = .clear
    var dashLength: CGFloat = 0
    var betweenDashesSpace: CGFloat = 0

    var dashBorder: CAShapeLayer?

    override func layoutSubviews() {
        super.layoutSubviews()
        dashBorder?.removeFromSuperlayer()
        let dashBorder = CAShapeLayer()
        dashBorder.lineWidth = dashWidth
        dashBorder.strokeColor = dashColor.cgColor
        dashBorder.lineDashPattern = [dashLength, betweenDashesSpace] as [NSNumber]
        dashBorder.frame = bounds
        dashBorder.fillColor = nil
        if cornerRadius > 0 {
            dashBorder.path = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius).cgPath
        } else {
            dashBorder.path = UIBezierPath(rect: bounds).cgPath
        }
        layer.addSublayer(dashBorder)
        self.dashBorder = dashBorder
    }
}
カテゴリー
iOS Swift

UIImpactFeedbackGeneratorを使う

iOS機器をクリックする時に、「ピクッ」「プクッ」と若干の振動を起こすためのAPI、UIImpactFeedbackGeneratorの使い方。

class ViewController: UIViewController {
  var impactGenerator: UIImpactFeedbackGenerator!
  override func viewDidLoad() {
    super.viewDidLoad()
    impactGenerator = UIImpactFeedbackGenerator(style: .medium)
    impactGenerator.prepare()
  }

  @objc func onTap() {
    impactGenerator.impactOccured()
  }
}
カテゴリー
iOS Swift

iOS & Swift 101(初歩)Part 2

前回のPart 1と同様に、簡単な画面を作成していきます。

class ViewController: UIViewController {
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    view.backgroundColor = .white
    
    let square = UIView()
    square.backgroundColor = .red
    view.addSubview(square)
    // frame.sizeを指定し
    square.frame.size = CGSize(width: 100, height: 100)
    // 位置情報を指定することも出来る
    square.center = view.center
    
    // UIButtonクラスを使用してみる
    let button = UIButton()
    button.backgroundColor = .green
    button.setTitle("Hello", for: .normal)
    button.setTitleColor(.black, for: .normal)
    // buttonにアクションを追加
    button.addTarget(self, action: #selector(onButtonTapped), for: .touchUpInside)
    view.addSubview(button)
    let buttonSize: CGFloat = 100
    button.frame = CGRect(x: view.bounds.width/2 - buttonSize/2, y: view.bounds.height*3/4 - buttonSize/2, width: buttonSize, height: buttonSize)
  }
  
  // "hello"とprintする簡単なfunction
  // @objcキーワードは、Objective-CというSwiftの前身の言語のAPIを使う場合のもの。button.addTargetを使う場合は必要。
  @objc private func onButtonTapped() {
    print("hello")
  }
}

buttonをクリックすると”hello”とprintされます。

さて、ここで、buttonをクリックすると赤い四角が黄色になる仕組みを組もうとするとどうすればいいでしょうか?

onButtonTapped( )の中にこう書きたいのですが、

@objc private func onButtonTapped() {
    square.backgroundColor = .yellow
}

このままだと「Cannot find ‘square’ in scope」というエラーになります。’square’ という名前のオブジェクトがスコープ内に見つからない、という意味です。

さて、現時点ではsquareはviewDidLoad( )内で定義されています。関数の中で定義された変数(variable, varキーワードで定義)や定数(constant, letキーワードで定義は関数の中でしか扱えません。これを変数や定数の「スコープ」(範囲)といいます。

onButtonTapped( )内で扱えるようにするには、その関数と変数のスコープを合わせます。つまり、同じclass内のトップレベルで定義します。

class ViewController: UIViewController {
  
  let square: UIView = {
    let square = UIView()
    square.backgroundColor = .red
    return square
  }()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    view.backgroundColor = .white
    
    view.addSubview(square)
    square.frame.size = CGSize(width: 100, height: 100)
    square.center = view.center
    
    let button = UIButton()
    button.backgroundColor = .green
    button.setTitle("Hello", for: .normal)
    button.setTitleColor(.black, for: .normal)
    button.addTarget(self, action: #selector(onButtonTapped), for: .touchUpInside)
    view.addSubview(button)
    let buttonSize: CGFloat = 100
    button.frame = CGRect(x: view.bounds.width/2 - buttonSize/2, y: view.bounds.height*3/4 - buttonSize/2, width: buttonSize, height: buttonSize)
  }
  
  @objc private func onButtonTapped() {
    square.backgroundColor = .yellow
  }
}

ボタンをクリックすると黄色に変化するプログラムを書けました!

さて、次に、ほんの少しだけ高度な内容についてですが、iOS開発をする上でとても重要なViewControllerのLifecycleの話をします。

例えばiPhoneでは、画面にこのような部分があります。SafeAreaといって、この部分にはアプリに大きく影響する内容のものは表示しない方がいいでしょう。

このpostに書いていますが、view.safeAreaInsets情報はUIViewControllerのlifecycleの中で得られるタイミングが決まっています。viewDidLoad( )がコールされるタイミングではまだ得られておらず、viewWillLayoutSubviews( )で得られます。

viewDidLoad( )はUIViewControllerの生成時に一度だけ呼ばれるため、view.addSubview( )などをコールする場所としては適していますが、実はframeを設定するなどレイアウト関連の作業を実施するには適していません。また、viewWillLayoutSubviews( )はlifecycleの中で状況に応じて何度もコールされるため、view.addSubview( )を実行するには適していません。

つまり、実行する内容によってコールする場所を考慮する必要がある、ということです。

文字にすると難しそうに見えますが、実際にコードを書いて説明します。以下のコードは意図の通りの結果は得られません。

class ViewController: UIViewController {
  // viewWidth、topInsetはcomputed propertyです
  // 何かしらの計算の結果を返すvariableのことをcomputed propertyと呼びます
  var viewWidth: CGFloat {
    return view.bounds.width
  }
  // 
  var topInset: CGFloat {
    view.safeAreaInsets.top
  }
  
  let square: UIView = {
    let square = UIView()
    square.backgroundColor = .red
    return square
  }()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    view.backgroundColor = .white
    
    view.addSubview(square)
    square.frame = CGRect(x: 0, y: topInset, width: viewWidth, height: 100)
  }
}

下記のようにviewWillLayoutSubviews( )でコールすると、safeAreaInsetsの情報が反映されます。

class ViewController: UIViewController {
  
  var viewWidth: CGFloat {
    return view.bounds.width
  }
  
  var topInset: CGFloat {
    view.safeAreaInsets.top
  }
  
  let square: UIView = {
    let square = UIView()
    square.backgroundColor = .red
    return square
  }()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    view.backgroundColor = .white
    
    view.addSubview(square)
  }
  
  override func viewWillLayoutSubviews() {
    square.frame = CGRect(x: 0, y: topInset, width: viewWidth, height: 100)
  }
}
カテゴリー
iOS Swift

iOS & Swift 101 (初歩) Part 1

最初は取っ付きにくい部分はあまり語らず、出来るだけ直感的に手を動かせるように説明していきます。手を動かして行くと徐々に分かってくると思うからです!

また、後々役に立つため、今回はStoryboardを使用せずに全てコードで書いて行きます。

まずはXcodeのメニューから新しいプロジェクトを作成します。

iOSのApp、を選択し、プロジェクト名(なんでもOK)を入力、InterfaceはStoryboard、LanguageはSwiftを選択してください。

まずはこのまま空のプロジェクトをビルドしてみましょう。ビルド先にSimulatorもしくは実機を選択し、右向きの三角ボタン(プレイボタン)をクリックするか、ショートカットCommand + Rでビルド出来ます。

まっ白なスクリーンが表示されます。

この表示されているものがUIViewControllerです。もう少し正確にいうとViewControllerが標準で持っているviewという「部品」です。

Xcodeの一番左にナビゲーションが表示されているので、ViewControllerという名前のファイルをクリックして開きましょう。ちなみに、この赤く囲っているボタンを押すとナビゲーションを隠したり開いたり出来ます。

ViewController最初は以下のようになっています。「クラスの継承」が最初は分からないかも知れませんが、それは後々説明します。

ちなみに、// の後のテキストは、次に改行されるまで全てコメントとして扱われ、プログラム自体には影響を及ぼしません。プログラムというものは多くの場合、将来の自分が見てもよく分からないことがあります。そのため、どのようなつもり、背景でそう書いたのかを書き残しておくと半年後、一年後の自分に感謝されます ^^ このブログもそのようなきっかけで始めました。。。

import UIKit

// UIViewControllerというクラスを継承(inherit)したViewControllerというクラス
class ViewController: UIViewController {
  // UIViewControllerが持つ標準のfunc
  override func viewDidLoad() {
    super.viewDidLoad()
    
  }
}

ごく簡単に説明すると、iOSのスクリーン上に何かを表示させるためには、最低一つのUIViewController(Root ViewControllerと呼びます)が必要ということです。プロジェクト作成時にはXcodeがViewControllerをひとつ自動で作成してくれます。

viewDidLoadというfunctionは、UIViewControllerが標準で持っている関数ですが、これもまた後々Life Cycle(UIViewControllerが作成されてから破棄されるまで)を解説する時にもう少し詳しく説明します。

実際に手を動かすと徐々に分かってくるので、viewに色をつけてみましょう。viewDidLoad内に以下のように書きます。

class ViewController: UIViewController {
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    view.backgroundColor = .systemPink
  }
}

Command + Rでビルドしてみると、濃いピンクに変更されました。

iOS開発では、このviewに対して別のviewを追加し画面を組んで行きます。

では実際に別のviewを追加してみましょう。以下のように書きます。

class ViewController: UIViewController {
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    view.backgroundColor = .systemPink

    // UIViewクラスのインスタンスを作成しanotherViewという名前をつける
    let anotherView = UIView() 
  // 色をつける
    anotherView.backgroundColor = .systemCyan 
  // 親viewにsub viewとして追加
    view.addSubview(anotherView) 
    // 追加しただけだとまだサイズが指定されていないため表示されないので、CGRectを使って指定する
  anotherView.frame = CGRect(x: 0, y: 0, width: 100, height: 100) 
  }
}

Command + Rでビルドすると、左上コーナーにanotherViewが表示されました。

ここではCGRectというクラスを使ってanotherViewのframeを設定しています。frameは親viewに対してどの位置を起点に、どのサイズで表示するか、というものです。

iOSでは左上コーナーが(x: 0, y: 0)という座標でxは左方向へ、yは下方向へプラスの値ということになっています。(ちなみにmacOSの場合は逆で、左下コーナーが(0, 0)です。なぜでしょうね?中の人の派閥が違うんでしょうか??笑)

では、このCyan(シアン)色の四角を、親viewに対して中央に配置してみましょう。いくつかやり方があるのですが、最も簡単な方法は以下です。

class ViewController: UIViewController {
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    view.backgroundColor = .systemPink
    
    let anotherView = UIView()
    anotherView.backgroundColor = .systemCyan
    view.addSubview(anotherView)
    anotherView.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
    // anotherViewのcenterを親viewのcenterに合わせる
    anotherView.center = view.center
  }
}

ビルドするとCyanの四角が中央に来ました。

別の方法としては、親viewの中のどこに配置するのかを数値で指定するものです。

そのためには親viewのサイズが分かると良さそうです。

先ほどのviewDidLoad内の各ポイントでいくつかの値をprint出力してみたいと思います。

ちなみに、swift標準のprint関数はとても便利です。print関数の引数(arugument)には、コンマ(,)で区切ることで幾つでもオブジェクトをprint出力出来ますし、”文字列”(string)の中に\( )でオブジェクトを囲ってもstringとして出力出来ます。#functionや#lineなどのキーワードを使うことで、どの関数の何行目の出力なのかが分かり、後々デバッグが楽になると思います。ここでは敢えて色々な書き方をしています ^^

class ViewController: UIViewController {
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    print("\(#function) printing view.bounds: ", view.bounds)
    print("printing view.frame: ", view.frame)
    view.backgroundColor = .systemPink
    
    let anotherView = UIView()
    anotherView.backgroundColor = .systemCyan
    view.addSubview(anotherView)
    anotherView.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
    
    print(#line, " printing anotherView.frame: ", anotherView.frame)
    print("\(#line) printing anotherView.bounds: \(anotherView.bounds)")
    
    anotherView.center = view.center
    
    print("\(#line) frame after centered: ", anotherView.frame)
    print("\(#line) bounds after centered: ", anotherView.bounds)
  }
}

ここで注目したいのは、viewのframeとboundsです。簡単に言うと、frameは親viewの中の位置情報を含んだもの、boundsは自分自身におけるサイズです。これらはiOS開発でしょっちゅう使うので、とても大切です。

ビルドすると、以下のように出力されます。太字はコメントです。

view.boundsは親view自身におけるサイズ情報
viewDidLoad() printing view.bounds:  (0.0, 0.0, 390.0, 844.0)
親viewのframeの位置(起点、origin)は、root viewであるため(0.0, 0.0)
printing view.frame:  (0.0, 0.0, 390.0, 844.0)
centeringする前のanotherViewのframe
24  printing anotherView.frame:  (0.0, 0.0, 100.0, 100.0)
centeringする前のanotherViewのbounds
25 printing anotherView.bounds: (0.0, 0.0, 100.0, 100.0)
centering後のanotherViewのframeは、親view内の位置情報を含みます
29 frame after centered:  (145.0, 372.0, 100.0, 100.0)
centering後のboundsは自分自身における情報なので、変わらず
30 bounds after centered:  (0.0, 0.0, 100.0, 100.0)

では、親viewのサイズが分かったので、それを利用してcenter配置してみます。

x軸の中心値はview.bounds.width/2、y軸の中心値はview.bounds.height/2 でゲット出来ます。anotherViewのサイズは100なので、その半分の値100/2を引くことで、anotherViewのorigin(左上コーナー)とするべき値が得られます。ビルドすると同じ結果が得られます。

class ViewController: UIViewController {
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    view.backgroundColor = .systemPink
    
    let anotherView = UIView()
    anotherView.backgroundColor = .systemCyan
    view.addSubview(anotherView)
    // Swiftでは文(statement)の途中で改行も可能です。
    anotherView.frame = CGRect(
      x: view.bounds.width/2 - 100/2,
      y: view.bounds.height/2 - 100/2, width: 100, height: 100
    )
  }
}

同じ要領で、anotherViewの中に別のviewを配置してみましょう。

class ViewController: UIViewController {
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    view.backgroundColor = .systemPink
    
    let anotherView = UIView()
    anotherView.backgroundColor = .systemCyan
    view.addSubview(anotherView)
    anotherView.frame = CGRect(
      x: view.bounds.width/2 - 100/2,
      y: view.bounds.height/2 - 100/2, width: 100, height: 100
    )
    
    let oneMoreView = UIView()
    // .yellowとも書けますが、省略せずに書くとUIColor.yellowです。Swiftは型を推定出来る場合、型を省略して書くことが出来ます。
    oneMoreView.backgroundColor = UIColor.yellow
    // ここではCGFloat型の定数sizeを作成して使っています
    let size: CGFloat = 80
    anotherView.addSubview(oneMoreView)
    oneMoreView.frame = CGRect(x: anotherView.bounds.width/2 - size/2, y: anotherView.bounds.height/2 - size/2, width: size, height: size)
  }
}

黄色のoneMoreViewを円形にしてみましょう。

class ViewController: UIViewController {
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    view.backgroundColor = .systemPink
    
    let anotherView = UIView()
    anotherView.backgroundColor = .systemCyan
    view.addSubview(anotherView)
    anotherView.frame = CGRect(
      x: view.bounds.width/2 - 100/2,
      y: view.bounds.height/2 - 100/2, width: 100, height: 100
    )
    
    let oneMoreView = UIView()
    oneMoreView.backgroundColor = UIColor.yellow
    let size: CGFloat = 80
    anotherView.addSubview(oneMoreView)
    oneMoreView.frame = CGRect(x: anotherView.bounds.width/2 - size/2, y: anotherView.bounds.height/2 - size/2, width: size, height: size)
   // UIViewの持つlayerというオブジェクトのcornerRadiusというattributeにsize/2を入力します。
    oneMoreView.layer.cornerRadius = size/2
  }
}

UIView以外にもUIButtonやUILabelなどさまざまな部品がUIKitフレームワークには用意されていますが、配置の基本は同じです。

Part 2では変数(や定数)のスコープや、UIViewControllerのLife Cycleを説明するために、もう少しだけ機能を追加した画面を作ってみます。

カテゴリー
iOS Swift

UIViewをsubviewごとRotateしたい

sample code

square.transform = CGAffineTransformMakeRotation(CGFloat.pi/2) のようにすると可能

import UIKit

class ViewController: UIViewController {
  
  var viewWidth: CGFloat {
    return view.bounds.width
  }
  
  var viewHeight: CGFloat {
    return view.bounds.height
  }

  lazy var square: UIView = {
    let view = UIView()
    view.backgroundColor = .red
    return view
  }()
  
  lazy var smallSquare: UIView = {
    let view = UIView()
    view.backgroundColor = .blue
    return view
  }()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    view.backgroundColor = .white
    
    view.addSubview(square)
    square.addSubview(smallSquare)
  }
  
  override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()
    layout()
  }
  
  private func layout() {
    let squareWidth: CGFloat = 300
    let squareHeight: CGFloat = 200
    
    let innerSquareWidth: CGFloat = 250
    let innerSquareHeight: CGFloat = 150
    
    square.frame = CGRect(x: viewWidth/2 - squareWidth/2, y: viewHeight/2 - squareHeight/2, width: squareWidth, height: squareHeight)
    smallSquare.frame = CGRect(x: square.bounds.width/2 - innerSquareWidth/2, y: square.bounds.height/2 - innerSquareHeight/2, width: innerSquareWidth, height: innerSquareHeight)
    
    square.transform = CGAffineTransformMakeRotation(CGFloat.pi/2)
  }
}
カテゴリー
AudioKit iOS Swift

AudioKitにSoundFontを読み込みシーケンスの最小構築

gitHub repo

SoundFontのBankが一つの場合は:

func loadMelodicSoundFont(url: URL, preset: Int, in bundle: Bundle = .main) throws {
  try loadSoundFont(url: url, preset: preset, type: kAUSampler_DefaultMelodicBankMSB, in: bundle)
}

Bankがいくつかある場合はtype: IntにBank番号をアサインして使う。

func loadSoundFont(_ file: String, preset: Int, bank: Int, in bundle: Bundle = .main) throws {
  guard let url = findFileURL(file, withExtension: ["sf2", "dls"], in: bundle) else {
  Log("Soundfont file not found: \(file)")
  throw NSError(domain: NSURLErrorDomain, code: NSFileReadUnknownError, userInfo: nil)
  }
  do {
    var bMSB: Int
    if bank <= 127 {
      bMSB = kAUSampler_DefaultMelodicBankMSB
  } else {
    bMSB = kAUSampler_DefaultPercussionBankMSB
  }
  let bLSB: Int = bank % 128
  try samplerUnit.loadSoundBankInstrument(
      at: url,
      program: MIDIByte(preset),
      bankMSB: MIDIByte(bMSB),
      bankLSB: MIDIByte(bLSB)
    )
  } catch let error as NSError {
    Log("Error loading SoundFont \(file)")
    throw error
  }
}
// setup example
private func setup() {
  // load sound font
  loadSF2(name: soundFonts[1], bank: 17, preset: 27, sampler: guitarSampler)
  
  // create a track if the mainSequencer doesn't have one
  if mainSequencer.trackCount < 1 {
    let _ = mainSequencer.newTrack("guitarTrack")
  }
  // add sampler to the mixer
  mixer.addInput(guitarSampler)
  // assign mixer as the engine's output
  engine.output = mixer
  do {
    try engine.start()
  } catch {
    print("error starting the engine: \(error)")
  }
}

public func loadSequence() {
  // time will be tracking the position of the cursor
  var time = 0.0
  // I don't need to set the playhead to zero here...
  // mainSequencer.setTime(0.0)
  let guitarTrackManager = mainSequencer.tracks[0]
  guitarTrackManager.clearRange(start: Duration(beats: time), duration: Duration(beats: 200))
  // adding a note
  guitarTrackManager.add(noteNumber: 50, velocity: 127, position: Duration(beats: time), duration: Duration(beats: 0.5))
  // then, increment the time
  time += 0.5
  // add another note
  guitarTrackManager.add(noteNumber: 52, velocity: 127, position: Duration(beats: time), duration: Duration(beats: 1))
  // connect track to the sampler's midi in
  guitarTrackManager.setMIDIOutput(guitarSampler.midiIn)
    
}
  
public func play() {
  loadSequence()
  mainSequencer.stop()
  // rewind the playhead to the very start
  mainSequencer.setTime(0.0)
  mainSequencer.setTempo(60)
  mainSequencer.play()
}

// in case you want to enable/disable looping  
public func toggleLoop() {
  if mainSequencer.loopEnabled {
    mainSequencer.disableLooping()
  } else {
    mainSequencer.enableLooping()
  }
}