20190215のiOSに関する記事は9件です。

iOS 12.2 beta で PWA 対応が進展してたらしい

iOSの対応状況ってどんなだろうと調べたら割とタイムリーだったので。

雑にリンクをシェアする

https://medium.com/@firt/pwas-on-ios-12-2-beta-the-good-the-bad-and-the-not-sure-yet-if-good-a37b6fa6afbf
https://medium.com/dev-channel/progressive-web-app-progress-in-ios-12-2-beta-1-build-16e5181f-a18cd05ca361

どんな

詳しくは上のリンクを。以下特に確かめてません。

  • Web Share API level 1
    • 知らなかったけどこういうセマンティックなAPI好き
  • マルチタスク時にも状態を維持
    • Page Lifecycle API未実装
    • 例えばバックグラウンドになったイベント、戻ってきたイベントで処理を発火させることなどができない
  • Abortable Fetch
    • fetchをタイムアウトさせられる。
  • ジェスチャーによるナビゲーション(右にスワイプで戻るとか)
  • カラーピッカー的なフォーム
  • Experimental で気になったやつ
    • Media Recorder
    • Web Authentication

など。
こういう記事を書くととりあえずそれが何者であるか程度は調べるので健康に良いですね。

雑リンク(参考):11.3のとき

https://medium.com/@takeshiamano/ios%E3%81%AE11-3%E3%81%8B%E3%82%89%E3%81%AEpwa%E5%AF%BE%E5%BF%9C%E3%81%A7%E3%81%A7%E3%81%8D%E3%82%8B%E3%82%88%E3%81%86%E3%81%AB%E3%81%AA%E3%81%A3%E3%81%9F%E3%81%93%E3%81%A8-313f638a172b

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

iOSでAndroidのFragmentのような実装をする (iOSで画面入れ替え)

はじめに

iOSでAndroidのFragmentのような実装をしたいと考えました。
AndroidのFragmentのような…というのは、ここでは1画面の中に固定View(Activity)と複数可変View(Fragment)があり、可変Viewがそれぞれ入れ替わるようなイメージしています。
image.jpeg

実現方法

今回は、ボタンを押したら2つのViewが切り替わるサンプルをつくりました。

storyboard

image.png
このような構造にしました。薄い青いViewがContainerです。

ViewController

Swiftは4.2です。

ViewController.swift
class ViewController: UIViewController {

    @IBOutlet weak var container:UIView!
    let redView:UIView = UINib(nibName: "RedView", bundle:nil).instantiate(withOwner: self, options: nil)[0] as! UIView
    let blueView:UIView = UINib(nibName: "BlueView", bundle:nil).instantiate(withOwner: self, options: nil)[0] as! UIView
    var changeFlag = false

    override func viewDidLoad() {
        super.viewDidLoad()
        changeView()
    }


    func changeView() {
        let width = self.container.bounds.size.width//containerの横幅を取得
        let height = self.container.bounds.size.height//containerの縦幅を取得
        let frame = CGRect(x:0, y:0, width:width, height:height)//containerの幅に設定

        //フラグによってViewをaddSubView、removeFromSuperviewして入れ替える
        if changeFlag {
            blueView.removeFromSuperview()
            redView.frame = frame
            container.addSubview(redView)
            changeFlag = false
        } else {
            redView.removeFromSuperview()
            blueView.frame = frame
            container.addSubview(blueView)
            changeFlag = true
        }
    }

    @IBAction func click(sender:AnyObject){
        changeView()
    }
}

addSubViewと、removeFromSuperviewを用いてContainerの中にViewを追加したり削除したりし、Viewの入れ替えを実現しました 。

DEMO

demo.gif

このように、ボタンを押したら青と赤のViewが入れ替わります!

全ソース

サンプルコードはこちらに置いてあります!
https://github.com/mii-chang/ChangeViewSample

参考になれば嬉しいです◎!

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

最新バージョンのdSYMアップロードをほぼ自動化する

fastlaneのdownload_dsymsupload_symbos_to_crashlyticsを組み合わせれば,dSYMのダウンロードからアップロードまでを簡単に自動化できます.
ただし,bitcodeを有効にしている場合には,App Store Connect上での処理完了を待つ必要があります.

メール受信をトリガにする方法などもありますが,自分のチームではもう少しお手軽な方法で(ほぼ)自動化しているので,そのやり方を紹介します.

まずは以下のlaneを作成します.

Fastfile
lane :refresh_dsyms do |options|
  version = options[:version] || 'latest'

  download_dsyms(
    version: version
  )

  upload_symbols_to_crashlytics(
    api_token: ENV['CRASHLYTICS_API_TOKEN']
  )

  clean_build_artifacts
end

作成したlaneは以下のように使用します.

# 最新バージョンのdSYMをアップロード
$ fastlane refresh_dsyms

# 任意のバージョンのdSYMをアップロード
$ fastlane refresh_dsyms version:1.2.3

自分のチームでは,これらを以下のようなやり方で運用しています.

  1. 毎週,CI上で fastlane refresh_dsyms を実行し、最新バージョンのdSYMに更新
    • 毎週1回のペース(金曜に申請し、翌週リリース)で定期的にリリースをしているので、
      土曜0時に実行すれば最新版のdSYMに更新できる
  2. 非定期リリース(Hotfixなど)があれば,ローカルからバージョンを指定(= version: x.x.xを指定)してdSYMを更新

ローカルでの実行も混ざり得るのでもっといい感じの運用にしたいところではありますが,今のところ定期的なリリースサイクルを回せているので,手動での更新はほとんど必要ないです.

チームのスケジュールを考慮することで,完全自動化までは行かなくとも,それに近い結果を手軽得ることができます.

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

[iOS]Swift4で音声の入力・出力をする方法

はじめに

この記事では、マイクから音声を入力して一旦ファイル化し、その音をスピーカーから出力するまでの処理をSwift4で書いてみることにします。
Swiftのアップデートが早くてSwift4もすぐにオワコンになってしまうかもしれないですが、自分用の備忘も兼ねて記録しておきます:grinning:

使ったもの

  • Xcode 10.1
  • Swift 4.2
  • AVFoundation

共通で必要な処理の実装

AVFoundationのインスタンスを生成する
let session = AVAudioSession.sharedInstance()
再生と録音をすることを宣言してからアクティブ化する
try session.setCategory(.playAndRecord, mode: .default)
try session.setActive(true)

音声入力の実装

まず録音で使用するAVAudioRecorderを宣言する
var audioRecorder: AVAudioRecorder!
録音フォーマットの設定をする
let settings = [
    AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
    AVSampleRateKey: 44100,
    AVNumberOfChannelsKey: 2,
    AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
]
AVAudioRecorderインスタンスを生成する
audioRecorder = try AVAudioRecorder(url: getAudioFileUrl(), settings: settings)
audioRecorder.delegate = self as? AVAudioRecorderDelegate
func getAudioFileUrl() -> URL {
    let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
    let docsDirect = paths[0]
    let audioUrl = docsDirect.appendingPathComponent("recording.m4a")

    return audioUrl
}
ボタンを押したら録音開始(録音中に押したら録音停止)
@IBAction func record(_ sender: Any) {
    if !audioRecorder.isRecording {
        audioRecorder.record()
    } else {
        audioRecorder.stop()
    }
}

音声出力の実装

