20200328のSwiftに関する記事は3件です。

Swiftでiosアプリ開発-1【個人記録】

概要

「たった2日でマスターできるiPhoneアプリ開発集中講座 Xcode 11 Swift 5対応」
に沿って、Swiftでのiosアプリ開発を学習してみることにしました。その記録用です。

じゃんけんアプリ

楽器アプリ

パーツの配置

楽器アプリには、シンバル、ギター、Play、Stopというパーツがある。
それぞれ、のパーツは押すことによってプログラムが動作することが要求されるため、パーツはそれぞれButtonである。

1.使う画像をAssets.xcassetsに格納する

これで、プログラムやAttributesInspectorから画像を呼び出せるようになる。

2.パーツを配置する

AttributesInspectorから+ボタンで、配置したいパーツを選択し、それをドラッグ&ドロップで配置していく。
スクリーンショット 2020-03-28 18.25.59.png

3.配置されたパーツのレイアウトを変更する

AttributesInspectorで全て行うことができる。
Buttonには、取り込んだImageを反映することもでき、それもここで行う。
スクリーンショット 2020-03-28 18.28.21.png

4.配置されたパーツのAutoLayoutを設定する

AutoLayoutというのは、様々なデバイスで適切にパーツが表示されるための便利な設定みたいなもの。
中央から垂直方向、水平方向にどれだけ距離があるかを設定することで、様々なデバイスで適切な見た目になるようにすることができる。
スクリーンショット 2020-03-28 18.36.36.png
スクリーンショット 2020-03-28 18.37.44.png

水平方向マイナス:左側
水平方向プラス:右側
垂直方向マイナス:上側
垂直方向プラス:下側

に設定した値だけ、中央からパーツがずれていく。
例えば、ギターは、中央からみて、右上に配置したいので、
Horizontally in Container : 80
Vertically in Container : -80
となる。

5.音源を準備する

a.音源ファイルをMyMusic配下に格納する

スクリーンショット 2020-03-28 19.26.50.png

b.AVFoundationを読み込む

import UIKit
//追加
import AVFoundation

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    } 

AVfoundationとは、音や画像を扱いやすくしてくれるフレームワークである。

c.音源ファイルのパスと、AVfoundationのインスタンスを生成する

import UIKit
import AVFoundation

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }
//定数cymbalPathに、音源ファイルのパスを格納。Bundleクラスはファイルや画像を管理しているクラス。
    let cymbalPath = Bundle.main.bundleURL.appendingPathComponent("cymbal.mp3")
//変数cymbalPlayerにAVVudioAVVudioplayerクラスのインスタンスを格納し、そのクラスのメソッドを使えるようにしておく。
    var cymbalPlayer = AVAudioPlayer()

6.パーツとプログラムを関連づける

a.パーツをCtrlキーを押したまま、ドラッグ&ドロップ

b.音を再生するプログラムを作成

import UIKit
import AVFoundation

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }
    let cymbalPath = Bundle.main.bundleURL.appendingPathComponent("cymbal.mp3")
    var cymbalPlayer = AVAudioPlayer()
//追加
    @IBAction func cymbal(_ sender: Any) {
//AVAudioplayerに音源ファイルを指定
    do{
        cymbalPlayer = try AVAudioPlayer(contentsOf:cymbalPath, fileTypeHint:nil)
//音の再生
            cymbalPlayer.play()
        }catch{
            print("シンバルで、エラーが発生しました。")
        }
    }
    let guitarPath = Bundle.main.bundleURL.appendingPathComponent("guitar.mp3")
       var guitarPlayer = AVAudioPlayer()

    @IBAction func guitar(_ sender: Any) {
        do{
               guitarPlayer = try AVAudioPlayer(contentsOf:guitarPath, fileTypeHint:nil)
                   guitarPlayer.play()
               }catch{
                   print("ギターで、エラーが発生しました。")
               }
    }
}

例外処理
Swiftでは、例外が発生しうるクラスを利用する時は、例外処理を明示して書かないとエラーになってしまう。

do {
    try メソッド呼び出し
}catch {
    エラー処理
}

のように書く。
AudioPlayerとかBundleとかクラスがあって、それを継承して、そこのメソッドを使うためにインスタンスを作成して音を鳴らしているというのは理解できるけれど、具体的に各クラスがどのようになっているのかまではわかっていない。
とりあえず今は先に進んで、あとで戻ってきたときに、そこは調べたいと思う。

