20190215のSwiftに関する記事は8件です。

XcodeでBuildTargetを切り替える

環境

Mac
システムのバージョン:macOS 10.13.6 (17G5019)
Xcode Version :10.1

C++をMacで勉強してる時に参考にしていた書籍にスクリプトのビルド対象を変更するとコードを消さずに新しいコードを試せると書いてあったのに見つからずに手間取ったのでメモ程度に

スクリーンショット 2019-02-09 22.06.41.png

  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

【Swift】フォントのサイズを指定する時の注意

概要

フォントサイズを指定する時に少しずれる時があったのでその原因をメモ。

原因

Int型の奇数を割った時に小数点以下が切り捨てられていた。

コード

$ swift

  1> let a:Int = 15
a: Int = 15
  2> a/2
$R0: Int = 7
  3> Double(a/2)
$R1: Double = 7
  4> let b = 15/2
b: Int = 7

解説

Int型で15を宣言した後で
2> 2で割る
小数切り捨て
3> 2で割ったものをDouble型でキャスト
Doubleの中でそもそも小数切り捨てされているのでやっていることはDouble(7)
4> 15/2で7.5が入るかと思ったが7

解決

コード

  5> let c:Double = 15
c: Double = 15
  6> b/2
$R0: Double = 7.5
  7> let d = 15.0
d: Double = 15
  8> d/2
$R1: Double = 7.5

解説

解決策としては
1. 代入する時に型を明示的に示してあげる。
2. 明示しないが小数点をつける。

まとめ

大きな文字ではそこまで違いがわからないかもしれないが、小さい文字だと違いがわかるので気をつけましょう。

  • このエントリーをはてなブックマークに追加
  • 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で続きを読む

Swift4.2でFirebase Auth + Facebook Loginを実装する

iOSアプリケーションにおいて、Firebase Auth+Facebook Loginでユーザ認証を実装する。
公式ドキュメントでSwift4の情報がまだまだ少なく嵌ったため、備忘の為の記事です。

また、Githubへソースを上げているので、参考にしてください。
https://github.com/w2-yamaguchi/firebaseAuthTest

なお、Swift3のサンプルは以下に記事を書いています。参考になれば幸いです。
https://qiita.com/w2-yamaguchi/items/6aabc18759e863a87d9f

環境

  • Xcode10.1
  • Swift4.2
  • Firebase
  • Facebook SDK

手順

1. Firebaseのプロジェクト作成、FacebookのMyApps作成

<< 記載予定 >>
一応、以下の公式ドキュメントが参考になります。
https://firebase.google.com/docs/auth/ios/facebook-login?hl=ja
https://developers.facebook.com/docs/facebook-login/ios

2. Xcodeプロジェクトへ各SDKを導入

CocoaPodsでSDKを導入します。

Podfile
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'firebaseAuthTest' do
  # Comment the next line if you're not using Swift and don't want to use dynamic frameworks
  use_frameworks!

  # Pods for firebaseAuthTest
  pod 'Firebase/Core'
  pod 'Firebase/Auth'
  pod 'FacebookCore'
  pod 'FacebookLogin'
  pod 'FacebookShare'

end

導入されたSDKのバージョンは以下を参照してください。

$ pod install
Analyzing dependencies
Downloading dependencies
Installing Bolts (1.9.0)
Installing FBSDKCoreKit (4.40.0)
Installing FBSDKLoginKit (4.40.0)
Installing FBSDKShareKit (4.40.0)
Installing FacebookCore (0.5.0)
Installing FacebookLogin (0.5.0)
Installing FacebookShare (0.5.0)
Installing Firebase (5.16.0)
Installing FirebaseAnalytics (5.5.0)
Installing FirebaseAuth (5.3.0)
Installing FirebaseAuthInterop (1.0.0)
Installing FirebaseCore (5.2.0)
Installing FirebaseInstanceID (3.4.0)
Installing GTMSessionFetcher (1.2.1)
Installing GoogleAppMeasurement (5.5.0)
Installing GoogleUtilities (5.3.7)
Installing nanopb (0.3.901)
Generating Pods project
Integrating client project

3.実装

ソースコード
https://github.com/w2-yamaguchi/firebaseAuthTest

Info.plist

Info.plist
    <key>CFBundleURLTypes</key>
    <array>
        <dict>
            <key>CFBundleURLSchemes</key>
            <array>
                <string>fb*****</string>
            </array>
        </dict>
    </array>
    <key>FacebookAppID</key>
    <string>*****</string>
    <key>FacebookDisplayName</key>
    <string>firebaseAuthTest</string>

AppDelegate.swift

AppDelegate.swift
import Firebase
import FacebookCore

====

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.

        // MARK: - Firebase
        FirebaseApp.configure()

        // MARK: - Facebook
        SDKApplicationDelegate.shared.application(application, didFinishLaunchingWithOptions: launchOptions)

        return true
    }

    func applicationDidBecomeActive(_ application: UIApplication) {
        // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.

        // MARK: - Facebook
        AppEventsLogger.activate(application)
    }

    func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
        // MARK: - Facebook
        return SDKApplicationDelegate.shared.application(app, open: url, options: options)
    }

ViewController.swift

ViewController.swift
import UIKit
import FirebaseAuth
import FacebookLogin

class ViewController: UIViewController, LoginButtonDelegate {

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

        let loginButton = LoginButton(readPermissions: [ .publicProfile, .email ])
        loginButton.center = view.center
        loginButton.delegate = self

        view.addSubview(loginButton)
    }

    func loginButtonDidCompleteLogin(_ loginButton: LoginButton, result: LoginResult) {
        switch result {
        case .failed(let error):
            print(error)
        case .cancelled:
            print("Facebook: User cancelled login.")
        case .success(_, _, let accessToken):
            print("Facebook: User is logged in!")
            let credential = FacebookAuthProvider.credential(withAccessToken: accessToken.authenticationToken)
            firebaseSignIn(credential: credential)
        }
    }

    func loginButtonDidLogOut(_ loginButton: LoginButton) {
        firebaseSignOut()
    }

    func firebaseSignIn(credential: AuthCredential) {
        Auth.auth().signInAndRetrieveData(with: credential) { (authResult, error) in
            if let e = error {
                print(e.localizedDescription)
                return
            }
            print("Firebase: User is signed in!")
        }
    }

    func firebaseSignOut() {
        do {
            try Auth.auth().signOut()
            print("Firebase: User is signed out.")
        } catch let error as NSError {
            print ("Firebase: Error signing out: %@", error)
        }
    }

}

これで完了です!!

  • このエントリーをはてなブックマークに追加
  • 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のページングの仕組みを紐解いたら有能でした

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で続きを読む