Xcode 11.2 + Storyboard UITextView でクラッシュ
Xcode 11.2 で Storyboard か xib で UITextView
を使っていると iOS 13.2 より前のバージョンでクラッシュするという凶悪なバグが発生している。
実際にクラッシュを起こしてみると以下の様に _UITextLayoutView
ってクラスは無いとエラーメッセージが表示された。
*** Terminating app due to uncaught exception 'NSInvalidUnarchiveOperationException', reason: 'Could not instantiate class named _UITextLayoutView because no class named _UITextLayoutView was found; the class needs to be defined in source code or linked in from a library (ensure the class is part of the correct target)'
Stack Overflow
Apple Developer Forums
これらの書き込みをまとめると
- 影響範囲は現行最新版の iOS 13.2 より前の全てのバージョン
- 発生条件は Storyboard か xib で
UITextView
を使っている事 - 回避策1
UITextView
を全てコードで生成する - 回避策2 Xcode 11.1 を使う
回避策については Apple の人も同様に言っている https://forums.developer.apple.com/thread/125287#391939
対策
UITextView
の Storyboard 生成をコード生成に置き換えるかどうかはプロジェクト次第(コスト次第)だと思う。
もしソースの公開されていない外部ライブラリを使っている場合、中でどういう実装をしているのか分からないので Xcode 11.1 に戻した方が無難そう。
何らかの事情で Xcode 11.2 を使い続ける場合、配布ビルドのみ Xcode 11.1 で行うのもアリかと。
余談
Developer Forums に _UITextLayoutView
を動的に追加すれば良いなんてコメントもあった。
https://forums.developer.apple.com/thread/125287#391979
なるほどと思い実際に試してみたところ以下の Objective-C 実装を application(_:didFinishLaunchingWithOptions:)
のタイミングで実行する事で iOS 12 でもクラッシュを回避する事ができた。
がしかしコメントにもある様に、実際にこの実装でリリースするのは如何なものかいう感じなので次のバージョンの Xcode が出るまでの開発中の一時凌ぎとして使うならといったところ。興味が湧いて実装しただけなので参考までに。*1
@interface UITextViewWorkaround : NSObject + (void)executeWorkaround; @end @implementation UITextViewWorkaround + (void)executeWorkaround { if (@available(iOS 13.2, *)) { } else { const char *className = "_UITextLayoutView"; Class cls = objc_getClass(className); if (cls == nil) { cls = objc_allocateClassPair([UIView class], className, 0); objc_registerClassPair(cls); #if DEBUG printf("added %s dynamically\n", className); #endif } } } @end
_UITextLayoutView
の superClass
が UIView
である事は以下の記事の方法で確認した。
以上
追記
iOS と tvOS については Xcode 11.2.1 GM で修正された。
https://developer.apple.com/documentation/xcode_release_notes/xcode_11_2_1_gm_seed_release_notes/
*1:この実装を掲載した後まるっと StackOverflow に転載されていてびっくり
複数の AVSpeechSynthesizer の同時再生 iOS 13
最近 iOS 13 でバグも含め挙動が変わった点で色々と苦労している。そんな中良い改善点もあった。タイトルの複数の AVSpeechSynthesizer の同時再生がその1つ。
これまでは AVSpeechSynthesizer
の同時再生はできなかった。できないと一口に言っても妙な挙動をするのでこの機会にまとめて整理しておく。
検証のためのコード
以下の様なソースコードでポチポチボタンをタップして色々なパターンで動作を確認してみた。
import UIKit import AVFoundation class ViewController: UIViewController { let synthesizerA = AVSpeechSynthesizer() let synthesizerB = AVSpeechSynthesizer() let synthesizerC = AVSpeechSynthesizer() override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. } @IBAction func speak1() { let utterance = AVSpeechUtterance(string: "1, 1, 1") utterance.rate = 0.2 synthesizerA.speak(utterance) } @IBAction func speak2() { let utterance = AVSpeechUtterance(string: "2, 2, 2") utterance.rate = 0.2 synthesizerA.speak(utterance) } @IBAction func speak3() { let utterance = AVSpeechUtterance(string: "3, 3, 3") utterance.rate = 0.2 synthesizerB.speak(utterance) } @IBAction func speak4() { let utterance = AVSpeechUtterance(string: "4, 4, 4") utterance.rate = 0.2 synthesizerB.speak(utterance) } @IBAction func speak5() { let utterance = AVSpeechUtterance(string: "5, 5, 5") utterance.rate = 0.2 synthesizerC.speak(utterance) } @IBAction func speak6() { let utterance = AVSpeechUtterance(string: "6, 6, 6") utterance.rate = 0.2 synthesizerC.speak(utterance) } @IBAction func speakAll() { speak1() speak2() speak3() speak4() speak5() speak6() } }
検証結果
記号の意味は以下とする
|
は再生開始点
-
は生成中
>
は再生終了点
iOS 7
未確認
iOS 8~12
Bの開始時にAが再生中であればBは開始されない。
A |-----> B |
Aの再生中にBを2度開始すると、2回目のBは順番待ちにスタックされ再生中のAの終わりにBの2つめが再生される。また2つ目のBのスタックはCによって打ち消されない。
A |----------> B | B | ----->
上記のケースでAの再生中にCを2度開始してもスタックされたBの2つ目が再生される。
A |----------> B | B | -----> C | C |
Aの再生中にBを2度開始してさらにAを開始するとAの終わりにはAの2つ目が再生される。
再生中のAへのスタックは他のスタックを打ち消す。
A |----------> B | B | A | ----->
iOS 13
同時再生可能!
AとBは完全にパラレルでインスタンス毎に動作する。
音声再生中の次の音声再生開始はインスタンス毎にスタックされ他には影響しない。
A |-------> B |-----------> A | --------> B | --------->
別の視点での利点
以下の様に実装した再生の一時停止と再生再開のコードを使って検証
@IBAction func pauseOrResumeA() { if synthesizerA.isSpeaking { if synthesizerA.isPaused { synthesizerA.continueSpeaking() } else { synthesizerA.pauseSpeaking(at: .immediate) } } }
先ほどの図式に /
を一時停止と再生再開として追加する。
iOS 12
これまでは同時再生ができなかったので、Aを再生後、一時停止をしてBを2回再生開始した場合でも、Aの再生再開と再生完了までBは永久に再生が行われないという問題(仕様)があった。
Aが一時停止中(isSpeaking && isPaused)だとBは永久に再生されない
A |---------/ B | B |
Aが再生再開(continueSpeaking)して再生終了(!isSpeaking)するとようやくBの再生が始まる。一時停止と再生再開の挙動を除くと最初のケースで説明した状況と同じ。
A |---------/ /---> B | B | ----->
実際の実装ケースでは画面別に AVSpeechSynthesizer
を使っている場合、一時停止のまま別の画面に移動してしまうと他の画面で一切 AVSpeechSynthesizer
動作しなくなるという問題がある。このため画面から移動する際には強制的に AVSpeechSynthesizer
を停止する必要がある。
さらにまた元の画面に戻った際に続きから再生を再開したいのであれば一時停止した位置を保持しておき、そこから新たに再生を開始するという工夫も必要。
iOS 13
同時再生が可能なので、Aが一時停止中だろうがなかろうがBは関係なく再生できる。
A |---------/ /---> B |------------>
実装への影響としては複数の AVSpeechSynthesizer
のインスタンスの同時再生が可能になったので、画面毎(もしくはView単位、Class単位)で使っている AVSpeechSynthesizer
が再生中か、一時停止状態か、停止状態かを気にする必要がなくなった。
それぞれ別個に一時停止とその地点からの再生再開が可能。
他のインスタンスとの協調動作を気にかける必要が無く実装の切り出しや細分化もしやすくなる。
iOS 13 で AVSpeechSynthesisVoice.currentLanguageCode() の挙動が変更に(多分バグ)
Xcode 11, iOS 12.2, iOS 13.1, iOS 13.1.3 で動作確認
デフォルトの読み上げ音声の言語の確認
AVSpeechSynthesisVoice.currentLanguageCode()
参考に読み上げ確認の実装
(AVSpeechSynthesisVoice
を設定せずデフォルト状態での読み上げ)
let synthesize = AVSpeechSynthesizer() let utterance = AVSpeechUtterance(string: "1 2 3") // utterance.voice = AVSpeechSynthesisVoice() synthesize.speak(utterance)
iOS 12
デバイスの言語設定の優先順序に基づいて LanguageCode が返される。
英語にしか対応していない未ローカライズのアプリでもデバイスの言語設定が 日本語(日本)
になっていれば AVSpeechSynthesisVoice.currentLanguageCode()
は ja-JP
を返しデフォルトで日本語で読み上げが行われる。
iOS 13
デバイスの言語設定ではなくアプリの表示言語が反映される様に挙動が変わっている。
例えば英語にしか対応していない未ローカライズではデバイスの言語設定が 日本語(日本)
になっていても AVSpeechSynthesisVoice.currentLanguageCode()
は en-US
を返す様になってしまっており、デバイスの設定言語で読み上げされないという現象が発生してしまっている。
日本語設定のデバイスが手元にあれば以下で簡単に挙動を確認できる
- Xcode のテンプレートからアプリを作成して(ローカライズなど何もせず)
AVSpeechSynthesisVoice.currentLanguageCode()
を出力する。 - iOS 12 では
ja-JP
、iOS 13 ではen-US
になる。 - iOS 12 では
AVSpeechSynthesizer
はデフォルトでデバイス設定の日本語で読み上げする。 - iOS 13 ではデバイス設定が日本語でもアプリが日本語ローカライズされていないと
AVSpeechSynthesizer
はデフォルトで英語で読み上げする。
iOS 12 | iOS 13 | |
---|---|---|
Settings>General>Language & Region>iPhone Language | 日本語(日本) | 日本語(日本) |
Locale.preferredLanguages.first | ja-JP | ja-JP |
AVSpeechSynthesisVoice.currentLanguageCode() | ja-JP | en-US |
仕様変更かバグか
公式ドキュメント
currentLanguageCode() - AVSpeechSynthesisVoice | Apple Developer Documentation
Return Value An NSString object containing the BCP 47 language and locale code for the user’s current locale. Discussion This code reflects the user’s language and region preferences as selected in the Settings app.
設定アプリで選択した言語と地域が反映されると書いてある。
よって iOS 13 での挙動は意図しないものなので多分バグ。*1*2
Workaround
iOS 13 では iOS 12 の処理を真似た独自実装で currentLanguageCode
を生成して返す様に試みる。
Workaround for that AVSpeechSynthesisVoice current ...
実装内容1
AVSpeechSynthesisVoice
の言語コードについては Objective-C ヘッダに以下のコメントの記述がある
Use a BCP-47 language tag to specify the desired language and region.
システムの優先言語設定を取得できる Locale.preferredLanguages
の値は zh-Hant-HK
の形式でこの場合
zh
がlanguageCode
Hant
がscriptCode
HK
がregionCode
となる。
AVSpeechSynthesisVoice.speechVoices()
で取得できる利用可能な音声の language
の形式は zh-HK
となっており scriptCode
は存在しない。
このためわざわざ preferredLanguages
から一度 Locale
を作成して languageCode
と regionCode
を取り出して再構築するという手順を踏んでいる。
実装内容2
オリジナルの currentLanguageCode
の挙動を確認したところ、speechVoices
のリストに存在しない言語と地域がデバイスに設定されている時は en-US
を返す様になっていたのでそこを踏襲しつつ、現在のデバイス設定の言語コードと地域コードから AVSpeechSynthesisVoice
用の言語コードを作成する様にしている。
例えば uk
ウクライナ語、vi
ベトナム語などOSで選択可能な言語で且つ読み上げ不可能な言語(speechVoices
のリストに存在していない言語)を選択した場合、currentLanguageCode
は en-US
を返す。
また ja-US
など地域が不一致のパターンにおいても en-US
を返す。*3
macOS
追試で macOS でも調べてみたところ、Catalina では
languageCode
はアプリの言語regionCode
はシステム環境設定の地域設定
となる様で日本語環境では AVSpeechSynthesisVoice.currentLanguageCode
が en-JP
を返した。
iOS の様に利用可能な音声リストが無い場合は en-US
を返す様な処理が入っていないっぽい。
そして en-JP
で読み上げ可能な言語が無いのでデフォルト設定のままでは読み上げされないという現象が起きていた。
よって en-US
なり ja-JP
なりの利用可能な AVSpeechSynthesisVoice
を自分で設定してやる必要がある。*4
ちなみに Mojave ではそもそも AVSpeechSynthesizer
AVSpeechSynthesisVoice
AVSpeechUtterance
あたりは import できてコンパイルもできるけど一切機能しないという問題があるらしいので挙動の検証以前の問題だった。
Mojave では旧来の NSSpeechSynthesizer
を使うのが正解っぽい。
更新
Workaround を修正
Workaround for that AVSpeechSynthesisVoice current ...
Locale
の identifier
には regionCode
が含まれていない場合があるので対応した。
具体的には en-US
ja-JP
以外に en
ja
のケースがある。
また、前述のデバイス言語から読み上げ音声への変換の例外(zh-Hans
から zh-CN
といったケース)に対応するために、一旦 AVSpeechSynthesisVoice
を作成して取得される language
を戻り値として使用する様に変更した。
iOS 13 で EventKitUI に追加された UIFont のカテゴリメソッド(多分Private用)
タイトルの通り iOS 13 で EventKitUI に UIFont
のカテゴリメソッドが追加された。
@property(nonatomic, readonly, class) UIFont *ek_defaultOccurrenceSecondaryTextFont; + (UIFont *)ek_defaultOccurrencePrimaryTextFontForSizeClass:(UIUserInterfaceSizeClass)sizeClass; + (UIFont *)ek_defaultOccurrenceSmallPrimaryTextFontForSizeClass:(UIUserInterfaceSizeClass)sizeClass;
EventKit の prefix ek
がついている。
Objective-C の場合、名前の衝突を防ぐためにカテゴリメソッドには prefix とアンダーバーを付けるのが通例。
この時点で既に Public API っぽくない匂いがしてくる。
公式ドキュメントは存在している
- https://developer.apple.com/documentation/uikit/uifont/3255206-ek_defaultoccurrencesecondarytex
- https://developer.apple.com/documentation/uikit/uifont/3255205-ek_defaultoccurrenceprimarytextf
- https://developer.apple.com/documentation/uikit/uifont/3255207-ek_defaultoccurrencesmallprimary
使ってみる
このカテゴリメソッド、Swift から直接は読み込めずに利用する事ができない。
その理由は上記のメソッドが EventKitUI/UIFont+EKDayOccurrenceView.h
に定義されているにも関わらず framework のヘッダや modulemap でこのヘッダを読み込む様に指定されていないため。
/* * EventKitUI.h * EventKitUI * * Copyright 2010 Apple Inc. All rights reserved. * */ #import <EventKit/EventKit.h> #import <EventKitUI/EventKitUIDefines.h> #import <EventKitUI/EKEventEditViewController.h> #import <EventKitUI/EKEventViewController.h> #import <EventKitUI/EKCalendarChooser.h>
Objective-C から利用するには以下の様に直接ファイルヘッダを指定して読み込む。
Swift から利用したい場合は <Module>-Bridging-Header.h
で以下を指定する。
#import <EventKitUI/UIFont+EKDayOccurrenceView.h>
使った結果
ログ出力の実装
NSLog(@"%@", UIFont.ek_defaultOccurrenceSecondaryTextFont); NSLog(@"%@", [UIFont ek_defaultOccurrencePrimaryTextFontForSizeClass:UIUserInterfaceSizeClassCompact]); NSLog(@"%@", [UIFont ek_defaultOccurrencePrimaryTextFontForSizeClass:UIUserInterfaceSizeClassRegular]); NSLog(@"%@", [UIFont ek_defaultOccurrencePrimaryTextFontForSizeClass:UIUserInterfaceSizeClassUnspecified]); NSLog(@"%@", [UIFont ek_defaultOccurrenceSmallPrimaryTextFontForSizeClass:UIUserInterfaceSizeClassCompact]); NSLog(@"%@", [UIFont ek_defaultOccurrenceSmallPrimaryTextFontForSizeClass:UIUserInterfaceSizeClassRegular]); NSLog(@"%@", [UIFont ek_defaultOccurrenceSmallPrimaryTextFontForSizeClass:UIUserInterfaceSizeClassUnspecified]);
ログの結果
<UICTFont: 0x7f9d01403320> font-family: "UICTFontTextStyleCaption2"; font-weight: normal; font-style: normal; font-size: 11.00pt <UICTFont: 0x7f9d01404960> font-family: ".SFUI-Semibold"; font-weight: bold; font-style: normal; font-size: 13.00pt <UICTFont: 0x7f9d01404960> font-family: ".SFUI-Semibold"; font-weight: bold; font-style: normal; font-size: 13.00pt <UICTFont: 0x7f9d01404960> font-family: ".SFUI-Semibold"; font-weight: bold; font-style: normal; font-size: 13.00pt <UICTFont: 0x7f9d01207540> font-family: ".SFUI-Semibold"; font-weight: bold; font-style: normal; font-size: 11.00pt <UICTFont: 0x7f9d01404960> font-family: ".SFUI-Semibold"; font-weight: bold; font-style: normal; font-size: 13.00pt <UICTFont: 0x7f9d01207540> font-family: ".SFUI-Semibold"; font-weight: bold; font-style: normal; font-size: 11.00pt
実際の見た目
メソッド名から察するに EventKitUI でのイベントの繰り返し設定周りで使われているフォントだと思われるけど、実際にUIのどこに使われているのかは分からなかった。
Private で使っているAPIのヘッダが Public ヘッダとして出されちゃってる感があるので小ネタというか参考程度に。Apple Feedback Assistant にレポートすれば多分将来いなくなると思う。*1 *2
ちなみに UIFont+EKDayOccurrenceView.h
の5行目のファイルテンプレート部分に Created by でファイル作成者のアカウント名と日付が載っている。
通常の Apple 謹製 framework の Public Header にこの項目は載っていない事からも間違って Public に出てきている事が推測される。
This API seems to be private use. You can directly ...
追記
iOS 13.4 Beta 1 で deprecated に修正されました。
// // UIFont+EKDayOccurrenceView.h // EventKit // // Copyright 2019 Apple Inc. All rights reserved. // #import <UIKit/UIKit.h> NS_ASSUME_NONNULL_BEGIN @interface UIFont (EKDayOccurrenceView) @property (nonatomic, readonly, class) UIFont *ek_defaultOccurrenceSecondaryTextFont API_DEPRECATED("No replacement. This is not intended for third party use and will return nil.", ios(13.0, 13.4)) API_UNAVAILABLE(macos, macCatalyst, watchos); + (UIFont *)ek_defaultOccurrencePrimaryTextFontForSizeClass:(UIUserInterfaceSizeClass)sizeClass API_DEPRECATED("No replacement. This is not intended for third party use and will return nil.", ios(13.0, 13.4)) API_UNAVAILABLE(macos, macCatalyst, watchos); + (UIFont *)ek_defaultOccurrenceSmallPrimaryTextFontForSizeClass:(UIUserInterfaceSizeClass)sizeClass API_DEPRECATED("No replacement. This is not intended for third party use and will return nil.", ios(13.0, 13.4)) API_UNAVAILABLE(macos, macCatalyst, watchos); @end NS_ASSUME_NONNULL_END
prepareForSegue での segue の identifier による分岐を Swift enum で管理する
まず初めに以下の様なString型の enum を用意する。
enum SegueIdentifier: String { case a case b }
Swift 5 以前
UIViewController
の prepareForSegue
メソッド内で segue.identifier
から enum SegueIdentifier
の作成を行う。
segue.identifier
の型は String?
なので以下の様にguard文(もしくはif let文)で unwrap する必要がある。
override func prepare(for segue: UIStoryboardSegue, sender: Any?) { guard let identifier = segue.identifier, let segueIdentifier = SegueIdentifier(rawValue: identifier) else { return } switch segueIdentifier { case .a: // do something break case .b: // do something break } }
Swift 5.1
Swift 5.1 ではSwitch文に optional の enum
が利用できる様になったのでguard文を一部簡略化して以下の様に書ける。
SegueIdentifier(rawValue: identifier)
が nil
の場合に対応するために case .none:
が必要になる。
override func prepare(for segue: UIStoryboardSegue, sender: Any?) { guard let identifier = segue.identifier else { return } switch SegueIdentifier(rawValue: identifier) { case .a: // do something break case .b: // do something break case .none: break } }
以下の様に case .none:
の代わりに default:
でもコンパイルは通るけど、その場合 enum
の定義が増えた際に新しい enum
の値は default
にマッチしてしまうためコンパイラによる警告が出ない。.none
を使っている場合は新しい enum
の値が記述されておらず case
が網羅されていない事をコンパイラが警告してくれるため .none
を使う方が良いと思われる。
override func prepare(for segue: UIStoryboardSegue, sender: Any?) { guard let identifier = segue.identifier else { return } switch SegueIdentifier(rawValue: identifier) { case .a: // do something break case .b: // do something break default: break } }
Swift 5.1 + extension
Switch文が optional に対応した事で enum
が作成出来ない時(rawValue の値が不正で初期化に失敗した時)はどうせ .none
に引っかかるしわざわざnullチェックのguard文を書かなきゃいけないのは面倒だな、、という気持ちが出てきた。
segue.identifier
の型がString
ではなくString?
enum Foo: String
=protocol RawRepresentable
のイニシャライザの引数が optional を許容しない
init?(rawValue: Self.RawValue)
この2点がguard文を必須にしているため、以下の様な extension を書いて RawRepresentable
に optional な引数を許容するイニシャライザを追加してみる。
gist38ded67adef6a706b05bbaeed3c1c9ff
その結果以下の様に記述できる様になる。
override func prepare(for segue: UIStoryboardSegue, sender: Any?) { // boilerplate has gone! switch SegueIdentifier(rawValue: segue.identifier) { case .a: // do something break case .b: // do something break case .none: break } }
extension を1つ記述するだけで全ての同様の箇所のguard文(もしくはif let文)を消去する事ができる。
ボイラープレートを減らす事ができてスッキリ。
iOS 13 で UIView の beginAnimations 等のアニメーション関連のメソッドが deprecated
タイトルの通り、iOS 13 で以下のメソッドが depecated になった。
UIView - UIKit | Apple Developer Documentation
Animating Views
class func beginAnimations(String?, context: UnsafeMutableRawPointer?) class func commitAnimations() class func setAnimationStart(Date) class func setAnimationsEnabled(Bool) class func setAnimationDelegate(Any?) class func setAnimationWillStart(Selector?) class func setAnimationDidStop(Selector?) class func setAnimationDuration(TimeInterval) class func setAnimationDelay(TimeInterval) class func setAnimationCurve(UIView.AnimationCurve) class func setAnimationRepeatCount(Float) class func setAnimationRepeatAutoreverses(Bool) class func setAnimationBeginsFromCurrentState(Bool) class func setAnimationTransition(UIView.AnimationTransition, for: UIView, cache: Bool)
代替が無くて困るという訳ではないけど既存コードの置き換えが必要になってくる。
beginAnimations, commitAnimations の置き換え
従来は begin
と commit
の間に処理を書けば良いだけだったのが、新パターン*1では Xcode での警告 Use the block-based animation API instead
の通りブロックベースのAPIを使う必要があり、また animated
が true
と false
の時で呼び出すメソッドも変わってくるため一度処理をブロックでまとめてそれぞれのメソッドの引数に渡す必要がある。
func setShowsSomething(_ shows: Bool, animated: Bool) { let block = { // do something } if animated { UIView.animate(withDuration: CATransaction.animationDuration(), animations: block) } else { UIView.performWithoutAnimation(block) } }
また、別のアニメーション処理からこのメソッドが呼び出された際にも正しく指定した時間でアニメーション処理を行って欲しい場合は overrideInheritedDuration: UIView.AnimationOptions
オプションを使うと良い。
func setShowsSomething(_ shows: Bool, animated: Bool) { let block = { // do something } if animated { UIView.animate(withDuration: CATransaction.animationDuration(), delay: 0, options: [.overrideInheritedDuration], animations: block) } else { UIView.performWithoutAnimation(block) } }
逆にこのオプションを指定しない場合は別アニメーションで指定されたアニメーション時間が継承されてアニメーションが発生する事になる。
UIView.animate(withDuration: 1.0) { self.view1.backgroundColor = .red UIView.animate(withDuration: 0.2) { // 0.2 は無視されて1.0で動作する self.view2.backgroundColor = .red } }
継承と上書きのどちらが良いかはケースバイケースだろうけど継承されるのがデフォルトの挙動というのは一応気にした方がいいと思う。
Duration に 0 を指定すればどうか?
animated
が false
の時はアニメーション時間を0にすれば挙動は同じでは?と思うかも知れない(この実装例はまま見る)。
この場合条件分岐は三項演算子で animated ? CATransaction.animationDuration() : 0
とシンプルに書けるしブロックの変数化も不要で直接記述出来る。
func setShowsSomething(_ shows: Bool, animated: Bool) { UIView.animate(withDuration: animated ? CATransaction.animationDuration() : 0, animations: { // do something }) }
1つ気を付けなければならないのはアニメーション時間の継承の問題で、例えば上記のメソッドを以下の様に呼んだとすると*2 animated: false
にも関わらず実際には1.0秒のアニメーションが発生してしまう。
UIView.animate(withDuration: 1.0) { setShowsSomething(true, animated: false) // 1.0秒でアニメーション動作 }
上記の挙動を避けるには animated
false
の際には performWithoutAnimation
を使うか、 withDuration: 0
を指定する場合は以下の様にやはり overrideInheritedDuration: UIView.AnimationOptions
オプションを使って実装する必要がある。
UIView.animate(withDuration: animated ? CATransaction.animationDuration() : 0, delay: 0, options: [.overrideInheritedDuration], animations: { // do something })
余談 performWithoutAnimation と animate:withDuration: 0 の挙動の違い
performWithoutAnimation
animate:withDuration: 0
は挙動が違う点があって、ブロック処理内でさらに呼び出された他の処理のアニメーションが有効か否かが変わってくる。
performWithoutAnimation
が間に挟まるとそれ以下のネストではアニメーションが発生しなくなる。
アニメーションしない
UIView.animate(withDuration: 1.0) { UIView.performWithoutAnimation { UIView.animate(withDuration: 0.2) { // without animation } } }
1.0秒でアニメーション
UIView.animate(withDuration: 1.0) { UIView.animate(withDuration: 0) { UIView.animate(withDuration: 0.2) { // animate with 1.0 sec } } }
performWithoutAnimation
を挟むと overrideInheritedDuration
があろうともアニメーションしない
UIView.animate(withDuration: 1.0) { UIView.performWithoutAnimation { UIView.animate(withDuration: 0.2, delay: 0, options: [.overrideInheritedDuration], animations: { // without animation }) } }
0.2秒でアニメーション
UIView.animate(withDuration: 1.0) { UIView.animate(withDuration: 0) { UIView.animate(withDuration: 0.2, delay: 0, options: [.overrideInheritedDuration], animations: { // animate with 0.2 sec }) } }
余談 performWithoutAnimation と animate:withDuration: 0 のパフォーマンスの違い
以下のコードでそれぞれを10,000回呼び出してみたところおおよそ以下の様な結果に。
やはりアニメーション無し指定の方がパフォーマンスが良いが現実的なケースでは無いのであくまで参考程度に。
iOS 13, iPhone XS | iOS 13, iPad Pro 11インチ | |
---|---|---|
performWithoutAnimation | 0.06-0.07sec | 0.064-0.074sec |
animate:withDuration: 0 | 2.74sec | 2.74sec |
@IBAction func test1() { let start = CFAbsoluteTimeGetCurrent() let view = self.view for _ in 0..<10000 { UIView.performWithoutAnimation { view?.alpha = CGFloat.random(in: 0..<1) } } let end = CFAbsoluteTimeGetCurrent() print(#function, end - start) } @IBAction func test2() { let start = CFAbsoluteTimeGetCurrent() let view = self.view for _ in 0..<10000 { UIView.animate(withDuration: 0) { view?.alpha = CGFloat.random(in: 0..<1) } } let end = CFAbsoluteTimeGetCurrent() print(#function, end - start) }
結論
アニメーション無しの際にネスト以下もアニメーションを無しにするのであれば animated
false
では performWithoutAnimation
を使う。
ネストとパフォーマンスをケアする必要が無い場合は三項演算子+ブロックの直接記述が楽。
overrideInheritedDuration
オプションはケースバイケースで。
最後に duration について
アニメーションのデフォルト値の取得には CATransaction.animationDuration()
を使ったがこのメソッドの実際の返り値は 0.25 になる。
しかし UIView.beginAnimations()
で処理を行った際に UIView.inheritedAnimationDuration
でデフォルトのアニメーション時間を確認してみるとこちらでは 0.2 が取得されるので*3、厳密に挙動を揃えたいのであれば CATransaction.animationDuration()
は使わずに 0.2 を指定する必要がある。
print(CATransaction.animationDuration()) // 0.25 UIView.beginAnimations(nil, context: nil) print(UIView.inheritedAnimationDuration) // 0.2 UIView.commitAnimations()
iOS 13 beta 3 で追加された Core Image API
前回までの変更点
今回は iOS 13 beta 3 で変更点をチェック。
追加フィルタは1つで、他若干の追加APIが見られた。
https://gist.github.com/Koze/86315376f00fa44662f0d841f046e5d2/revisions
iOS 13 beta 3
CIRoundedRectangleGenerator
まず追加フィルタ
roundedRectangleGenerator() - CIFilter | Apple Developer Documentation
試して見たところ UIBezierPath
の init(roundedRect:cornerRadius:)
と同じで rect と radius とを設定して、シェイプの fill color を設定するだけのシンプルなものだった。
transformed(by:highQualityDownsample:)
こちらは追加API
transformed(by:highQualityDownsample:) - CIImage | Apple Developer Documentation
新たに追加されたAPIだけど iOS 10 まで遡って利用可能。ドキュメントは記載が無いがヘッダにコメントが載っていた。
// specifying true or false here will override the context's kCIContextHighQualityDownsample setting.
これまでは CIContext
でハイクオリティのダウンサンプリングを利用するかどうかを設定可能だったが、今回のオプションではコンテキスト全体ではなくてスケール処理の一部でハイクオリティを利用するかどうかをコントロール出来る様になりそう。
kCIInputEnableEDRModeKey
追加のオプションキー
kCIInputEnableEDRModeKey - Core Image | Apple Developer Documentation
こちらもまだドキュメントは無いがヘッダにはよれば以下
/** NSNumber (BOOL) : Allows the output to have an Extended Dynamic Range with values greater than 1 possible */
Extended Dynamic Range を有効にするオプションキーらしい。
@available(iOS 12.0, *) public let kCIInputEnableEDRModeKey: String
現在は String
型になっていて上記の様に宣言されているが、CIRAWFilter
内で宣言されており併記されている他のキーは全て CIRAWFilterOption
型になっているので、このキーも今後のアップデートで CIRAWFilterOption
に属する様になるかも知れない。