7.完成形

import UIKit
import AVFoundation

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }
//シンバルの音源のPathと、AVAudioPlayerのインスタンスを作成
    let cymbalPath = Bundle.main.bundleURL.appendingPathComponent("cymbal.mp3")
    var cymbalPlayer = AVAudioPlayer()
//シンバル(ボタン)と関連づけ
    @IBAction func cymbal(_ sender: Any) {
//AVAudioplayerに音源ファイルを指定
    do{
        cymbalPlayer = try AVAudioPlayer(contentsOf:cymbalPath, fileTypeHint:nil)
//音の再生
            cymbalPlayer.play()
        }catch{
            print("シンバルで、エラーが発生しました。")
        }
    }
//ギターの音源のPathと、AVAudioPlayerのインスタンスを作成
    let guitarPath = Bundle.main.bundleURL.appendingPathComponent("guitar.mp3")
    var guitarPlayer = AVAudioPlayer()
//ギター(ボタン)と関連づけ
    @IBAction func guitar(_ sender: Any) {
//AVAudioplayerに音源ファイルを指定
        do{
               guitarPlayer = try AVAudioPlayer(contentsOf:guitarPath, fileTypeHint:nil)
//音の再生
                   guitarPlayer.play()
               }catch{
                   print("ギターで、エラーが発生しました。")
               }
    }
//BGMの音源のPathと、AVAudioPlayerのインスタンスを作成
    let backmusicPath = Bundle.main.bundleURL.appendingPathComponent("backmusic.mp3")
    var backmusicPlayer = AVAudioPlayer()
//Play(ボタン)との関連づけ
    @IBAction func play(_ sender: Any) {
//AVAudioplayerに音源ファイルを指定

        do{
            backmusicPlayer = try AVAudioPlayer(contentsOf: backmusicPath, fileTypeHint: nil)
//ボタン押下後の再生回数を定義
            backmusicPlayer.numberOfLoops = -1
//音の再生

            backmusicPlayer.play()
        }catch{
            print("エラーが発生しました。")
        }
    }
//Stop(ボタン)と関連づけ
    @IBAction func stop(_ sender: Any) {
//BGMの停止
        backmusicPlayer.stop()
    }
}

image.png

8.リファクタリング

共通部分(音楽再生やパスをAVAudioPlayerに渡したりするところ)が多いので、そこをメソッドにしてしまう。
あと、定数や変数の定義は、最初にしてしまった方がみやすい。

import UIKit
import AVFoundation

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }
//共通部分をメソッド化
    fileprivate func soundPlayer(player:inout AVAudioPlayer,path:URL,count: Int){
        do{
            player = try AVAudioPlayer(contentsOf: path, fileTypeHint: nil)
            player.numberOfLoops = count
            player.play()
        }catch{
            print("エラーが発生しました")
        }
    }
//シンバルの音源のPathと、AVAudioPlayerのインスタンスを作成
    let cymbalPath = Bundle.main.bundleURL.appendingPathComponent("cymbal.mp3")
    var cymbalPlayer = AVAudioPlayer()
//ギターの音源のPathと、AVAudioPlayerのインスタンスを作成
    let guitarPath = Bundle.main.bundleURL.appendingPathComponent("guitar.mp3")
    var guitarPlayer = AVAudioPlayer()
//BGMの音源のPathと、AVAudioPlayerのインスタンスを作成
    let backmusicPath = Bundle.main.bundleURL.appendingPathComponent("backmusic.mp3")
    var backmusicPlayer = AVAudioPlayer()
//シンバル(ボタン)と関連づけ
    @IBAction func cymbal(_ sender: Any) {
    soundPlayer(player: &cymbalPlayer, path: cymbalPath, count: 0)
    }
//ギター(ボタン)と関連づけ
    @IBAction func guitar(_ sender: Any) {
    soundPlayer(player: &guitarPlayer, path: guitarPath, count: 0)
    }
//Play(ボタン)との関連づけ
    @IBAction func play(_ sender: Any) {
    soundPlayer(player: &backmusicPlayer, path: backmusicPath, count: -1)
    }
//Stop(ボタン)と関連づけ
    @IBAction func stop(_ sender: Any) {
//BGMの停止
        backmusicPlayer.stop()
    }
}

メソッド定義部分

fileprivate func soundPlayer(player:inout AVAudioPlayer,path:URL,count: Int)