まず再生で使用するAVAudioEngine、AVAudioFile、AVAudioPlayerNodeを宣言する
var audioEngine: AVAudioEngine!
var audioFile: AVAudioFile!
var audioPlayerNode: AVAudioPlayerNode!
AVAudioEngine、AVAudioFile、AVAudioPlayerNode各々のインスタンスを生成する
audioEngine = AVAudioEngine()
audioFile = try AVAudioFile(forReading: getAudioFileUrl())
audioPlayerNode = AVAudioPlayerNode()
//上で書いたfuncと同じ
func getAudioFileUrl() -> URL {
    let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
    let docsDirect = paths[0]
    let audioUrl = docsDirect.appendingPathComponent("recording.m4a")

    return audioUrl
}
Nodeのつなぎ合わせをする
audioEngine.attach(audioPlayerNode)
audioEngine.connect(audioPlayerNode, to: audioEngine.outputNode, format: audioFile.processingFormat)
ボタンを押したら再生開始(再生中に押したら初めから再生)
@IBAction func play(_ sender: Any) {
    do {
    //中略(インスタンス生成やNodeのつなぎ合わせはここでやる)
        audioPlayerNode.stop()
        audioPlayerNode.scheduleFile(audioFile, at: nil)

        try audioEngine.start()
        audioPlayerNode.play()
    } catch let error {
        print(error)
    }
}

通しで書くとこんな感じ

import UIKit
import AVFoundation

class ViewController: UIViewController {

    var audioRecorder: AVAudioRecorder!
    var audioEngine: AVAudioEngine!
    var audioFile: AVAudioFile!
    var audioPlayerNode: AVAudioPlayerNode!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.

        let session = AVAudioSession.sharedInstance()

        do {
            try session.setCategory(.playAndRecord, mode: .default)
            try session.setActive(true)

            let settings = [
                AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
                AVSampleRateKey: 44100,
                AVNumberOfChannelsKey: 2,
                AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
            ]

            audioRecorder = try AVAudioRecorder(url: getAudioFileUrl(), settings: settings)
            audioRecorder.delegate = self as? AVAudioRecorderDelegate
        } catch let error {
            print(error)
        }

    }

    @IBAction func record(_ sender: Any) {
        if !audioRecorder.isRecording {
            audioRecorder.record()
        } else {
            audioRecorder.stop()
        }
    }


    @IBAction func play(_ sender: Any) {
        audioEngine = AVAudioEngine()
        do {
            audioFile = try AVAudioFile(forReading: getAudioFileUrl())

            audioPlayerNode = AVAudioPlayerNode()
            audioEngine.attach(audioPlayerNode)
            audioEngine.connect(audioPlayerNode, to: audioEngine.outputNode, format: audioFile.processingFormat)

            audioPlayerNode.stop()
            audioPlayerNode.scheduleFile(audioFile, at: nil)

            try audioEngine.start()
            audioPlayerNode.play()
        } catch let error {
            print(error)
        }
    }

    func getAudioFileUrl() -> URL {
        let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
        let docsDirect = paths[0]
        let audioUrl = docsDirect.appendingPathComponent("recording.m4a")

        return audioUrl
    }

}

こんなクラスファイルがあると実装が楽

この辺のユーティリティと言うほどではないけれど、いろんな画面から呼び出しそうな処理はこんな感じで一箇所にまとめて外出ししてしまった方がコードが冗長にならなくていいなあと思います:ok_woman:
あくまで一例ですが…

AudioManager.swift
import UIKit
import AVFoundation

final class AudioManager {

    var audioRecorder: AVAudioRecorder!
    var audioEngine: AVAudioEngine!
    var audioFile: AVAudioFile!
    var audioPlayerNode: AVAudioPlayerNode!
    var audioUnitTimePitch: AVAudioUnitTimePitch!

    init() {}

    func setUpAudioRecorder() {
        let session = AVAudioSession.sharedInstance()

        do {
            try session.setCategory(.playAndRecord, mode: .default)
            try session.setActive(true)

            let settings = [
                AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
                AVSampleRateKey: 44100,
                AVNumberOfChannelsKey: 2,
                AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
            ]

            audioRecorder = try AVAudioRecorder(url: getAudioFilrUrl(), settings: settings)
            audioRecorder.delegate = self as? AVAudioRecorderDelegate
        } catch let error {
            print(error)
        }
    }

