Skip to content

fumiyasac/InteractiveUISample

Repository files navigation

InteractiveUISample

[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

※ その他残りのアニメーションを伴うコンテンツ画面に関しては随時追加予定です。

本サンプルの画面キャプチャ

画面キャプチャその1

今回のサンプルの画面一覧その1

画面キャプチャその2

今回のサンプルの画面一覧その2

画面キャプチャその3

今回のサンプルの画面一覧その3

画面キャプチャその4

今回のサンプルの画面一覧その4

InterfaceBuilder構造における図解

Storyboardの構成①:メイン部分

Storyboardの構成:その1

Storyboardの構成②:ストーリー部分

Storyboardの構成:その2

MVP Pattern

MVP Pattern

UI実装設計の重要ポイントになる部分

1. パララックス(視差効果)表現とフェードするアニメーションを組み合わせたアニメーション実装

パララックス(視差効果)表現とフェードするアニメーションを組み合わせたアニメーション実装

動きの仕様メモ:

  1. AutoLayoutのConstraintの変更を利用した画像のパララックス(視差効果)アニメーション
  2. 表示するタイミングでのセル自体のアルファ値を変更するCoreAnimation

2. スクロールの変化量に応じてヘッダー画像とナビゲーションを変化させるアニメーション実装

スクロールの変化量に応じてヘッダー画像とナビゲーションを変化させる

動きの仕様メモ:

  1. コンテンツが一番上にある状態で下にスクロールをすると、ヘッダー画像が伸びるような動きをする。
  2. コンテンツが一番上にある状態で上にスクロールをすると、ヘッダー画像がずれながらダミーのヘッダーが徐々に現れる。(背景のアルファ値が1に近づきながら、タイトルと戻るボタンが下から徐々に現れる)
  3. ヘッダー画像が完全に隠れたら、タイトルと戻るボタンは現れたままの状態になり、更に上へスクロールを続けても位置はそのまま固定されている。

3. カスタムトランジションとアファイン変換を活用した3D回転のような画面遷移に関する解説

3D回転のような画面遷移をするためのカスタムトランジションの設定

iOS13以降のOSに対応するための変更点:

StoryViewController.swiftに対応するInterfaceBuilder上に設定したModal遷移のSegueは下記の様に設定しています。

  • Kind → 「Present Modally」に設定
  • Presntation → 「Full Screen」に設定
  • Transition → 「Cover Vertical」に設定

4. iPhoneXのSafeAreaの考慮と調整について

iPhoneXのSafeAreaの考慮と調整について

【追加コード】

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
    }
}()

5. UIScrollViewの活用

UIScrollViewとContainerViewを組み合わせてタブメニューUIを作成する

UIScrollViewとContainerViewを組み合わせてタブメニューUIを作成する

ScrollViewを利用して複雑なレイアウトを作成する

ScrollViewを利用して複雑なレイアウトを作成する

6. UITableViewの表現

アコーディオンのようにコンテンツを開閉して表示するUITableView

アコーディオンのようにコンテンツを開閉して表示するUITableView

動きの仕様メモ:

  1. UITableViewのStyleを「Plain」から「Grouped」へ変更している。
  2. セクションごとの更新はstoryRelatedTableView.reloadSections(NSIndexSet(index: section) as IndexSet, with: .automatic)で行う
  3. セルに表示するデータと表示・非表示の管理はsectionStateLists: [(extended: Bool, genre: Genre)]が行う。

7. その他UI表現に関する設定

細かな点になりますが、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リクエスト送信&レスポンス取得の非同期処理ハンドリング

補足事項:

  1. AlamofireについてはVer5.x系からは実装方法が大きく変化があった部分になります。 → Alamofire 5 Tutorial for iOS: Getting Started
  2. API通信処理部分におけるSuccess(成功)Failure(失敗)時のハンドリング処理部分にはPromiseKitを利用してPresenter側でも処理がわかりやすくなる様にしています。

解説記事

このサンプル全体の詳細解説とポイントをまとめたものは下記に掲載しております。

About

[ING] - ライブラリを使わないでアニメーション表現を盛り込んだ機能サンプル

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages