[ING]ライブラリを使わないでアニメーション表現を盛り込んだ機能サンプル(iOS Sample Study: Swift)
UIのスクロールやタブUIの切り替えを伴うようなコンテンツにて動きの中でポイントとなりそうな部分にアニメーション不自然にならない心地よいタイミングでに盛り込むプラクティスをするために作成したサンプルになります。
今回のサンプルに関しては、主に下記の6つの機能についてを実装しています。
- UITableViewのセルが出現した際にふわっとフェードインがかかる動き → 特にAWAの動きを参考にした部分
- スクロールの変化量に伴って他のView要素の位置が切り替わる動き → ヘッダーの動き方はReactNativeのサンプルもプラスで参考にした部分
- UIButtonやUILabelに一工夫を加えた動き → 独自で実装をした部分
- カスタムトランジションとアファイン変換を活用した3D回転のような画面遷移 → 参考:Custom UIViewController Transitions: Getting Started (raywnderlich.com)
- アコーディオンのようにコンテンツを開閉して表示するUITableView → セクション単位で折りたたんでコンテンツを表示・非表示を切り替える動き
- UIScrollViewとUIImageViewを組み合わせて拡大・縮小ができるフォトギャラリー → その他のアプリでもよくあるフォトギャラリーのようなUI
※ その他残りのアニメーションを伴うコンテンツ画面に関しては随時追加予定です。
動きの仕様メモ:
- AutoLayoutのConstraintの変更を利用した画像のパララックス(視差効果)アニメーション
- 表示するタイミングでのセル自体のアルファ値を変更するCoreAnimation
動きの仕様メモ:
- コンテンツが一番上にある状態で下にスクロールをすると、ヘッダー画像が伸びるような動きをする。
- コンテンツが一番上にある状態で上にスクロールをすると、ヘッダー画像がずれながらダミーのヘッダーが徐々に現れる。(背景のアルファ値が1に近づきながら、タイトルと戻るボタンが下から徐々に現れる)
- ヘッダー画像が完全に隠れたら、タイトルと戻るボタンは現れたままの状態になり、更に上へスクロールを続けても位置はそのまま固定されている。
iOS13以降のOSに対応するための変更点:
StoryViewController.swift
に対応するInterfaceBuilder上に設定したModal遷移のSegueは下記の様に設定しています。
- Kind → 「Present Modally」に設定
- Presntation → 「Full Screen」に設定
- Transition → 「Cover Vertical」に設定
【追加コード】
iPhoneXをはじめとする、ノッチがある端末の判定については値での判定をしないで下記のような形で判定する方が良さそうに思います。
// -----
// (1) ノッチ判定用のExtension
// -----
extension UIDevice {
func hasNotch() -> Bool {
if let keyWindow = UIApplication.shared.windows.first(where: { $0.isKeyWindow }), keyWindow.safeAreaInsets.bottom > 0 {
return true
}
return false
}
}
// -----
// (2) SafeAreaの有無によって調整が必要な部分での利用例
// -----
// グラデーションヘッダー用のY軸方向の位置(iPhoneX用に補正あり)
private let gradientHeaderViewPositionY: CGFloat = {
let window = UIApplication.shared.windows.filter { $0.isKeyWindow }.first
let statusBarHeight = window?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0
return -statusBarHeight
}()
// ナビゲーションバーの高さ(iPhoneX用に補正あり)
private let navigationBarHeight: CGFloat = {
if UIDevice.current.hasNotch() {
return 88.5
} else {
return 64.0
}
}()
動きの仕様メモ:
- UITableViewのStyleを「Plain」から「Grouped」へ変更している。
- セクションごとの更新は
storyRelatedTableView.reloadSections(NSIndexSet(index: section) as IndexSet, with: .automatic
)で行う - セルに表示するデータと表示・非表示の管理は
sectionStateLists: [(extended: Bool, genre: Genre)]
が行う。
細かな点になりますが、iOS13以降で改めて必要となった変更点についてのメモになります。
従来通りのModal表示をするための追加対応 ※iOS13以上:
※ 特にカスタムトランジションを伴う部分でこの実装を忘れてしまうと、画面遷移に不具合が発生する場合があります。
// カスタムトランジションのプロトコルを適用させる
let navigationController = UINavigationController(rootViewController: storyPageViewController)
navigationController.transitioningDelegate = self
// Modalの画面遷移を実行する
// MEMO: iOS13以降のPresent/Dismiss時の調整
// Present/Dismissで実行するカスタムトランジションの場合ではこの設定を忘れると画面遷移がおかしくなるので注意
if #available(iOS 13.0, *) {
navigationController.modalPresentationStyle = .fullScreen
}
self.present(navigationController, animated: true, completion: nil)
UINavigationBarにおけるBackButton長押しの無効化 ※iOS14以上:
※ UIBarButtonItemを継承したクラスを用意し、長押しメニューのsetter部分を空にしてしまう形に変更します。
// UIViewControllerの拡張
extension UIViewController {
// 戻るボタンの「戻る」テキストを削除した状態にするメソッド
func removeBackButtonText() {
let backButtonItem = BackBarButtonItem(title: "", style: .plain, target: nil, action: nil)
self.navigationController!.navigationBar.tintColor = UIColor.white
self.navigationItem.backBarButtonItem = backButtonItem
}
}
class BackBarButtonItem: UIBarButtonItem {
@available(iOS 14.0, *)
override var menu: UIMenu? {
set {
// MEMO: 長押しメニューを消去する
// Do Nothing.
}
get {
return super.menu
}
}
}
Scrollの挙動に合わせたUINavigation部分に重ねる変化を加える場合の補足 ※iOS15以上:
iOS15以上
// -----
// (1) UINavigationBarを透過する部分の抜粋
// -----
// NavigationControllerのカスタマイズを行う
if #available(iOS 15.0, *) {
// MEMO: iOS14以前で実施していた調整をiOS15で実施する場合には、
// self.navigationController?.navigationBar → navigationBarAppearanceで設定していく方針を取ることになります。
// ※ navigationBarAppearanceでは便利なプロパティも増えています。
let navigationBarAppearance = UINavigationBarAppearance()
navigationBarAppearance.configureWithOpaqueBackground()
navigationBarAppearance.titleTextAttributes = [
NSAttributedString.Key.font : UIFont(name: "HelveticaNeue-Bold", size: 14.0)!,
NSAttributedString.Key.foregroundColor : UIColor.clear
]
navigationBarAppearance.backgroundColor = UIColor.clear
navigationBarAppearance.shadowColor = UIColor.clear
navigationBarAppearance.shadowImage = UIImage()
UINavigationBar.appearance().isTranslucent = true
UINavigationBar.appearance().standardAppearance = navigationBarAppearance
UINavigationBar.appearance().scrollEdgeAppearance = navigationBarAppearance
} else {
self.navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
self.navigationController?.navigationBar.shadowImage = UIImage()
self.navigationController?.navigationBar.tintColor = UIColor.white
self.navigationItem.hidesBackButton = true
}
// -----
// (2) 普通にUINavigationBarを表示する部分の抜粋
// -----
// MEMO: 遷移元となるArticleViewControllerでUINavigationBarで変更を加えてしまっているので、この部分で元の設定を再度適用する
if #available(iOS 15.0, *) {
let navigationBarAppearance = UINavigationBarAppearance()
navigationBarAppearance.configureWithOpaqueBackground()
navigationBarAppearance.titleTextAttributes = [
NSAttributedString.Key.font : UIFont(name: "HelveticaNeue-Bold", size: 14.0)!,
NSAttributedString.Key.foregroundColor : UIColor.white
]
navigationBarAppearance.backgroundColor = UIColor(code: "#76b6e2")
UINavigationBar.appearance().standardAppearance = navigationBarAppearance
UINavigationBar.appearance().scrollEdgeAppearance = navigationBarAppearance
} else {
self.navigationController?.navigationBar.barTintColor = ColorDefinition.navigationColor.getColor()
self.navigationController?.navigationBar.isTranslucent = false
self.navigationController?.navigationBar.tintColor = UIColor.white
self.navigationController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor : UIColor.white]
}
UIまわりの実装と直接関係のない部分に関しては、下記のライブラリを使用しました。
ライブラリ名 | 当該ライブラリの用途 |
---|---|
SwiftyJSON | JSONデータの解析をしやすくする |
Alamofire | HTTP/HTTPSのネットワーク通信用 |
SDWebImage | 画像URLからの非同期での画像表示とキャッシュサポート |
FontAwesome.swift | 「Font Awesome」アイコンの利用 |
PromiseKit | APIリクエスト送信&レスポンス取得の非同期処理ハンドリング |
補足事項:
- AlamofireについてはVer5.x系からは実装方法が大きく変化があった部分になります。 → Alamofire 5 Tutorial for iOS: Getting Started
- API通信処理部分における
Success(成功)
&Failure(失敗)
時のハンドリング処理部分にはPromiseKitを利用してPresenter側でも処理がわかりやすくなる様にしています。
このサンプル全体の詳細解説とポイントをまとめたものは下記に掲載しております。