    func playSound(speed: Float, pitch: Float, echo: Bool, reverb: Bool) {
        audioEngine = AVAudioEngine()

        let url = getAudioFilrUrl()

        do {
            audioFile = try AVAudioFile(forReading: url)

            audioPlayerNode = AVAudioPlayerNode()
            audioEngine.attach(audioPlayerNode)

            audioUnitTimePitch = AVAudioUnitTimePitch()
            audioUnitTimePitch.rate = speed
            audioUnitTimePitch.pitch = pitch
            audioEngine.attach(audioUnitTimePitch)

            let echoNode = AVAudioUnitDistortion()
            echoNode.loadFactoryPreset(.multiEcho1)
            audioEngine.attach(echoNode)

            let reverbNode = AVAudioUnitReverb()
            reverbNode.loadFactoryPreset(.cathedral)
            reverbNode.wetDryMix = 50
            audioEngine.attach(reverbNode)

            if echo && reverb {
                connectAudioNodes(audioPlayerNode, audioUnitTimePitch, echoNode, reverbNode, audioEngine.outputNode)
            } else if echo {
                connectAudioNodes(audioPlayerNode, audioUnitTimePitch, echoNode, audioEngine.outputNode)
            } else if reverb {
                connectAudioNodes(audioPlayerNode, audioUnitTimePitch, reverbNode, audioEngine.outputNode)
            } else {
                connectAudioNodes(audioPlayerNode, audioUnitTimePitch, audioEngine.outputNode)
            }

            audioPlayerNode.stop()
            audioPlayerNode.scheduleFile(audioFile, at: nil)

            try audioEngine.start()
            audioPlayerNode.play()
        } catch let error {
            print(error)
        }
    }

    private func connectAudioNodes(_ nodes: AVAudioNode...) {
        for x in 0..<nodes.count - 1 {
            audioEngine.connect(nodes[x], to: nodes[x+1], format: audioFile.processingFormat)
        }
    }

    func getAudioFilrUrl() -> URL {
        let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
        let docsDirect = paths[0]
        let audioUrl = docsDirect.appendingPathComponent("recording.m4a")

        return audioUrl
    }
}

おわりに

単に音声の入出力だけでも結構色々やらなきゃいけないことが多いみたいでなれるまでは大変かもしれませんね。
個人的にはNodeをつなぎ合わせるという考え方が、楽器→エフェクター→アンプの接続を想起させるような作りになっていて面白いと思いました:open_mouth:

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

2019年2月のApple Developerのポリシー変更(Apple IDの2ファクタ認証が必須になる件ほか)

2月14日にApple Developerサポートからこんなメールが飛んできました。
見逃している方もいらっしゃるかと思ってシェアいたします。
Apple Developerで使っている Apple IDの2ファクタ認証が必須になるとのことです。

Dear XXXX,

In an effort to keep your account more secure, two-factor authentication will be required to sign in to your Apple Developer account and Certificates, Identifiers & Profiles starting February 27, 2019.
This extra layer of security for your Apple ID helps ensure that you're the only person who can access your account.
If you haven't already enabled two-factor authentication for your Apple ID, please learn more and update your security settings.

If you have any questions, contact us.

Best regards,
Apple Developer Relations

ということで、早めに設定しておかないと、慌てることになりそうです。

また、Apple DeveloperとApp Store Connectのメンバーおよびロールが統合されるとのこと。
https://developer.apple.com/support/teams/

こちらは2月12日から開始されているそうです。

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

【Swift】画面の一部でタップの反応が遅い時の対処

概要

アプリを作っていて画面の一部をホールドすることでアクションを起こそうとしていたがその処理が画面端の時だけ遅れていて対処にハマったのでその対処法をメモ。

実装環境

Xcode 10.1
Swift 4.2.1

コード