fileprivate・・・アクセス修飾詞といって、このファイルの中でのみ呼び出せるメソッドになる。
player:inout AVAudioPlayer・・・AVAudioPlayerクラスの変数を、受け取って、最終的にこの変数を戻り値として返すよ、という意味。returnを明記する必要がなくなる。

呼び出し部分

@IBAction func play(_ sender: Any) {
    soundPlayer(player: &backmusicPlayer, path: backmusicPath, count: -1)
    }

soundPlayerにそれぞれ3つの引数をわたしている。
左からインスタンス、音源ファイルのパス、そして繰り返す回数である。(-1はループになる)

参照渡し
playerという引数は、参照渡しという方法でメソッド側に値が渡されている。
呼び出し元の変数がメソッドの影響を受けるとき、参照渡しが行われる。

参照渡しと値渡しの例
http://swift-salaryman.com/inout.php
より引用

import UIKit
class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        var testInt = 0;
        println(testInt);//結果ー>0
        sanshouWatashi_NO(testInt);
        println(testInt);//結果ー>0
        sanshouWatashi_YES(&testInt);
        println(testInt);//結果ー>1
    }

    //参照渡しではない通常(呼び出し元影響受けない)
    func sanshouWatashi_NO(var param1:Int){
        param1 += 1;
    }

    //参照渡し(呼び出し元影響受ける)
    func sanshouWatashi_YES(inout param1:Int){
        param1 += 1;
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
}

cf.AVAudioPlayer

https://developer.apple.com/documentation/avfoundation/avaudioplayer
AVAudioPlayerクラスで使えるメソッド等が公開されている。

感想

超シンプルじゃんけんアプリと音楽再生アプリを作成してみて、なんとなく流れを掴めた。
まずはパーツを配置して、そのパーツの大きさやフォーマットを整える。
そしてそのパーツとプログラムをリンクさせる。
とりあえずまずは、この本を最後までやりたい。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

初心者によるRxSwiftのデモ

RxSwiftとは

RxSwiftの前身はMicrosoftが開発した.NET用のライブラリ「Reactive Extensions」です。このReactive Extensionsの概念が有効だったため、数々のプログラミング言語へと移植されました。その中でSwiftに移植されたのがRxSwiftです。

リアクティブプログラミングとは

リアクティブプログラミングとは「値の変化」と、それに対する「振る舞い」の関係を宣言的に記述するプログラミングの一種です。
例えばボタンを押すとラベルの文字が変化する、のようなユーザーインタラクティブなシステムや、通信処理などの非同期処理などで簡潔にかけるため特に有用です。数々の言語へ移植されている点がその有用性の証明とも言えます。

SwiftではiOS13専用で公式よりCombineというフレームワークがリリースされましたが、これはRxSwiftの概念を多く真似して作られているようです。

Combineを理解するための前提として、また簡潔なコードを書くためRxSwiftを始めてみようと思います。

参考書籍: 比較して学ぶRxSwift入門

カウンターデモアプリ

以下のように、ボタンを押すとラベルの数字が増減、または0にリセットされるデモアプリを作ってみようと思います。

截圖 2020-03-28 15.18.52.png

https://github.com/Satoru-PriChan/RxSwiftCounterApp

コールバックで実装

比較対象としてコールバックで実装してみます。

CounterViewModel.swift
import Foundation
class CounterViewModel {
    private(set) var count = 0

    func incrementCount(callback: (Int) -> ()) {
        count += 1
        callback(count)
    }

    func decrementCount(callback: (Int) -> ()) {
        count -= 1
        callback(count)
    }

    func resetCount(callback: (Int) -> ()) {
        count = 0
        callback(count)
    }
}
ViewController.swift
import UIKit

class ViewController: UIViewController {

    @IBOutlet private weak var countLabel: UILabel!
    private var viewModel: CounterViewModel!


    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.

        viewModel = CounterViewModel()
    }


    @IBAction func countUp(_ sender: UIButton) {
        viewModel.incrementCount(callback: { [weak self] count in
            self?.updateCountLabel(count)
        })
    }

    @IBAction func countDown(_ sender: UIButton) {
        viewModel.decrementCount(callback: { [weak self] count in
            self?.updateCountLabel(count)
        })
    }

    @IBAction func reset(_ sender: UIButton) {
        viewModel.resetCount(callback: { [weak self] count in
            self?.updateCountLabel(count)
        })
    }

    private func updateCountLabel(_ count: Int) {
        countLabel.text = String(count)
    }
}

