- 投稿日:2020-09-18T22:22:05+09:00
【Swift5】FSCalendarのカレンダー表示モードを変更する
はじめに
カレンダー付きToDoアプリを制作する際にFSCalendarというライブラリを使用しましたので備忘録として投稿します。
初学者ですので訂正点ございましたら、ご指摘よろしくお願いします。概要
FSCalendarではカレンダーの表示モードを
月表示
や週表示
に任意で変更することができます。
制作したアプリの用途に絡めると「ボタンのタップイベントにて表示モードを変更する」になります。また、FSCalendarの導入に関しましてはこちらを参照ください。
実行環境
【Xcode】 Version 11.7
【Swift】 version 5.2.4
【CocoaPods】version 1.9.3
【FSCalendar】version 2.8.1実装後の画面
実装コード
全体のコードになります。
サンプルコードではなく、自作アプリのコードになりますので関連箇所を抜粋しております。
また、FSCalendarのDelegateとDataSourceはstoryboard上で追加しております。MainViewController.swiftimport UIKit import FSCalendar import CalculateCalendarLogic # ・・・省略・・・ class MainViewController: UIViewController { @IBOutlet weak var calendar: FSCalendar! @IBOutlet weak var calendarHeight: NSLayoutConstraint! # ・・・省略・・・ override func viewDidLoad() { super.viewDidLoad() // calendarの曜日部分を日本語表記に変更 calendar.calendarWeekdayView.weekdayLabels[0].text = "日" calendar.calendarWeekdayView.weekdayLabels[1].text = "月" calendar.calendarWeekdayView.weekdayLabels[2].text = "火" calendar.calendarWeekdayView.weekdayLabels[3].text = "水" calendar.calendarWeekdayView.weekdayLabels[4].text = "木" calendar.calendarWeekdayView.weekdayLabels[5].text = "金" calendar.calendarWeekdayView.weekdayLabels[6].text = "土" // calendarの曜日部分の色を変更 calendar.calendarWeekdayView.weekdayLabels[0].textColor = .systemRed calendar.calendarWeekdayView.weekdayLabels[6].textColor = .systemBlue # ・・・省略・・・ } // calendarの表示形式変更 @IBAction func changeButtonAction(_ sender: Any) { if calendar.scope == .month { calendar.setScope(.week, animated: true) changeButton.title = "月表示" // calendarを更新 calendar.reloadData() } else if calendar.scope == .week { calendar.setScope(.month, animated: true) changeButton.title = "週表示" // calendarを更新 calendar.reloadData() } } # ・・・省略・・・ } extension MainViewController: FSCalendarDelegate, FSCalendarDataSource, FSCalendarDelegateAppearance { # ・・・省略・・・ func calendar(_ calendar: FSCalendar, boundingRectWillChange bounds: CGRect, animated: Bool) { calendarHeight.constant = bounds.height self.view.layoutIfNeeded() } # ・・・省略・・・ }実装方法(storyboardにて)
1.storyboardに
FSCalendar
を配置する
- 配置方法の手順はこちらを参照下さい。
2.配置した
FSCalendar
にAutoLayoutの制約を付ける
上
と左右
に0
を設定します。(※1)Height
を設定します。今回は350
にしました。この制約が大切なので忘れずに設定して下さい。(※2)Constraint to margins
にチェックが入っていると、余計なmargin
が入ってしまうので外しておきましょう。(※3)実装方法(コードにて)
1.
FSCalendar
クラスのcalendar
を定義し、storyboardで紐づけする@IBOutlet weak var calendar: FSCalendar!2.
NSLayoutConstraint
クラスのcalendarHeight
を定義する
- コードでAutoLayoutの制約を付ける場合、
NSLayoutConstraint
クラスによって定義することができます。@IBOutlet weak var calendarHeight: NSLayoutConstraint!3.AutoLayoutで制約の設定をした
Height
と2.で定義したcalendarHeight
をstoryboardで紐づけする
- 添付写真は
calendar Height
と紐づけされた後の表記になっております。紐付ける前はheight = 350
となります。4.ボタンのタップイベントでカレンダーの表示モードを変更する
- 以下のコードを用いることで表示モードを変更することができます。
//月ごとの表示にしたい時 calendar.setScope(.month, animated: true) //週ごとの表示にしたい時 calendar.setScope(.week, animated: true)
- 上記をボタンのタップイベントに絡めていきます。説明が抜けておりますが
changeButtonAction
は「実装後の画面」で操作しているボタンになります。// calendarの表示形式変更 @IBAction func changeButtonAction(_ sender: Any) { if calendar.scope == .month { calendar.setScope(.week, animated: true) changeButton.title = "月表示" // calendarを更新 calendar.reloadData() } else if calendar.scope == .week { calendar.setScope(.month, animated: true) changeButton.title = "週表示" // calendarを更新 calendar.reloadData() } }5.Viewの大きさのリサイズ
- FSCalendarは
月表示
が標準で、コードで表示モードを週表示
に切り替えると、レイアウトの自動調整がかかりません。この問題は下記コードを追加することにより解決することができます。func calendar(_ calendar: FSCalendar, boundingRectWillChange bounds: CGRect, animated: Bool) { calendarHeight.constant = bounds.height self.view.layoutIfNeeded() }補足(カレンダー表示について)
- 週の
表示名
や色
を変更したい場合は下記コードを追加することで「実装後の画面」のようになります。// calendarの曜日部分を日本語表記に変更 calendar.calendarWeekdayView.weekdayLabels[0].text = "日" calendar.calendarWeekdayView.weekdayLabels[1].text = "月" calendar.calendarWeekdayView.weekdayLabels[2].text = "火" calendar.calendarWeekdayView.weekdayLabels[3].text = "水" calendar.calendarWeekdayView.weekdayLabels[4].text = "木" calendar.calendarWeekdayView.weekdayLabels[5].text = "金" calendar.calendarWeekdayView.weekdayLabels[6].text = "土" // calendarの曜日部分の色を変更 calendar.calendarWeekdayView.weekdayLabels[0].textColor = .systemRed calendar.calendarWeekdayView.weekdayLabels[6].textColor = .systemBlue
日付
のフォーマットはstoryboardで変更することができます。参考
- 投稿日:2020-09-18T22:04:58+09:00
Swift のOptionalをUnwrapする5つの方法 4個目はお勧め
- 投稿日:2020-09-18T21:42:20+09:00
クロージャと、@escapingと、循環参照。
クロージャ
使い方
変数
や引数
に、関数の処理を直接代入する。使う理由
最近のプログラミング言語では、
引数に関数を入れるとか、戻り値に関数を入れるとかが当たり前になっているので、
そういうときにクロージャを使うと綺麗に記述できるので、みんな嬉しい。
引数
として使うパターンが多い名前の由来
コード
let, varに、関数の処理を直接代入
// MARK: - let, varに、関数の処理を直接代入 // 定数closureが、恰も関数のように扱える let closure = { () -> () in print("Hello World!")} closure() let closure_2 = { () -> Void in print("hamburger")} closure_2() // 戻り値がVoid型、引数がない場合は、(引数) -> 戻り値の型 in を省略可 let closure_3 = { print("Yeah!") } closure_3() // 型を指定 let closure_4: (Int, Int) -> Void = { (num1: Int, num2: Int) -> Void in print(num1 + num2) } // 型推論ってやつ let closure_5 = { (num1: Int, num2: Int) -> Void in print(num1 + num2) } closure_4(100, 199) closure_5(100, 199) // 因みに、closure_4 は num1, num2を省略可。 // 内部引数名を省略 -> 「$」を使用。 // $0 は最初の引数を表し、$1 は2番目の引数を表します。 let closure_6: (Int, Int, Int, Int) -> Void = { print ($0 - $1 + $2 + $3) } closure_6(30, 20, 100, 10) // 120型推論とは?
型の指定をしなくても、代入した値に応じて値の型を推論してくれる機能。
つまり、Swiftは変数の宣言時に型の指定を省略できる。( -> むしろ推奨。)内部引数とは?
関数を呼び出すときは、
外部引数名
を利用します。普段 私たちは、
内部引数名
を外部引数名
として、関数を呼び出しています。
違いはこちら
クロージャを引数として、関数を実行
引数
として使うパターンが多い// MARK: - クロージャを引数として、関数を実行 func closureTest(num1: Int, num2: Int, closure: (Int, Int) -> Int) { print(closure(num1, num2)) } // return文が1行のみの場合には「return」は省略可 closureTest(num1: 300, num2: 5000, closure: { (num1, num2) -> Int in return num1 + num2 }) closureTest(num1: 300, num2: 5000, closure: { (num1, num2) -> Int in num1 + num2 }) // 引数の型を入力しないと、こうなるので注意。 // closureTest(<#T##<<error type>>#>, <#T##<<error type>>#>, <#T##<<error type>>#>) // トレーリング クロージャ (= Trailing Closure) closureTest(num1: 100, num2: 400) { (num1, num2) -> Int in num1 + num2 // return省略 }Trailing Closureとは?
関数の引数のうち 最後の引数がクロージャの場合、
クロージャを( )
の外に書くことができる。func testprint(str1: String, closure: (String) -> Void) { closure("僕の名前は\(str1)です。") } // トレーリングクロージャの場合 -> 美しい testprint(str1: "玄邪 太郎") { string in print(string) } // 通常のクロージャの場合 -> 可読性が悪い... testprint(str1: "玄邪 太郎",{ string in print(string) })
- クロージャが引数だと、
{ }
の外に( )
を包まないといけないので、 可読性が悪くなる。(-> そこで、Trailing Closureが考案)クロージャの、基本的な性質2つ
- ?クロージャーは関数の
引数
や変数
として使える- ?クロージャーは自分が定義されたスコープを
キャプチャ
する引数としてのクロージャ、利点?
- コードが綺麗になる。
- 呼び出し側で、処理が記述できる。
呼び出され側は、クロージャに引数として値を渡し、
「値は渡すから、その値はそちらで好きに料理してね」という具合です。class Foo { let val:Int = 10 func testClosure(closure: (Int) -> Void) { closure(self.val) // self } } class Bar { let foo = Foo() // 引数の値を倍にする func twice() { foo.testClosure{ arg in print(arg * 2) } // arg = argument = 引数 } // 引数の値を半分にする func half() { foo.testClosure{ arg in print(arg / 2) } } } let bar = Bar() bar.twice() // 20 bar.half() // 5クロージャによる、 変数と定数のキャプチャ?
ローカルスコープ(=
scope_1
)で定義された変数や定数は、
ローカルスコープ内でしか使用できませんが、(=scope_2
では使用不可)クロージャが参照している変数や定数は、
クロージャが実行されるスコープ(=scope_4
)が
変数や定数が定義されたローカルスコープ以外(=scope_3以外
)であっても、
クロージャの実行時に使用できます。これは、クロージャが 自身の定義されたスコープ(=
scope_3
) の
変数や定数への参照を保持している為で、この機能をキャプチャ
と言います。class Foo { // scope_2関数は、普通の関数scope_1を2回実行しています。 func scope_1() { var toto = 1 toto += 1 // toto = toto + 1 と同義 (= 変数totoの値を更新) print(toto) } func scope_2() { scope_1() scope_1() } // scope_4関数は、scope_3関数から返されたクロージャを2回実行しています。 func scope_3() -> () -> Void { var tete = 10 let closure = { tete += 1 print(tete) } return closure } func scope_4() { let tutu = self.scope_3() tutu() tutu() } } let test = Foo() test.scope_2() // 2, 2 test.scope_4() // 11, 12// scope_2関数は、普通の関数scope_1を2回実行しています。
普通の関数は、実行後はリセットされるので、
1回目の実行で「2」と表示されても、実行後ローカル変数toto
の値は「1」に戻ります。なので2回目の実行でも「2」と表示されます。
// scope_4関数は、scope_3関数から返されたクロージャを2回実行しています。
クロージャが、
scope_3
関数のローカル変数tete
を参照しているので、
1回目の実行で「11」、2回目の実行では1回目の参照を保持しているので「12」となります。
キャプチャとは?
自身の定義されたスコープの変数や定数への、参照を「保持」する機能。
クロージャ特有の機能。クロージャにて、self を使う理由
明示的な
self
には、循環参照 が存在しないよう確認を促す役割がある。(Escaping Closuresは)循環参照を起こす危険性があるので、
(プログラマにそれを意識させるために)クロージャーの中で親スコープを強参照するときは、selfをつける。
self
を付けることが、循環参照を防ぐことに直結しているわけではなくて、
コードの意図が明確になり、循環参照しないかにプログラマが意識を向けて書くようになる。
@escaping
@escaping
とは?
- Swiftの 属性(= attribute)の1つ
@escaping
属性は、クロージャに対して指定する追加情報。- 関数に引数として渡されたクロージャが、関数のスコープ外で保持される可能性があることを示す。
var array = [()->Void]() func testEscaping(arg: @escaping ()->Void) { //関数のスコープ外の配列に追加 //クロージャが関数外で保持されることになるので、@escaping属性が必要 array.append(arg) } testEscaping {print("玄邪 一郎")} testEscaping {print("玄邪 二郎")} array.forEach { $0() } //Swiftではクロージャの引数に自動的に$0、$1、$2…と順に名前が付与されます。属性(= attribute)とは?
- コンパイラに対し、宣言や型の補足情報を伝えるもの。
@
を使う。
@escaping
を、 使う理由複数のタスクを、非同期で並列処理させるため。
どんな時に、 必要か
- クロージャが、スコープ外で
強参照
されるとき (= プロパティとして保持されるとき)- クロージャを、
非同期
で実行するとき (= メソッド内ですぐに実行されないとき)クロージャをすぐに実行し、どこからも強参照されない場合は、
@escaping
は必要ありません。よくある例
非同期処理をする、完了ハンドラとしてのクロージャ。
// NG: コンパイルエラー func someAsyncMethod(completion: () -> Void) { DispatchQueue.main.async { completion() } } // OK func someAsyncMethod(completion: @escaping () -> Void) { DispatchQueue.main.async { completion() } } // completionHandler -> クエリが完了したときに実行される // DispatchQueue -> 処理待ちタスクを追加するためのキュー「循環参照」 に 気を付けよう?
@escaping
なクロージャは、どこか から強参照される可能性があります。
その参照元をクロージャ内で強参照
すると、循環参照になります...循環参照
循環参照とは?
お互いにインスタンスを参照しあうため、
どちらも解放されずにそのまま残り続けてしまう現象。? (ぐるぐる...)なぜNG?
生成したインスタンスが メモリから解放されないと、
メモリ リーク
となるから。
「解放」 = "ここ使い終わったから どうぞ"?メモリ リーク (= Memory leak)
永久にメモリを消費し続ける現象。
再現性が低く、テストもデバッグも極めて困難な悪質なバグとして有名。class Sample {} // Sample クラスのインスタンスが生成されるが参照カウンタが 0 のままなのですぐに解放される Sample() // Sample クラスのインスタンスが生成されて sample に代入されているので参照カウンタが 1 となり解放されない var sample: Sample? = Sample() // nil を代入すると参照がなくなるのでインスタンスの参照カウンタが 0 になり解放される sample = nil参照カウンタ
TODO: 後述
循環参照 を 防ぐには?
弱参照
をする。
weak または unowned という修飾子を使う。(unknownedではない...!)un owned: 所有されていない
un known: 知られていない, 不明な
un known ed -> こんな言葉ない
weak
とunowned
の違いTODO: 後述
なぜweakが、 循環参照を防げるのか?
TODO: 後述
[ ] とは何か
TODO: 後述
普段なにげなく書いている[unowned self]の意味を調べる
参照カウントについて
これもおしまい
- 投稿日:2020-09-18T21:26:46+09:00
iOS14で「バックグラウンド再生」が出来ない不具合への対処
概要
現在、リリースしているメディア管理系アプリで行ったiOS14に関する不具合調査とその対策です。
経緯とバグ調査
アプリを使って頂いているユーザーさんから「iOS14でバックグラウンド再生が出来ない」
との問い合わせがありました。
早速、手元の実機で調査してみると
動画ファイルを再生中 → アプリをバックグラウンドへ移行 → 再生が一時停止されてしまう
という現象を確認出来ました。
これが、iOS14の仕様(であればアプリ側のバグ)なのか
iOS14側の不具合なのかは今のところ分かりません。
ただ、音声ファイルの再生時にこの現象は起きないので
(バックグラウンドでも再生は継続される)おそらく、iOS14の新しい仕様の可能性が高いのかなと思います。
ちなみに一時停止してしまった動画は、
コントロールセンターからプレイバックボタンを押す事で、再び再生が可能でした。
対策
バックグラウンドで動画再生させるケースはあまりない?と思われがちだが
実際、弊アプリのユーザーさんでは音楽動画などをBG再生して聴かれるケースも多いです。
症状は動画ファイル再生時のみですが、取り急ぎでも対応が必要です。
まずAVAudioSessionで色々やってみる
AVFoundation FrameworkのAVAudioSessionのカテゴリーを調整してみます。
AVAudioSession.sharedInstance().setCategory(.playback, mode: .moviePlayback, options: [])ここのmodeとoptionsを調整してみます。
extension AVAudioSession.Mode { public static let `default`: AVAudioSession.Mode public static let voiceChat: AVAudioSession.Mode public static let gameChat: AVAudioSession.Mode public static let videoRecording: AVAudioSession.Mode public static let measurement: AVAudioSession.Mode public static let moviePlayback: AVAudioSession.Mode public static let videoChat: AVAudioSession.Mode public static let spokenAudio: AVAudioSession.Mode public static let voicePrompt: AVAudioSession.Mode }public struct CategoryOptions : OptionSet { public init(rawValue: UInt) public static var mixWithOthers: AVAudioSession.CategoryOptions { get } public static var duckOthers: AVAudioSession.CategoryOptions { get } public static var allowBluetooth: AVAudioSession.CategoryOptions { get } public static var defaultToSpeaker: AVAudioSession.CategoryOptions { get } public static var interruptSpokenAudioAndMixWithOthers: AVAudioSession.CategoryOptions { get } public static var allowBluetoothA2DP: AVAudioSession.CategoryOptions { get } public static var allowAirPlay: AVAudioSession.CategoryOptions { get } }色々と組み合わせてみるが、効果無し。
仕方がないので、少し強引な対策で対処
根本的な対策方法については、引き続き調べていくとして
とりあえずの落としどころが欲しかったので、
まずAVPlayerItemのstatusを監視
システムから再生が停止されてしまった際 → playbackLikelyToKeepUpの変更を検知
isPlaybackLikelyToKeepUpそのタイミングでバックグラウンド再生中であれば、再び動画を再生させる。
と言う方法で対処することにしました。
override open func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?){ if let item = object as? AVPlayerItem, let keyPath = keyPath { if item == self.playerItem { switch keyPath { // ...略 case #keyPath(AVPlayerItem.playbackLikelyToKeepUp): debugPrint("PlaybackLikelyToKeepUp is changed") // ** BG状態かつ、再生ステータスなのにAVPlayerが停止という状態をチェック if isInBackground, isInconsistentPlaybackState { play() debugPrint("Background Playback Forced Resuming >> Did Execute") } default: break } } } }調査は継続しつつ、これで様子を見ていきたいと思います。
- 投稿日:2020-09-18T19:28:54+09:00
iOS 14 UIDatePickerの以前と同じサイズ指定
iOS14 以降でUITextField の入力補助として UIDatePicker を使っている場合に
以前と同じサイズでピッカーを利用する例lazy var datePicker: UIDatePicker = { let datePicker: UIDatePicker = UIDatePicker() datePicker.datePickerMode = UIDatePicker.Mode.date datePicker.timeZone = NSTimeZone.local datePicker.locale = Locale.getPreferred() datePicker.date = Calendar.current.date(byAdding: .hour, value: 1, to: Date())! datePicker.minimumDate = Date() if #available(iOS 13.4, *) { datePicker.frame = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: 250.0) datePicker.preferredDatePickerStyle = .wheels } return datePicker }() self.textField.inputView = self.datePicker
- 投稿日:2020-09-18T19:21:28+09:00
NavigationControllerを使わずに横に遷移させる
コード
func tappedNextButton() { let nextvc = NextViewController() nextvc.modalPresentationStyle = .fullScreen let transition = CATransition() transition.duration = 0.23 transition.type = CATransitionType.push transition.subtype = CATransitionSubtype.fromRight view.window!.layer.add(transition, forKey: kCATransition) self.present(nextvc, animated: false, completion: nil) }presentをanimated: false にするのを忘れないようにしてください。
Storyboardを使っている場合でも動きました。
- 投稿日:2020-09-18T16:21:10+09:00
iPadOS / macOSに対応しているか確認する
モチベーション
Apple SiliconによるiOSがiPadOS / macOSに統合の流れがあり、SwiftUIで広い画面を想定した実装を心がけた方が何かと役に立ちそう
iPad and iPhone apps on Apple Silicon Macsチェック項目
- iPadOS向けビルドするには何をすればいいのか?
- フルSwiftUIに移行するにはどうすればいいのか?mainに何を書けばいいのか?
- OS(Sizeに応じて)出し分けたい
- readable width などのOS標準が無いか調べる
- iPhoneで回転せず、iPadでのみ回転させたい
iPadOS向けビルドするには何をすればいいのか?
ビルド対象の追加
[Target] > [General] > [Deployment Info] の iPad にチェックがついていれば良い。
NavigationViewでフルスクリーン表示する
NavigationView
をデフォルトのまま利用すると、iPad上ではカラム表示されてしまいます。
NavigationView
を使うがiPadでも全画面で利用したい場合に下記の設定を入れる。NavigationView { ... some view } .navigationViewStyle(StackNavigationViewStyle())フルSwiftUIに移行するにはどうすればいいのか?mainに何を書けばいいのか?
App名と同じ.swiftファイルを作成する
メインとなるアプリ用のstructを用意し、bodyの中でWindowGroupを宣言する。
HogeApp.swift@main struct HogeApp: App { @StateObject private var model = HogeModel() @StateObject private var store = Store() var body: some Scene { WindowGroup { HogehogeContentView() .environmentObject(model) .environmentObject(store) } } }ScreenDelegateとAppDelegateを消す
ScreenDelegateとAppDelegateを消すだけで良いと言われたが、下記エラーが発生
Could not find a storyboard named 'Main' in bundle NSBundle
Info.plist内の
Application Session Role
以下の内容をまるっと消すと動いたOS(Sizeに応じて)出し分けたい
readable width などは無さそう、自前でmaxWidthを設定する必要がありそう
iPadの設定アプリなどのTableViewを見ると項目が画面幅最大まで広がらず、適度な大きさにとどまる様になっています。
この設定は readableContentGuide として主にTableViewなどで利用されています。SwiftUIに上記の様な仕組みがあるか、確認したが見当たらなかった。
いずれは追加されるかもしれないが、一時的な解決策としてViewに対してmaxWidth
を設定するようにしました。幅の参考値としてiOSでの読みやすい幅を読んだところ、サイズに関わらず
672
が設定されるケースが多かったためその値を採用しました。iPhoneで回転せず、iPadでのみ回転させたい
こちらは想定通りでは有りませんがWorkaround的に動いているので記載します。正しい解決方法など有りましたコメントいただけますとありがたいです。
やはり必要だったAppDelegateファイルを再度用意し、回転動作を縦画面(.portrait)に固定します。
AppDelegate.swiftclass AppDelegate: NSObject, UIApplicationDelegate { private static var orientationLock = UIInterfaceOrientationMask.portrait func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask { AppDelegate.orientationLock } }次に、AppファイルにAppDelegateを結びつけます
HogeApp.swift@main struct HogeApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate /* ... */ }現状(Xcode 12)はこの設定がiOSでしか有効にならず、iPadOSでは無視され回転してしまいます。
目的は達成されているので、一旦このままにします。最後に
目に見える範囲だけ対応しましたので、他に懸念などお気づきの点があれば指摘、コメントお願い致します。
- 投稿日:2020-09-18T09:50:48+09:00
ScrollView のスクロール中だけ Lottie アニメーションを止めたい! (Swift)
はじめに
Lottie アニメーションを
UIScrollView
がスクロールしている最中のみ動かしたいときがあったので、その実装を紹介します。
具体的に言うと、
- スクロール中: アニメーション停止
- スクロール終了: アニメーション再開
という要求です。
準備: Lottie を動かす ViewController
ScrollView の動作関係なしに、ただ Lottie を動かす ViewController のコードを紹介します。
今回はこのコードを元に実装してみます。コメント多めに書いてますので、ある程度は追えるかと思います。
今回はなんとなくランダムな位置に複数の Lottie アニメーションを配置してみました。import UIKit import Lottie class ViewController: UIViewController { // MARK: - IBOutlet // scrollView の子要素で、 Lottie オブジェクトが配置される UIView @IBOutlet weak var contentView: UIView! // MARK: - Private properties var animations = [AnimationView]() // MARK: - Lifecycle override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. // ランダムな位置に 3 個の Lottie オブジェクトを生成・配置・再生する // 複数配置していることに特に意味はありません。 for _ in 1...3 { let x = CGFloat.random(in: 1.0...5.0) * 200.0 let y = CGFloat.random(in: 1.0...6.0) * 100.0 let animation = generateAnimation(x: x, y: y) animations.append(animation) contentView.addSubview(animation) animation.play() } } // MARK: - Private methods // 引数で受け取った座標を持つ Lottie オブジェクトを生成するメソッド // see: https://qiita.com/ngo275/items/c9e94bad7a7afc85e4f4 private func generateAnimation(x: CGFloat, y: CGFloat) -> AnimationView { let animationView = AnimationView(name: "sample_animation") animationView.frame = CGRect(x: x, y: y, width: view.bounds.width * 0.2, height: view.bounds.height * 0.2) animationView.loopMode = .loop animationView.contentMode = .scaleAspectFit animationView.animationSpeed = 1 return animationView } }スクロール中だけアニメーションを止めてみよう
このままでは
viewDidLoad
ライフサイクルでplay()
されっぱなしで、止めようがありません。
今回の要求として、「スクロール中はアニメーションを止める」というのがありますので、まずはUIScrollView
を Outlet 接続しましょう。// MARK: - IBOutlet // scrollView の子要素で、 Lottie オブジェクトが配置される UIView @IBOutlet weak var contentView: UIView! @IBOutlet weak var backgroundScrollView: UIScrollView! // 追加次に、追加した
backgroundScrollView
の delegate 先をself
つまりViewController
自身に設定します。// MARK: - Lifecycle override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. backgroundScrollView.delegate = self // 追加これで ScrollView の delegate メソッドが使えるようになりました。
UIScrollView
の delegate メソッドは沢山ありますが、今回は
- スクロールのし初め
- スクロールの終わり
を検知することで
- スクロールのし初め: アニメーションを一時停止
- スクロールの終わり: アニメーションを再開
という風にして実装していきます。
沢山ある delegate メソッドの解説は 公式ドキュメント や 他の方がまとめてくださった記事 に譲って割愛します。
今回使うのは、以下の 2 つです。// 1. // 指が画面に触れ、スクロールが開始した瞬間に呼ばれるメソッド func scrollViewWillBeginDragging(_ scrollView: UIScrollView) // 2. // 指が画面から離れ、慣性のスクロールが完全に止まった瞬間に呼ばれるメソッド func scrollViewDidEndDecelerating(_ scrollView: UIScrollView)これらのメソッドを使えば、以下のようにして、簡単にスクロール中にアニメーションを止めて、スクロールが終わったらアニメーションを再開するということが可能になります。
ちなみに、stop()
メソッドを使用していますが、pause()
メソッドでも大丈夫です。
アニメーションが最後まで行って止まるか、pause()
された瞬間に止まるかの違いです。// 指が画面に触れ、スクロールが開始した瞬間に呼ばれるメソッド func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { print(#function) // どの関数が呼ばれているか確認用に表示 // スクロール開始と同時にアニメーションをストップ animations.forEach { $0.stop() } } // 指が画面から離れ、慣性のスクロールが完全に止まった瞬間に呼ばれるメソッド func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { print(#function) // どの関数が呼ばれているか確認用に表示 // スクロール終了と同時にアニメーションをスタート animations.forEach { $0.play() } }あとは、これを実装して終わりです。
ViewController にUIScrollViewDelegate
を継承させます。
僕は extension して書いていますが、class ViewController: UIViewController, UIScrollViewDelegate
としても何も問題ありません。// MARK: - UIScrollViewDelegate extension ViewController: UIScrollViewDelegate { // 指が画面に触れ、スクロールが開始した瞬間に呼ばれるメソッド func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { print(#function) // どの関数が呼ばれているか確認用に表示 // スクロール開始と同時にアニメーションをストップ animations.forEach { $0.stop() } } // 指が画面から離れ、慣性のスクロールが完全に止まった瞬間に呼ばれるメソッド func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { print(#function) // どの関数が呼ばれているか確認用に表示 // スクロール終了と同時にアニメーションをスタート animations.forEach { $0.play() } } }これでビルドしてみると、スクロールしている間だけアニメーションが止まることを確認できるはずです。
最後に、完成した ViewController を載せます。import UIKit import Lottie class ViewController: UIViewController { // MARK: - IBOutlet // scrollView の子要素で、 Lottie オブジェクトが配置される UIView @IBOutlet weak var contentView: UIView! @IBOutlet weak var backgroundScrollView: UIScrollView! // MARK: - Private properties var animations = [AnimationView]() // MARK: - Lifecycle override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. backgroundScrollView.delegate = self // ランダムな位置に 3 個の Lottie オブジェクトを生成・配置・再生する // 複数配置していることに特に意味はありません。 for _ in 1...3 { let x = CGFloat.random(in: 1.0...5.0) * 200.0 let y = CGFloat.random(in: 1.0...6.0) * 100.0 let animation = generateAnimation(x: x, y: y) animations.append(animation) contentView.addSubview(animation) animation.play() } } // MARK: - Private methods // 引数で受け取った座標を持つ Lottie オブジェクトを生成するメソッド // see: https://qiita.com/ngo275/items/c9e94bad7a7afc85e4f4 private func generateAnimation(x: CGFloat, y: CGFloat) -> AnimationView { let animationView = AnimationView(name: "sample_animation") animationView.frame = CGRect(x: x, y: y, width: view.bounds.width * 0.2, height: view.bounds.height * 0.2) animationView.loopMode = .loop animationView.contentMode = .scaleAspectFit animationView.animationSpeed = 1 return animationView } } // MARK: - UIScrollViewDelegate extension ViewController: UIScrollViewDelegate { // 指が画面に触れ、スクロールが開始した瞬間に呼ばれるメソッド func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { print(#function) // どの関数が呼ばれているか確認用に表示 // スクロール開始と同時にアニメーションをストップ animations.forEach { $0.stop() } } // 指が画面から離れ、慣性のスクロールが完全に止まった瞬間に呼ばれるメソッド func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { print(#function) // どの関数が呼ばれているか確認用に表示 // スクロール終了と同時にアニメーションをスタート animations.forEach { $0.play() } } }参考文献