override func viewDidAppear(_ animated: Bool) {
        let window = view.window!
        let gr0 = window.gestureRecognizers![0] as UIGestureRecognizer
        let gr1 = window.gestureRecognizers![1] as UIGestureRecognizer
        gr0.delaysTouchesBegan = false
        gr1.delaysTouchesBegan = false
    }

解説

どうやらgestureRecognizersの最初の二つがデフォルトで遅延するようになっているので上記のコードをViewControllerクラス内に書き込めばtouchesBeganの遅延が解消されるようだ。

参考

UISystemGateGestureRecognizer and delayed taps near bottom of screen

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

【iOS】アプリ画面を縦固定に制御する

プロジェクトを選択し下記項目のPortraitを選択する。
コードの記述は不要です。

General > Deployment info > Device Orientation
スクリーンショット 2019-02-15 8.11.16.png

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

iOSのページングの仕組みを紐解いたら有能でした

iOSのUIPageViewControllerは思ったより有能だったという話です。
ただ「どう有能なのか」を理解するのに自分は思ったより時間がかかったので、まとめです。

※UIPageViewControllerはこんな感じのもの。

前提

  • UIScrollViewは今回使ってません。
  • ボタン等でPageViewを遷移させる場合は今回の話対象外です。UIPageViewControllerDataSourceのメソッドが呼ばれないので。

開発環境

  • Xcode 10.1
  • Swift 4.2

通常のUIPageViewControllerの実装方法

4枚のViewControllerをUIPageViewControllerで切り替えできるように実装する場合、
下記のような感じのコードになると思います。サンプルです。

  • Sample.storyboard 上に SamplePageViewController と4つのViewControllerがある
  • 他の画面から SamplePageViewController に遷移することを想定してる
class SamplePageViewController: UIPageViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        dataSource = self
        setViewControllers([getFirst()], direction: .forward, animated: true, completion: nil)
    }

    func getFirst() -> UIViewController {
        let storyBoard = UIStoryboard(name: "Sample", bundle: nil)
        let viewController = storyBoard.instantiateViewController(withIdentifier: "FirstViewController")
        return viewController
    }

    func getSecond() -> UIViewController {
        let storyBoard = UIStoryboard(name: "Sample", bundle: nil)
        let viewController = storyBoard.instantiateViewController(withIdentifier: "SecondViewController")
        return viewController
    }

    func getThird() -> UIViewController {
        let storyBoard = UIStoryboard(name: "Sample", bundle: nil)
        let viewController = storyBoard.instantiateViewController(withIdentifier: "ThirdViewController")
        return viewController
    }

    func getFourth() -> UIViewController {
        let storyBoard = UIStoryboard(name: "Sample", bundle: nil)
        let viewController = storyBoard.instantiateViewController(withIdentifier: "FourthViewController")
        return viewController
    }
}

extension SamplePageViewController: UIPageViewControllerDataSource {
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
        if viewController.isKind(of: FourthViewController.self) {
            // 今Fourthなら、Before画面はThird
            return getThird()
        } else if viewController.isKind(of: ThirdViewController.self) {
            // 今Thirdなら、Before画面はSecond
            return getSecond()
        } else if viewController.isKind(of: SecondViewController.self) {
            // 今Secondなら、Before画面はFirst
            return getFirst()
        } else {
            return nil
        }
    }

    func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
        if viewController.isKind(of: FirstViewController.self) {
            // 今Firstなら、After画面はSecond
            return getSecond()
        } else if viewController.isKind(of: SecondViewController.self) {
            // 今Secondなら、After画面はThird
            return getThird()
        } else if viewController.isKind(of: ThirdViewController.self) {
            // 今Thirdなら、After画面はFourth
            return getFourth()
        } else {
            return nil
        }
    }
}

storyboard使ってるとかView周りの扱い方は人それぞれだと思います、
今回はView周りの設定には言及しません。

UIPageViewControllerDataSource で2つのメソッドを利用しています。
以降、呼びやすいように AfterPageBeforePage と表記します。