ボタンに対応するメソッドが一つずつある(IBAction)ので、現段階ではシンプルですが、ボタンが増えた場合は見にくくなります。

また、CounterViewModelの側で、イベント処理の時にcallbackを一々呼ばなくてはなりません。

デリゲートパターンで実装

CounterPresenter.swift
protocol CounterDelegate: class {
    func updateCount(count: Int)
}

class CounterPresenter {
    private var count = 0 {
        didSet  {
            delegate?.updateCount(count: count)
        }
    }

    private var delegate: CounterDelegate?

    func attachView(_ delegate: CounterDelegate) {
        self.delegate = delegate
    }

    func detachView() {
        self.delegate = nil
    }

    func incrementCount() {
        count += 1
    }

    func decrementCount() {
        count -= 1
    }

    func resetCount() {
        count = 0
    }
}
ViewController.swift
import UIKit

class ViewController: UIViewController {

    @IBOutlet private weak var countLabel: UILabel!

    private let presenter = CounterPresenter()


    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.

        presenter.attachView(self)
    }


    @IBAction func countUp(_ sender: UIButton) {
        presenter.incrementCount()
    }

    @IBAction func countDown(_ sender: UIButton) {
        presenter.decrementCount()
    }

    @IBAction func reset(_ sender: UIButton) {
        presenter.resetCount()
    }
}

extension ViewController: CounterDelegate {
    func updateCount(count: Int) {
        countLabel.text = String(count)
    }
}

コールバックを呼び出す手間は無くなりました。

ボタン一つに対しIBAction一つが対応している点はコールバックと同じです。

RxSwiftで実装

CounterViewModel.swift
import Foundation
import RxSwift
import RxCocoa

struct CounterViewModelInput {
    let countUpButton: Observable<Void>
    let countDownButton: Observable<Void>
    let countResetButton: Observable<Void>
}

protocol CounterViewModelOutput {
    var counterText: Driver<String?> { get }
}

protocol CounterViewModelType {
    var outputs: CounterViewModelOutput? { get }
    func setup(input: CounterViewModelInput)
}

class CounterRxViewModel: CounterViewModelType {
    var outputs: CounterViewModelOutput?

    private let countRelay = BehaviorRelay<Int>(value: 0)
    private let initialCount = 0
    private let disposeBag = DisposeBag()

    init() {
        self.outputs = self
        resetCount()
    }

    func setup(input: CounterViewModelInput) {
        input.countUpButton
            .subscribe(onNext: { [weak self] in
            self?.incrementCount()
            })
            .disposed(by: disposeBag)

        input.countDownButton
            .subscribe(onNext: { [weak self] in
            self?.decrementCount()
            })
            .disposed(by: disposeBag)

        input.countResetButton
            .subscribe(onNext: { [weak self] in
            self?.resetCount()
            })
            .disposed(by: disposeBag)
    }

    private func incrementCount() {
        let count = countRelay.value + 1
        countRelay.accept(count)
    }

    private func decrementCount() {
        let count = countRelay.value - 1
        countRelay.accept(count)

    }

    private func resetCount() {
        countRelay.accept(initialCount)
    }
}

extension CounterRxViewModel: CounterViewModelOutput {
    var counterText: Driver<String?> {
        return countRelay
            .map { "Rx pattern: \($0)" }
            .asDriver(onErrorJustReturn: nil)
    }
}
ViewController.swift
import UIKit
import RxSwift

class ViewController: UIViewController {

    @IBOutlet private weak var countLabel: UILabel!
    @IBOutlet weak var countUpButton: UIButton!
    @IBOutlet weak var countDownButton: UIButton!
    @IBOutlet weak var resetButton: UIButton!

    private let disposeBag = DisposeBag()

    private var viewModel: CounterRxViewModel!


    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.

        setupViewModel()
    }

    func setupViewModel() {
        viewModel = CounterRxViewModel()

        let input = CounterViewModelInput(countUpButton: countUpButton.rx.tap.asObservable(),
                                          countDownButton: countDownButton.rx.tap.asObservable(),
                                          countResetButton: resetButton.rx.tap.asObservable())

        viewModel.setup(input: input)

        viewModel.outputs?.counterText
            .drive(countLabel.rx.text)
            .disposed(by: disposeBag)
    }
}

ViewControllerにおいて、ボタンと反応するメソッドが一対一では無くなり、見やすくなっています。

