for-case 文【Swift実践入門読書メモ】
Swift実践入門を読んでみて、気になったところのメモです。
for-case 文
- 第3章 3節 pp.91-92
Sequence プロトコルに準拠した型の要素のうち、パターンにマッチするものだけを列挙する繰り返し文です。
for 文にも case によるパターンマッチが行えて
let numbers: [Int] = [1, 2, 3, 4, 5] for case 2..4 in numbers { ... }
とか
enum Food { case hamburger, pizza, sushi } let foods: [Food] = [ .hamburger, .pizza, .sushi ] for case .hamburger in foods { ... }
みたいなことができます。
いまいち使い所がわからなかったですが、 以下のエントリで書いた if-case といい今回の for-case といい、色々なところでパターンマッチが行えるのかふむふむと思いました。
今後パターンマッチを行う際には、どの 〇〇-case を使うとスッキリ書けるかを意識しながらコードを書いてみようと思います。
何か良きケースがあったら追記します。
合わせて読みたい
- パターンマッチ
- 第3章 6節 pp.100-104
引数のクロージャの属性【Swift実践入門読書メモ】
Swift実践入門を読んでみて、気になったところのメモです。
引数のクロージャの属性
- 第4章 3節 pp.120-124
@escaping 属性
関数に引数として渡されたクロージャが、関数のスコープ外で保持される可能性があることを示す属性です。
こんな感じで使われます。
func execClosure(_ closure: @escaping () -> Void) { ... }
ということは、逆に言えば @escaping 属性がついていなければスコープ外で保持されることがないということです。そのため、 クロージャ内で self を参照していたりしても循環参照にならないため、 [weak self]
等を用いて弱参照にする必要がなくなります。
以前、以下のエントリで循環参照について考えた際には、どんな時に循環参照になってしまうのか考えるのが大変なので、問答無用で [weak self]
を使用するのが楽そうと思いましたが、 @escaping 属性の有無で判別することができるようです。
でも結局 @escaping 属性をつけるかどうかは人間が考える必要があって大変、ということはなく @escaping 属性をつけるべきときにつけていない場合は、コンパイルエラーとなるので気づくことができます。
class SampleClass { var closure: (() -> Void)? func execClosure(_ closure: () -> Void) { self.closure = closure } }
Playground execution failed: error: SampleApp.playground:4:24: error: assigning non-escaping parameter 'closure' to an @escaping closure self.closure = closure ^ SampleApp.playground:3:24: note: parameter 'closure' is implicitly non-escaping func execClosure(_ closure: () -> Void) { ^ @escaping
そのため、@escaping 属性がついていれば循環参照の恐れがあるので、クロージャによってキャプチャされる変数は [weak self]
等を用いて弱参照にしておく、ついていなければ強参照のまま利用するというようにすると良さそうです。
@autoclosure 属性
引数をクロージャで包むことで遅延評価を実現するための属性です。
引数の遅延評価が有効なケース
以下の printResults(result1:result2:)
は、第一引数の値が true なら最初の分岐以降の処理は実行されません。
func printResults(result1: Bool, result2: Bool) { if result1 { print("result1 is true") } else if result2 { print("result1 is false, result2 is true") } else { print("result1 is false, result2 is true") }
処理の順序はこんな感じになります。
- result1 を評価する
- true なら
print("result1 is true")
して終了 - false なら result2 を評価する
- true なら
print("result1 is false, result2 is true")
して終了 - false なら else 節に入って
print("result1 is false, result2 is true")
して終了
- true なら
- true なら
ですが、以下のように引数に関数の実行結果を直接渡している場合には、渡された関数は関数内の処理に入る前に実行されます。
printResult(result1: result1(), result2: result2())
- result1() を評価する ← 関数の処理前
- result2() を評価する ← 関数の処理前
- result1() の結果である result1 を評価する
- true なら
print("result1 is true")
して終了 - false なら result2() の結果である result2 を評価する
- true なら
print("result1 is false, result2 is true")
して終了 - false なら else 節に入って
print("result1 is false, result2 is true")
して終了
- true なら
- true なら
このとき、 result1() の結果が true になるのであれば result2() の評価は無駄になってしまいます。
これを防ぐために遅延評価が有効となります。
クロージャで包む
関数の実行結果を直接渡すと関数内の処理に入る前に実行されてしまいますが、以下のようにクロージャで包むと遅延評価を行うことができます。
func printResults(result1: Bool, result2: () -> Bool) { if result1 { print("result1 is true") } else if result2() { print("result1 is false, result2 is true") } else { print("result1 is false, result2 is true") } } printResults(result1: true, result2: { return result2() })
ですが、これだと printResults(result1:result2:)
のインターフェースが変わってしまいます(第二引数に渡す型が Bool から () -> Bool に変わっている) 。
そこで @autoclosure の登場です。
@autoclosure をつけると暗黙的にクロージャに包んでくれるので、関数のインターフェースは変わらずに関数を実行することができます。
func printResults(result1: Bool, result2: @autoclosure () -> Bool) { if result1 { print("result1 is true") } else if result2() { print("result1 is false, result2 is true") } else { print("result1 is false, result2 is true") } } printResults(result1: true, result2: result2())
@autoclosure をつけるとインターフェースが変わらないのは確かに良いなあと思いました。
ですが、あまり遅延評価を要するコードを書く機会がない (書くべきところに気づけていない?) ので、自分が使うことはあまりないような気がしました。
合わせて読みたい
- 弱参照による循環参照への対処
- 第11章 2節 pp.274-275
- クロージャ
- 第11章 3節 pp.276-285
if-case 文 【Swift実践入門読書メモ】
Swift実践入門を読んでみて、気になったところのメモです。
if-case 文
- 第3章 2節 p.81
if-case
を用いることで、以下のようなパターンマッチによる分岐が行えます。
if case 0...5 = 3 { print ("パターンにマッチしています") }
比較対象が式の右辺にくるということと、 =
が使われていることから、あまり直感的ではないのかなあという気がしました 。
そのため、上記のような Range を用いた式を書く場合には contains(:_)
や ~=
で事足りるということから if case
を積極的に使う必要もなさそうかなと思いました。
ですが、Enum の場合には使い所がありそうなので、全体を統一するという目的で Range においても if case
を使っていくのはありなのかなと思ったりしました。
Enum の場合には使い所がありそう
ですが、以下のように associated value が定義されている場合、 ==
を使った式で分岐させようとするとコンパイルエラーとなってしまいます。
enum Food { case hamburger(name: String) } let cheeseBurger = Food.hamburger(name: "cheese") if cheeseBurger == .hamburger { print("ハンバーガー") }
error: binary operator '==' cannot be applied to operands of type 'Food' and '_' if cheeseBurger == .hamburger { ~~~~~~~~~~~~ ^ ~~~~~~~~~~
かと言って switch 文を使うと記述量も多くなってしまうので、こういう時には if-case
を使うとシュッとかけて良さそうだなあと思いました。
if case .hamburger = cheeseBurger { print("ハンバーガー") }
合わせて読みたい
- パターンマッチ
- 第3章 6節 pp.100-104
画面遷移のアニメーションをカスタムする
UINavigationController を使った画面遷移のアニメーションのデフォルトは、横からスライドするアニメーションですが、これをカスタムすることができます。
最近、アニメーションをカスタムしたりといったようにカスタムトランジションについて学んでみたので、実装方法を書いてみたいと思います。
登場人物
アニメーションを管理するので少しコード量が多くなってしまって複雑に感じるかと思うのですが、登場するクラスとプロトコルに関しては以下の3つだけです。
- UINavigationController (クラス)
- UINavigationControllerDelegate (プロトロル)
- UIViewControllerAnimatedTransitioning (プロトコル)
カスタムトランジションのざっくりとした流れとしては、以下の通りです。
- 画面遷移のメソッドが呼ばれる (UINavigationController)
- 画面遷移のアニメーションを決定する (UINavigationControllerDelegate)
- アニメーションをする (UIViewControllerAnimatedTransitioning)
UIViewControllerAnimatedTransitioning
画面遷移時に行うアニメーションを定義するためのプロトコルです。
アニメーションをカスタムするためには、このプロトコルに準拠したオブジェクトを用意する必要があります。カスタムアニメーションに必要な最低限実装すべきメソッドは以下の2つです。
- func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval
- func animateTransition(using transitionContext: UIViewControllerContextTransitioning)
以下、上記メソッドの引数名は省略します。
transitionDuration
アニメーションにかける時間を定義するためのメソッドです。
animateTransition
遷移時に行うアニメーションを定義するためのメソッドです。このメソッド内でどういったアニメーションを行うのかを実装していきます。
アニメーションは、 このメソッドの引数として渡ってくる UIViewControllerContextTransitioning を用いて実装していくことになります。 このクラスが、遷移前の View 、遷移後の View、アニメーションを描写するための View を持っているので、これらを取り出してアニメーションを組み立てていきます。
なお、プッシュ時、ポップ時と共にこちらのメソッドが呼ばれるため、内部で分岐してアニメーションを切り替える必要があります。
しかし、このメソッドの引数として渡ってくる UIViewControllerContextTransitioning にはプッシュ、ポップを判別するための API が用意されていないため、自前で用意する必要があります。
UINavigationControllerDelegate
この Protocol にカスタムトランジションの様々な設定を行うためのメソッドが用意されています。 今回はアニメーションを変更するだけなので、以下のメソッドを実装します。
このメソッドの返り値として、 UIViewControllerAnimatedTransitioning に準拠したオブジェクトを返すことで、画面遷移時にカスタムしたアニメーションが行われるようになります。
nil
を返すと、横からスライドするデフォルトのアニメーションとなります。
UINavigationController
わざわざ書くまでもないかと思いますが、このクラスによって画面遷移を行います。 上記の UINavigationControllerDelegate に準拠したオブジェクトを UINavigationController オブジェクトの delegate プロパティにセットすることで、画面遷移時に UINavigationControllerDelegate の各メソッドが呼ばれるようになります。
具体的な実装
UIViewControllerAnimatedTransitioning
このプロトコルに準拠したクラスを作ります。
UIViewControllerAnimatedTransitioning が NSObjectProtocol を継承しているので、作成するクラスも NSObject を継承させる必要があります。
class CustomAnimatedTransitioning: NSObject, UIViewControllerAnimatedTransitioning { func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { // ... } func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { // ... } }
transitionDuration
アニメーションにかける、良い感じの値を返すようにするだけです。
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return 0.3 }
animateTransition
先にコードを書いてしまうと、プッシュの場合は以下のようになります。
// (1) guard let sourceView = transitionContext.view(forKey: .from), let destinationView = transitionContext.view(forKey: .to) else { // アニメーション終了の通知 transitionContext.completeTransition(!transitionContext.transitionWasCancelled) return } // (2) let containerView = transitionContext.containerView containerView.addSubview(sourceView) containerView.insertSubview(destinationView, aboveSubview: sourceView) // (3) UIView.animate( withDuration: transitionDuration(using: transitionContext), animations: { // プッシュのカスタムアニメーション処理 }, completion: { _ in // アニメーション終了の通知 transitionContext.completeTransition(!transitionContext.transitionWasCancelled) } )
(1) はじめに、引数の transitionContext からアニメーションに必要なオブジェクトを取り出します。
transitionContext.view(forKey: .from)
で遷移元の画面の View、 transitionContext.view(forKey: .to)
で遷移元の画面の View が取得できます。
これらのメソッドの返り値は Optional なので、取得できなかった場合は return
していますが、このとき transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
によって画面遷移が終了したことを通知します。
このメソッドの引数には画面遷移が正常に終了したかどうかを Bool で渡します。
transitionContext.transitionWasCancelled
で画面遷移がキャンセルされたかどうかを知ることができるので、今回はこの結果を渡しています。
(2) 続いて、transitionContext.containerView
により、 transitionContext からアニメーションを描画するためのキャンバスとなる View を取り出します。
そして、この View に先ほど取り出しておいた View 達を addSubview
や insertSubview
で追加していきます。
これでアニメーションさせるための前準備は終わりです。
(3) 最後に、画面遷移時に行いたいアニメーション処理を実行します。
アニメーションが終えたら、画面遷移が終了となるので、これを通知するために transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
を実行します (試してはいませんが、このタイミングでは引数を true
に固定してしまっても良いのかもしれません) 。
ポップの場合の処理も追加すると以下のようになるかと思います。
if プッシュなら { guard let sourceView = transitionContext.view(forKey: .from), let destinationView = transitionContext.view(forKey: .to) else { transitionContext.completeTransition(!transitionContext.transitionWasCancelled) return } let containerView = transitionContext.containerView containerView.addSubview(sourceView) containerView.insertSubview(destinationView, aboveSubview: sourceView) UIView.animate( withDuration: transitionDuration(using: transitionContext), animations: { // プッシュのカスタムアニメーション処理 }, completion: { _ in transitionContext.completeTransition(!transitionContext.transitionWasCancelled) } ) } else { guard let sourceView = transitionContext.view(forKey: .to), let destinationView = transitionContext.view(forKey: .from) else { transitionContext.completeTransition(!transitionContext.transitionWasCancelled) return } let containerView = transitionContext.containerView containerView.addSubview(destinationView) containerView.insertSubview(sourceView, aboveSubview: destinationView) UIView.animate( withDuration: transitionDuration(using: transitionContext), animations: { // ポップのカスタムアニメーション処理 }, completion: { _ in transitionContext.completeTransition(!transitionContext.transitionWasCancelled) } ) }
プッシュとポップの判別
後ほど説明しますが、 UIViewControllerAnimatedTransitioning に準拠したオブジェクトを生成するタイミングで画面遷移時の UINavigationControllerOperation の値を取得することができるので、これを保持しておいて参照できるようにしておくのが、個人的には良いのではないかと思っています。
let operation: UINavigationControllerOperation init(operation: UINavigationControllerOperation) { self.operation = operation } func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { switch operation { case .push: // プッシュのカスタムアニメーション処理 case .pop: // ポップのカスタムアニメーション処理 case .none: break } }
UINavigationControllerDelegate
このプロトコルに準拠したクラスを作ります。
こちらも NSObjectProtocol を継承しているので、作成するクラスも NSObject を継承させる必要があります。
今回は UINavigationController を継承したクラスを作成し、このクラスに適用することにします。
実装すべきメソッドを実装すると以下のようになります。
class NavigationController: UINavigationController, UINavigationControllerDelegate { func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { return CustomAnimatedTransitioning(operation: operation) } }
このメソッドの返り値として先ほどつくったクラスのオブジェクトを返すことで、画面遷移時にカスタムアニメーションが行われるようになります。
UIViewControllerAnimatedTransitioning に準拠したオブジェクトを生成するタイミングで画面遷移時の UINavigationControllerOperation の値を取得することができるので
先ほど上記のように書きましたが、それがこのタイミングとなります。
このメソッドの引数として 画面遷移の種類を表す UINavigationControllerOperation が渡ってくるので、これを CustomAnimatedTransitioning の初期化時に渡してあげることで、 animateTransition
が呼ばれたタイミングでプッシュとポップを判別することができるようになり、アニメーションを切り替えることができます。
引数の fromVC
と toVC
は、それぞれ遷移元、遷移先の ViewController です。そのため、画面によってデフォルトのアニメーションとカスタムアニメーションを切り替えたい場合には、以下のように分岐させることで可能となります。
if toVC is CustomViewController || fromVC is CustomViewController { return CustomAnimatedTransitioning(operation: operation) } else { // nil を返すとデフォルトのアニメーション return nil }
UINavigationController
画面遷移に使用するこのクラスのオブジェクトの delegate プロパティに、先ほどの UINavigationControllerDelegate に準拠したオブジェクトをセットします。そうすることで、画面遷移のメソッドが呼ばれた時に delegate で実装したメソッドが呼ばれ、カスタムアニメーションが実行されることになります。
なお、画面遷移のメソッドが呼ばれるタイミングより前に delegate をセットする必要があります。今回は、上記ですでに UINavigationController を継承したクラスをつくっているので、この init 内で delegate をセットするようしています。
class NavigationController: UINavigationController { required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) } override init(rootViewController: UIViewController) { super.init(rootViewController: rootViewController) delegate = self } } extension NavigationController: UINavigationControllerDelegate { func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { if toVC is DestinationViewController || fromVC is DestinationViewController { return CustomAnimatedTransitioning(operation: operation) } else { return nil } } }
以上で、アニメーションをカスタムするための実装は完了となります!
おわりに
この記事の頭に表示している GIF のアニメーションと同様のアニメーションを行うサンプルコードも用意してみたので、よかったらこちらも確認してみてください。
次は、インタラクティブなカスタムアニメーションを行うための実装について書いてみたいと思います。
UIPickerView に UILabel を表示するライブラリをつくった
タイトルの通り、 UIPickerView に UILabel を表示するライブラリを作りました。
これを使うとこんな感じにコンポーネントごとに簡単にラベルを表示することができます。
なお、Carthage からインストールできます。
使い方
LabelPickerComponent
LabelPickerComponent という、コンポーネントとラベルをまとめたクラスのインスタンスを用意します。
let hours = (0...23).map { "\($0)" } let attributes = [ NSFontAttributeName: UIFont.systemFont(ofSize: 16.0) ] let components = [ LabelPickerComponent( items: hours, itemAttributes: attributes maxItemWidth: 20.0, labelName: "hours", labelAttributes: attributes, labelNameWidth: 50.0 ) ]
maxItemWidth
が item を表示するために必要な width で、items の中で最も大きい item に合わせて指定してください。
labelNameWidth
は labelName を表示するために必要な width を指定してください。
LabelPicker
上記で用意したインスタンスを、 PickerView となる LabelPicker クラスのインスタンスにセットし、コンポーネントを表示する height を指定すると、ラベルの位置が自動で計算されます。
let pickerView = LabelPickerView() pickerView.components = components pickerView.rowHeight = 30.0 view.addSubview(pickerView)
PickerView の frame や maxItemWidth, labelNameWidth を使ってごりごり計算しているので、意図した表示にならない場合には maxItemWidth と labelNameWidth を調整して試してみてください。
オプション
オプションで見た目をちょっと変更できるプロパティを LabelPicker に2つ用意しました。
componentsWidthEqual
全コンポーネントの width を等しくするかどうかの設定です。
true にすると全ての width が等しくなり、 false にするとコンポーネント間のスペースが等しくなるようにレイアウトされます。
(下の画像だと少しわかりづらいですね、、)
true ( デフォルト ) | false |
---|---|
contentSeparateWidth
item と labelName 間のスペースを指定できます。
0.0 ( デフォルト ) | 15.0 |
---|---|
最後に
先日アプリを公開したのですが、このアプリの時間指定する UI に LabelPicker を使っているので、よかったら確認してみてください。
この記事を書いていて色々使いづらそうな点に気づいたので、そのうち修正してみようと思います。
初めてのライブラリ作成だったので、 ライブラリ開発の流れや、Carthage でインストールできるようにするための手順や、 CI でのテストの回し方等、あらゆるところでで色々つまづいたので、その辺りをまたブログにまとめようと思います。
iPhone用のアラームアプリを作った【iOS】
初の個人アプリをリリースしました!
そのアプリがこちらです。
簡単に言うとアラームアプリです。 アラームアプリは App Store にたくさんあるのですが、自分が欲しかった
- 秒まで指定できる
- 通知はバイブレーションのみ(音が鳴らない)
の2つの機能が備わっているものが見つからなかったので作ってみました(よく探せばあるかもですが) 。
作るに至った経緯
毎日、通勤で約1時間電車に乗っていたのですが、座れる時にはアラームをセットして寝ていました。
その際には、寝過ごさないように、他の方が作ったアラームを使って目的駅に到着する1分前に時間をセットしていました。
そのため、1分前になったらアラームが鳴って起きてはいたのですが、めちゃくちゃ眠い状態だと判断が鈍ってしまうようで、1分あればもう一度寝られると考えて二度寝してしまい、危うく乗り過ごしそうになることが何度もありました。
幸い乗り過ごしたことは一度もなかったのですが、これはいつかやらかすと思い、そもそも二度寝したくてもできないぎりぎりの時間(30秒前とか)をセットできれば良いのではと思いました。
ですが、この時使っていたアプリは1分単位でしか時間をセットできなかったので、秒まで指定できるアラームを探したところ、自分の欲しいようなアプリがなかなかなかったので、今回のアプリを作るに至りました。
技術的な話
このアプリを作った目的の一つとしては、新しいことを学ぶということもありました。
業務では、結構大きなサービスの iOS 開発をしているのですが、普段触るものとは違う技術を触ってみたい、また、一からアプリを作ることで、いつもであれば学ぶことのできない新しい学びがあるのではないかというように思っていました。
RxSwift + MVVM
iOS 開発のアーキテクチャについて学びたいと思っていたので、流行りの RxSwift を使い、結果的に MVVM について学ぼうということで RxSwift を使いました。
とりあえず Rx の雰囲気は掴めたような気がしているのですが、 RxSwift でコードちゃんと書いていますと言えるほどには身につかなかったなあという気がしています。
ですが、 RxSwift の学習コストの高さや、データバインドの良さや、関数型言語のように記述できることの便利さ等は実感できたので、ひとまずよかったかなあと思います。
ライブラリ公開
ライブラリを作ったことがなかったので、アプリを作っていく中で、ある程度抽象化できそうな部分があったらライブラリを作って公開してみたいなと思いました。
結果的に、 LabelPicker という UIPickerView 関連のライブラリを公開することができました。
アプリ内では、時刻、時間を指定する UI に使用しています。
標準の UIDatePicker でも時刻、時間はもちろん指定できるのですが、秒指定と単位の表示ができなかったので、これらを解決するためのものとして作りました。
これでライブラリの作り方を知れたので、また何か抽象化できそうなことがあればやってみようと思います。
他にも学べることはたくさんあったので、やって満足しています!
残念だった点としては、今はもう引っ越してしまい、長い電車通勤ではなくなってしまったため、ドッグフーディングができなくなってしまったということがありました。
でもまあ、日常生活の中で利用する機会はあるかと思うのでその際には使っていきます。
アプリに関して何かあればコメントください〜
バイブアラーム プライバシーポリシー
本サービスは、サービスの改善・広告配信の目的で、端末情報やサービスの利用状況を取得し、利用することがあります。
これらの情報は、サードパーティのサービスプロバイダを介して扱います。
本サービスでは、広告配信のために AdMob, サービスの利用状況を取得するために Firebase を使用しています。
これらのサービスのプライバシーポリシーは下記をご確認ください。
ご不明点やご提案がある場合は、遠慮なくご連絡ください。
なお、本ポリシーは必要に応じて変更することがあります。
Auto Layout の Content Hugging Priority と Content Compression Resistance Priority
すぐ忘れるので。
Content Hugging Priority
コンテンツの表示サイズに沿って表示されることの優先度。
そのため、この値が大きい方が優先的にコンテンツと同じサイズになる。
Content Compression Resistance Priority
コンテンツが圧縮されないことの優先度。
そのため、この値が大きい方が優先的にコンテンツが見切れないようになる。