AfterPageとBeforePageを使う場合について!この記事では言及します!

AfterPageとBeforePageの問題

AfterPageとBeforePageとは、
上記のコードを見れ貰えれば分かる人は分かると思いますが、
ページを左右にフリックで遷移できるように設定している部分です。

  • AfterPage: →側に遷移する際の画面の設定
  • BeforePage: ←側に遷移する際の画面の設定

このイベントに合わせて、各々のページに追加アクションさせようとすると、事件が発生します。
例えば、

func getFirst() -> UIViewController {
    let storyBoard = UIStoryboard(name: "Sample", bundle: nil)
    let viewController = storyBoard.instantiateViewController(withIdentifier: "FirstViewController")
    navigationItem.title = "Firstやで" // <-追加
    return viewController
}

このように、遷移に合わせてtitleを変更するように書いてみます。(4画面とも
-> なぜか思うようにtitleが変わってくれない。。。なんで。。。

調べていくと、AfterPageとBeforePageが呼ばれるタイミングに問題がありました!
「フリックする度に1回呼ばれる」は間違っていました!

フロー図でAfterPage・BeforePageのロードを追う

First->Second->Third->Fourth->Third->Second->Firstで遷移する例を基に、
フロー図でAfterPage・BeforePageのロードを追ってみます。

  • 補足1: ロード = AfterPage or BeforePageの呼び出し。
  • 補足2: ロードすると、ロードしたViewをPageViewControllerが持つことになります。

フロー図から見えた要点

  • PageViewControllerは最大、表示Viewと前後の合計3つまでViewを持つ
  • 最初は、最初の画面(First)しか持っていない
  • フリックをしようとして、遷移先のページを持っていなければ、AfterPage or BeforePageをロードしてページを取得する
  • フリックでページングの遷移が完了すると、表示Viewの前後のViewを確認し、なければAfterPage or BeforePageをロードして取得する
  • 持っているViewが、表示Viewまたは前後のViewでなければ破棄する

※要点多くなって要点っぽくなくなってしまった(:3」∠)

PageViewControllerが持つViewを3つに自動で設定してくれていて有能!!
と僕は感じました。おかげでViewを保持容量を制御してくれているので。

結論1:遷移のタイミングでアクションを追加したいなら

AfterPage or BeforePageのタイミングに追加するのはオススメしません。
その代わり、UIPageViewControllerDelegateに別の便利メソッドがあります。

extension PageViewController: UIPageViewControllerDelegate {

    // ページ遷移が完了したら、titleを変更する
    func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
        // viewControllers?[0] は現在のViewControllerを示す
        guard completed, let currentViewController = viewControllers?[0] else {
            return
        }
        switch currentViewController {
        case is FirstViewController:
            navigationItem.title = "First"
        case is SecondViewController:
            navigationItem.title = "Second"
        case is ThirdViewController:
            navigationItem.title = "Third"
        case is FourthViewController:
            navigationItem.title = "Fourth"
        default:
            break
        }
    }
}

ページ遷移が完了したタイミングで呼び出されるメソッドを利用すれば、
titleを変更する処理を問題なく実現できました。

結論2:AfterPageとBeforePageの内容を最適化するなら

BeforePageの下記コードは不要です。

if viewController.isKind(of: FourthViewController.self) {
    // 今Fourthなら、Before画面はThird
    return getThird()
}

なぜなら、Fourthの時点で、PageViewControllerがThirdを持っていないことはないからです。
そのため、上記コードは省略できます。

まとめ

省略しないで書くことを勧めます。w
説明をちゃんとしないと省略していい理由が分からないですし。
可読性を考えると、省略しないことを勧めます。

PageViewControllerの有能な点を理解して、活かした実装にしましょう。

補足:たまにPageView完了時にAfterPage呼び出されない

First -> Secondに遷移した際に、Thirdを読み込まないケースが自分の場合ありました。
フリックの勢いのせいかなwくらいしか検討がつかなくて、
原因はあんまり良くわかっていないので、知っている人いたら教えてくださいm(__)m