ViewModelにおいて、delegate?.updateCountのようにデータ更新の通知を行わなくても良くなっています。イベントが発生した場合、自動で通知が流れていきます。

比較的記述量が多い問題があります。極めて規模の小さいプロジェクトには向かないかもしれません。

WebViewアプリ

次はプログレスバー、アクティビティインジケータ(グルグル回る物)を備えたブラウザアプリを実装します。

截圖 2020-03-28 17.55.32.png
截圖 2020-03-28 17.55.49.png

https://github.com/Satoru-PriChan/RxSwiftWKWebViewDemo

通常のKVOパターンと、RxSwiftを使って実装し比較します。

KVOパターンで実装

KVOとはkey value observingの略で、プロパティの値の変化を監視する仕組みのことを言います(参照 https://qiita.com/ObuchiYuki/items/d00ce5f44725672184da

ViewController.swift
import UIKit
import WebKit

class ViewController: UIViewController {

    @IBOutlet weak var webView: WKWebView!
    @IBOutlet weak var progressView: UIProgressView!
    @IBOutlet weak var activityIndicator: UIActivityIndicatorView!

    private var observers = [NSKeyValueObservation]()

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.

        observers.append(webView.observe(\.isLoading,
                                         options: .new,
                                         changeHandler: { [weak self] (webView, change) in
            //observer webView's estimatedProgress

                                            guard change.newValue != nil else { return }

            if change.newValue ?? false {
                //when loading
                if self?.activityIndicator.isAnimating == false {
                    self?.activityIndicator.startAnimating()
                }

            } else {
                self?.activityIndicator.stopAnimating()

                //when load completed, set progress 0.0 in progressView.
                self?.progressView.setProgress(0.0, animated: false)

                //when load is completed, set title of loaded page as NavigationTitle.
                self?.title = webView.title
            }
        }))

        observers.append(webView.observe(\.estimatedProgress,
                                         options: .new,
                                         changeHandler: {[weak self] (_, change) in

                                            guard change.newValue != nil else { return }
                                            self?.progressView.setProgress(Float(change.newValue!), animated: true)
        }))
    }

    override func viewWillAppear(_ animated: Bool) {
        loadWebView()
    }

    private func loadWebView() {
        let url = URL(string: "https://www.google.com/")
        let urlRequest = URLRequest(url: url!)
        webView.load(urlRequest)
        progressView.setProgress(0.1, animated: true)

    }
}

observeメソッド内で、値の変化時に必要な処理を全て書かなければいけないので、少し読みづらくなっています。

RXSwift + RxWebKitで実装

ViewController.swift
import UIKit
import WebKit
import RxSwift
import RxCocoa
import RxOptional
import RxWebKit

class ViewController: UIViewController {

    @IBOutlet weak var webView: WKWebView!
    @IBOutlet weak var progressView: UIProgressView!
    @IBOutlet weak var activityIndicator: UIActivityIndicatorView!

    private let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.

        setupWebView()
    }

    private func setupWebView() {
        //define observer
        let loadingObservable = webView.rx.loading
                                .share()

        //display or hide progress bar
        loadingObservable
            .map { return !$0 }
            .observeOn(MainScheduler.instance)
            .bind(to: progressView.rx.isHidden)
            .disposed(by: disposeBag)

        //activity indicator
        loadingObservable.subscribe(onNext: {[weak self] (isLoading) in
            if isLoading {
                //start animating
                if self?.activityIndicator.isAnimating == false {
                        self?.activityIndicator.startAnimating()
                }
            } else {
                self?.activityIndicator.stopAnimating()
            }
            }).disposed(by: disposeBag)

        //navigationbar title
        loadingObservable
            .map { [weak self] _ in return self?.webView.title }
            .bind(to: navigationItem.rx.title)
            .disposed(by: disposeBag)

        //progress bar
        webView.rx.estimatedProgress
            .map { return Float($0) }
            .observeOn(MainScheduler.instance)
            .bind(to: progressView.rx.progress)
            .disposed(by: disposeBag)
    }

    override func viewWillAppear(_ animated: Bool) {
        loadWebView()
    }

    private func loadWebView() {
        let url = URL(string: "https://www.google.com/")
        let urlRequest = URLRequest(url: url!)
        webView.load(urlRequest)
        progressView.setProgress(0.1, animated: true)

    }
}

ネストが浅くなり、読みやすくなりました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOSアプリのMultitasking対応について

