- 投稿日:2019-02-15T22:40:31+09:00
XcodeでBuildTargetを切り替える
- 投稿日:2019-02-15T20:50:43+09:00
iOSでAndroidのFragmentのような実装をする (iOSで画面入れ替え)
はじめに
iOSでAndroidのFragmentのような実装をしたいと考えました。
AndroidのFragmentのような…というのは、ここでは1画面の中に固定View(Activity)と複数可変View(Fragment)があり、可変Viewがそれぞれ入れ替わるようなイメージしています。
実現方法
今回は、ボタンを押したら2つのViewが切り替わるサンプルをつくりました。
storyboard
このような構造にしました。薄い青いViewがContainerです。ViewController
Swiftは4.2です。
ViewController.swiftclass 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
このように、ボタンを押したら青と赤のViewが入れ替わります!
全ソース
サンプルコードはこちらに置いてあります!
https://github.com/mii-chang/ChangeViewSample参考になれば嬉しいです◎!
- 投稿日:2019-02-15T19:02:39+09:00
【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. 明示しないが小数点をつける。まとめ
大きな文字ではそこまで違いがわからないかもしれないが、小さい文字だと違いがわかるので気をつけましょう。
- 投稿日:2019-02-15T17:06:06+09:00
[iOS]Swift4で音声の入力・出力をする方法
はじめに
この記事では、マイクから音声を入力して一旦ファイル化し、その音をスピーカーから出力するまでの処理をSwift4で書いてみることにします。
Swiftのアップデートが早くてSwift4もすぐにオワコンになってしまうかもしれないですが、自分用の備忘も兼ねて記録しておきます使ったもの
- 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? AVAudioRecorderDelegatefunc 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 } }こんなクラスファイルがあると実装が楽
この辺のユーティリティと言うほどではないけれど、いろんな画面から呼び出しそうな処理はこんな感じで一箇所にまとめて外出ししてしまった方がコードが冗長にならなくていいなあと思います
あくまで一例ですが…AudioManager.swiftimport 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をつなぎ合わせるという考え方が、楽器→エフェクター→アンプの接続を想起させるような作りになっていて面白いと思いました
- 投稿日:2019-02-15T16:27:40+09:00
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/ios2. 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 project3.実装
ソースコード
https://github.com/w2-yamaguchi/firebaseAuthTestInfo.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.swiftimport 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.swiftimport 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) } } }これで完了です!!
- 投稿日:2019-02-15T11:36:25+09:00
【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
- 投稿日:2019-02-15T00:13:40+09:00
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つのメソッドを利用しています。
以降、呼びやすいようにAfterPage
・BeforePage
と表記します。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
- 投稿日:2019-02-15T00:13:40+09:00
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つのメソッドを利用しています。
以降、呼びやすいようにAfterPage
・BeforePage
と表記します。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