参考サイト

https://swiswiswift.com/2018/06/21/page-view-controller-with-page-control/
https://qiita.com/Takeshi_Akutsu/items/dbf54df8e8a50e8ed5be
https://qiita.com/yajamon/items/e1754e7fc847b595c26a

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

iOSのページングの仕組みを紐解いてフロー図にしてみた

iOSのUIPageViewControllerは思ったより有能だったという話です。
ただ「どう有能なのか」を理解するのに自分は思ったより時間がかかったので、まとめです。

※UIPageViewControllerはこんな感じのもの。

前提

  • UIScrollViewは今回使ってません。
  • ボタン等でPageViewを遷移させる場合は今回の話対象外です。UIPageViewControllerDataSourceのメソッドが呼ばれないので。

開発環境

  • Xcode 10.1
  • Swift 4.2

通常のUIPageViewControllerの実装方法

4枚のViewControllerをUIPageViewControllerで切り替えできるように実装する場合、
下記のような感じのコードになると思います。サンプルです。

  • Sample.storyboard 上に SamplePageViewController と4つのViewControllerがある
  • 他の画面から SamplePageViewController に遷移することを想定してる
class SamplePageViewController: UIPageViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        dataSource = self
        setViewControllers([getFirst()], direction: .forward, animated: true, completion: nil)
    }

    func getFirst() -> UIViewController {
        let storyBoard = UIStoryboard(name: "Sample", bundle: nil)
        let viewController = storyBoard.instantiateViewController(withIdentifier: "FirstViewController")
        return viewController
    }

    func getSecond() -> UIViewController {
        let storyBoard = UIStoryboard(name: "Sample", bundle: nil)
        let viewController = storyBoard.instantiateViewController(withIdentifier: "SecondViewController")
        return viewController
    }

    func getThird() -> UIViewController {
        let storyBoard = UIStoryboard(name: "Sample", bundle: nil)
        let viewController = storyBoard.instantiateViewController(withIdentifier: "ThirdViewController")
        return viewController
    }

    func getFourth() -> UIViewController {
        let storyBoard = UIStoryboard(name: "Sample", bundle: nil)
        let viewController = storyBoard.instantiateViewController(withIdentifier: "FourthViewController")
        return viewController
    }
}

extension SamplePageViewController: UIPageViewControllerDataSource {
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
        if viewController.isKind(of: FourthViewController.self) {
            // 今Fourthなら、Before画面はThird
            return getThird()
        } else if viewController.isKind(of: ThirdViewController.self) {
            // 今Thirdなら、Before画面はSecond
            return getSecond()
        } else if viewController.isKind(of: SecondViewController.self) {
            // 今Secondなら、Before画面はFirst
            return getFirst()
        } else {
            return nil
        }
    }

    func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
        if viewController.isKind(of: FirstViewController.self) {
            // 今Firstなら、After画面はSecond
            return getSecond()
        } else if viewController.isKind(of: SecondViewController.self) {
            // 今Secondなら、After画面はThird
            return getThird()
        } else if viewController.isKind(of: ThirdViewController.self) {
            // 今Thirdなら、After画面はFourth
            return getFourth()
        } else {
            return nil
        }
    }
}

storyboard使ってるとかView周りの扱い方は人それぞれだと思います、
今回はView周りの設定には言及しません。

UIPageViewControllerDataSource で2つのメソッドを利用しています。
以降、呼びやすいように AfterPageBeforePage と表記します。

AfterPageとBeforePageを使う場合について!この記事では言及します!

AfterPageとBeforePageの問題

AfterPageとBeforePageとは、
上記のコードを見れ貰えれば分かる人は分かると思いますが、
ページを左右にフリックで遷移できるように設定している部分です。

  • AfterPage: →側に遷移する際の画面の設定
  • BeforePage: ←側に遷移する際の画面の設定