はじめに

WWDC2019にて2020年4月までにMultitaskingをサポートすることが必須と発表されました。
そして今年1月、必須ではないにしてもMultitaskingをサポートすることを 強くお勧めする とアナウンスがありました。
もちろん対応しなければアプリをリジェクトされるわけではありませんが、今後Multitaskingが重要となっていくことは明らかです。
そこで今回はMultitaskingの対応方法および、注意すべき点についてまとめていきます。
iPhoneとiPadに適したユーザーインターフェイスの構築

Mutitasking設定方法

  1. Requires full screenにチェックが入っている場合は外します スクリーンショット 2020-03-28 15.17.59.png
  2. Device Orientationを4つ全ての方向に対応するようにチェックします スクリーンショット 2020-03-28 15.17.59 2 2.png
  3. もし、iPadのみで全ての方向に対応するようにする場合はGeneralDevice Orientationは弄らず、info.plistからSupported interface orientations (iPad)にて4つの方向全て対応するよう設定して下さい スクリーンショット 2020-03-28 15.18.32.png
  4. 最後に、もし起動画面をStoryboardではなくimageで作成している場合はLaunch Screen FileにてLaunch用のStoryboardを作成して設定します スクリーンショット 2020-03-28 15.17.59 2 3.png
  5. あとはiPadでビルドすればMutliTaskingに対応していることを確認することができます

Mutitasking対応方法

Mutitaskingの対応すると今までにはなかったSplit表示になる(画面分割される)タイミングなどでレイアウトを更新する必要が出てきます。
AutoLayoutにレイアウトの計算を全て任せることができていれば大きく対応する必要はありませんが、手動で計算をしている箇所等についてはこちらで意図的に再計算を走らせる必要があります。
もちろん、レイアウトを手動計算する方法で対応するよりもAutolayoutにて対応するのが理想ではあるとは思いますが、そう簡単に対応できるわけではありませんので、手動でのレイアウトが必要な場合の対応方法について記載します。

レイアウト再計算処理タイミングの見直し

ViewControllerのLifececle関数にてMultitaskingによる画面サイズの変更を検知してくれるのは大きく以下の2つです。

viewWillTransition(to:with:)

Multitasking(および画面回転)による画面サイズの変更はviewWillTransition(to:with:)にて検知できます。
また、サイズ変更後のアプリのWindowサイズは引数のsizeの中に格納されています。

/// 画面回転時、およびMultitaskingによる画面サイズ変更時に呼ばれる
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)
    coordinator.animate(alongsideTransition: { _ in
        // 遷移アニメーション時に行う処理を記述
    }, completion: { _ in
        // 遷移終了後に行う処理を記述
    })
}

viewDidLayoutSubviews()

viewDidLayoutSubviewsでも画面サイズの変更を検知することができます。
ただし、こちらはMultitaskingによる画面サイズ変更のみでなく、レイアウトが更新されるタイミング全てで呼ばれますので、コストがかなりかかってしまう可能性があります。
使用する場合は慎重に決めて下さい。

UIScreen.main.boundsについて

UIScreen.main.bounds、幅や高さ計算で何かと便利で使ってしまいがちですが、これを使用してframe計算をしている箇所についてはほぼ全て修正する必要があります。
UIScreen.main.boundsは、あくまで 端末のサイズ を取得するモノで、 アプリの表示サイズ を取得するモノではありません。
SplitViewで表示される際はアプリの表示サイズが端末サイズよりも小さい状態で表示される場合がありますので、UIScreen.main.boundsを使用しているとレイアウトが崩れる場合があります。
その場合は下記の方法にて、現在表示されているアプリサイズ(Window)のboundsをとってくることができますので、必要に応じてUIScreen.main.boundsの箇所を書き換えることで対応できます。

/// アプリの現在表示されているWindow(KeyWindow)のサイズを取得してくる
UIApplication.shared.windows.first(where: { $0.isKeyWindow })?.bounds

/// 下記でも同様の結果を取得できるが、keyWindowがiOS13にてdisabledとなっているので使用しない
UIApplication.shared.keyWindow?.bounds

おわりに

以上で基本的にはMutltaskingに対応することができると思います。
対応自体は比較的単純なのですが、規模が大きいアプリはカバーする画面数が多く大変になりがちです。
しかし、今後Multitaskingの重要性も増していくことと思いますので、是非この機会に対応してみてはいかがでしょうか?

参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む