このイベントに合わせて、各々のページに追加アクションさせようとすると、事件が発生します。
例えば、

func getFirst() -> UIViewController {
    let storyBoard = UIStoryboard(name: "Sample", bundle: nil)
    let viewController = storyBoard.instantiateViewController(withIdentifier: "FirstViewController")
    navigationItem.title = "Firstやで" // <-追加
    return viewController
}

このように、遷移に合わせてtitleを変更するように書いてみます。(4画面とも
-> なぜか思うようにtitleが変わってくれない。。。なんで。。。

調べていくと、AfterPageとBeforePageが呼ばれるタイミングに問題がありました!
「フリックする度に1回呼ばれる」は間違っていました!

フロー図でAfterPage・BeforePageのロードを追う

First->Second->Third->Fourth->Third->Second->Firstで遷移する例を基に、
フロー図でAfterPage・BeforePageのロードを追ってみます。

  • 補足1: ロード = AfterPage or BeforePageの呼び出し。
  • 補足2: ロードすると、ロードしたViewをPageViewControllerが持つことになります。

フロー図から見えた要点

  • PageViewControllerは最大、表示Viewと前後の合計3つまでViewを持つ
  • 最初は、最初の画面(First)しか持っていない
  • フリックをしようとして、遷移先のページを持っていなければ、AfterPage or BeforePageをロードしてページを取得する
  • フリックでページングの遷移が完了すると、表示Viewの前後のViewを確認し、なければAfterPage or BeforePageをロードして取得する
  • 持っているViewが、表示Viewまたは前後のViewでなければ破棄する

※要点多くなって要点っぽくなくなってしまった(:3」∠)

PageViewControllerが持つViewを3つに自動で設定してくれていて有能!!
と僕は感じました。おかげでViewを保持容量を制御してくれているので。

結論1:遷移のタイミングでアクションを追加したいなら

AfterPage or BeforePageのタイミングに追加するのはオススメしません。
その代わり、UIPageViewControllerDelegateに別の便利メソッドがあります。

extension PageViewController: UIPageViewControllerDelegate {

    // ページ遷移が完了したら、titleを変更する
    func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
        // viewControllers?[0] は現在のViewControllerを示す
        guard completed, let currentViewController = viewControllers?[0] else {
            return
        }
        switch currentViewController {
        case is FirstViewController:
            navigationItem.title = "First"
        case is SecondViewController:
            navigationItem.title = "Second"
        case is ThirdViewController:
            navigationItem.title = "Third"
        case is FourthViewController:
            navigationItem.title = "Fourth"
        default:
            break
        }
    }
}

ページ遷移が完了したタイミングで呼び出されるメソッドを利用すれば、
titleを変更する処理を問題なく実現できました。

結論2:AfterPageとBeforePageの内容を最適化するなら

BeforePageの下記コードは不要です。

if viewController.isKind(of: FourthViewController.self) {
    // 今Fourthなら、Before画面はThird
    return getThird()
}

なぜなら、Fourthの時点で、PageViewControllerがThirdを持っていないことはないからです。
そのため、上記コードは省略できます。

まとめ

省略しないで書くことを勧めます。w
説明をちゃんとしないと省略していい理由が分からないですし。
可読性を考えると、省略しないことを勧めます。

PageViewControllerの有能な点を理解して、活かした実装にしましょう。

補足:たまにPageView完了時にAfterPage呼び出されない

First -> Secondに遷移した際に、Thirdを読み込まないケースが自分の場合ありました。
フリックの勢いのせいかなwくらいしか検討がつかなくて、
原因はあんまり良くわかっていないので、知っている人いたら教えてくださいm(__)m

参考サイト

https://swiswiswift.com/2018/06/21/page-view-controller-with-page-control/
https://qiita.com/Takeshi_Akutsu/items/dbf54df8e8a50e8ed5be
https://qiita.com/yajamon/items/e1754e7fc847b595c26a

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