Skip to content

Instantly share code, notes, and snippets.

@fumiyasac
Last active November 21, 2018 08:04
Show Gist options
  • Save fumiyasac/3934ba8f44a8250d089ec5b1e945c79d to your computer and use it in GitHub Desktop.
Save fumiyasac/3934ba8f44a8250d089ec5b1e945c79d to your computer and use it in GitHub Desktop.
できるだけUI系のライブラリを用いないアニメーションを盛り込んだサンプル実装まとめ(前編) ref: https://qiita.com/fumiyasac@github/items/d1b56ffc6d7d46c0a616
import Foundation
import UIKit
import SwiftyJSON
struct Article {
//メンバ変数(取得したJSONレスポンスのKeyに対応する値が入る)
let id: Int
let thumbnailUrl: String
let title: String
let category: String
let mainText: String
let viewsCounter: Int
let likesCounter: Int
let summaryTitle: String
let summaryText: String
let publishedAt: String
//イニシャライザ(取得したJSONレスポンスに対して必要なものを抽出する)
init(json: JSON) {
self.id = json["id"].int ?? 0
self.thumbnailUrl = json["thumbnailUrl"].string ?? ""
self.title = json["title"].string ?? ""
self.category = json["category"].string ?? ""
self.mainText = json["mainText"].string ?? ""
self.viewsCounter = json["viewsCounter"].int ?? 0
self.likesCounter = json["likesCounter"].int ?? 0
self.summaryTitle = json["summaryTitle"].string ?? ""
self.summaryText = json["summaryText"].string ?? ""
self.publishedAt = json["publishedAt"].string ?? ""
}
}
import Foundation
import UIKit
class ArticleCustomTransition: NSObject {
//トランジション(実行)の秒数
fileprivate let duration: TimeInterval = 0.30
//ディレイ(遅延)の秒数
fileprivate let delay: TimeInterval = 0.00
//トランジションの方向(present: true, dismiss: false)
var presenting = true
}
extension ArticleCustomTransition: UIViewControllerAnimatedTransitioning {
//アニメーションの時間を定義する
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
/**
* アニメーションの実装を定義する
* この場合には画面遷移コンテキスト(UIViewControllerContextTransitioningを採用したオブジェクト)
* → 遷移元や遷移先のViewControllerやそのほか関連する情報が格納されているもの
*/
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
//コンテキストを元にViewのインスタンスを取得する(存在しない場合は処理を終了)
guard let fromView = transitionContext.view(forKey: UITransitionContextViewKey.from) else {
return
}
guard let toView = transitionContext.view(forKey: UITransitionContextViewKey.to) else {
return
}
//アニメーションの実態となるコンテナビューを作成する
let containerView = transitionContext.containerView
//ContainerView内の左右のオフセット状態の値を決定する
let offScreenRight = CGAffineTransform(translationX: containerView.frame.width, y: 0)
let offScreenLeft = CGAffineTransform(translationX: -containerView.frame.width, y: 0)
//遷移先のViewControllerの初期位置を決定する
if presenting {
toView.transform = offScreenRight
} else {
toView.transform = offScreenLeft
}
//アニメーションの実体となるContainerViewに必要なものを追加する
containerView.addSubview(toView)
//NavigationControllerに似たアニメーションを実装する
UIView.animate(withDuration: duration, delay: delay, options: .curveEaseInOut, animations: {
//遷移元のViewControllerを移動させる
if self.presenting {
fromView.transform = offScreenLeft
} else {
fromView.transform = offScreenRight
}
//遷移先のViewControllerが画面に表示されるようにする
toView.transform = CGAffineTransform.identity
}, completion:{ finished in
transitionContext.completeTransition(true)
})
}
}
import UIKit
import SDWebImage
class ArticleHeaderView: UIView {
private var imageView = UIImageView()
private var imageViewHeightLayoutConstraint = NSLayoutConstraint()
private var imageViewBottomLayoutConstraint = NSLayoutConstraint()
private var wrappedView = UIView()
private var wrappedViewHeightLayoutConstraint = NSLayoutConstraint()
//MARK: - Initializer
//このカスタムビューをコードで使用する際の初期化処理
required override init(frame: CGRect) {
super.init(frame: frame)
setupArticleHeaderView()
}
//このカスタムビューをInterfaceBuilderで使用する際の初期化処理
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupArticleHeaderView()
}
//MARK: - Function
//バウンス効果のあるUIImageViewに表示する画像をセットする
func setHeaderImage(_ thumbnailUrl: String) {
imageView.sd_setImage(with: URL(string: thumbnailUrl))
}
//UIScrollView(今回はUITableView)の変化量に応じてAutoLayoutの制約を動的に変更する
func setParallaxEffectToHeaderView(_ scrollView: UIScrollView) {
//スクロールビューの上方向の余白の変化量をwrappedViewの高さに加算する
//参考:http://blogios.stack3.net/archives/1663
wrappedViewHeightLayoutConstraint.constant = scrollView.contentInset.top
//Y軸方向オフセット値を算出する
let offsetY = -(scrollView.contentOffset.y + scrollView.contentInset.top)
//Y軸方向オフセット値に応じた値をそれぞれの制約に加算する
wrappedView.clipsToBounds = (offsetY <= 0)
imageViewBottomLayoutConstraint.constant = (offsetY >= 0) ? 0 : -offsetY / 2
imageViewHeightLayoutConstraint.constant = max(offsetY + scrollView.contentInset.top, scrollView.contentInset.top)
}
//MARK: - Private Function
private func setupArticleHeaderView() {
self.backgroundColor = UIColor.white
/**
* ・コードでAutoLayoutを張る場合の注意点等の参考
*
* (1) Auto Layoutをコードから使おう
* http://blog.personal-factory.com/2016/01/11/make-auto-layout-via-code/
*
* (2) Visual Format Languageを使う【Swift3.0】
* http://qiita.com/fromage-blanc/items/7540c6c58bf9d2f7454f
*
* (3) コードでAutolayout
* http://qiita.com/bonegollira/items/5c973206b82f6c4d55ea
*/
//Autosizing → AutoLayoutに変換する設定をオフにする
wrappedView.translatesAutoresizingMaskIntoConstraints = false
wrappedView.backgroundColor = UIColor.white
self.addSubview(wrappedView)
//このViewに対してwrappedViewに張るConstraint(横方向 → 左:0, 右:0)
let wrappedViewConstarintH = NSLayoutConstraint.constraints(
withVisualFormat: "H:|[wrappedView]|",
options: NSLayoutFormatOptions(rawValue: 0),
metrics: nil,
views: ["wrappedView" : wrappedView]
)
//このViewに対してwrappedViewに張るConstraint(縦方向 → 上:なし, 下:0)
let wrappedViewConstarintV = NSLayoutConstraint.constraints(
withVisualFormat: "V:[wrappedView]|",
options: NSLayoutFormatOptions(rawValue: 0),
metrics: nil,
views: ["wrappedView" : wrappedView]
)
self.addConstraints(wrappedViewConstarintH)
self.addConstraints(wrappedViewConstarintV)
//wrappedViewの縦幅をいっぱいにする
wrappedViewHeightLayoutConstraint = NSLayoutConstraint(
item: wrappedView,
attribute: .height,
relatedBy: .equal,
toItem: self,
attribute: .height,
multiplier: 1.0,
constant: 0.0
)
self.addConstraint(wrappedViewHeightLayoutConstraint)
//wrappedViewの中にimageView入れる
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.backgroundColor = UIColor.white
imageView.clipsToBounds = true
imageView.contentMode = .scaleAspectFill
wrappedView.addSubview(imageView)
//wrappedViewに対してimageViewに張るConstraint(横方向 → 左:0, 右:0)
let imageViewConstarintH = NSLayoutConstraint.constraints(
withVisualFormat: "H:|[imageView]|",
options: NSLayoutFormatOptions(rawValue: 0),
metrics: nil,
views: ["imageView" : imageView]
)
//wrappedViewの下から0pxの位置に配置する
imageViewBottomLayoutConstraint = NSLayoutConstraint(
item: imageView,
attribute: .bottom,
relatedBy: .equal,
toItem: wrappedView,
attribute: .bottom,
multiplier: 1.0,
constant: 0.0
)
//imageViewの縦幅をいっぱいにする
imageViewHeightLayoutConstraint = NSLayoutConstraint(
item: imageView,
attribute: .height,
relatedBy: .equal,
toItem: wrappedView,
attribute: .height,
multiplier: 1.0,
constant: 0.0
)
wrappedView.addConstraints(imageViewConstarintH)
wrappedView.addConstraint(imageViewBottomLayoutConstraint)
wrappedView.addConstraint(imageViewHeightLayoutConstraint)
}
}
import Foundation
import UIKit
import SwiftyJSON
//ArticleViewController側で実行したい処理をプロトコルに定義しておく
protocol ArticlePresenterProtocol: class {
func showArticle(_ article: Article)
func hideArticle()
}
class ArticlePresenter {
var presenter: ArticlePresenterProtocol!
//MARK: - Initializer
init(presenter: ArticlePresenterProtocol) {
self.presenter = presenter
}
//MARK: - Functions
//サンプル記事データを取得する
func getArticle() {
let apiRequestManager = APIRequestManager(endPoint: "article.json", method: .get)
apiRequestManager.request(
success: { (data: Dictionary) in
let article = Article.init(json: JSON(data))
//通信成功時の処理をプロトコルを適用したViewController側で行う
self.presenter.showArticle(article)
},
fail: { (error: Error?) in
//通信失敗時の処理をプロトコルを適用したViewController側で行う
self.presenter.hideArticle()
}
)
}
}
import UIKit
class ArticleStoryTableViewCell: UITableViewCell {
//UI部品の配置
@IBOutlet weak private var articleStoryImageWrappedView: UIView!
@IBOutlet weak private var articleStoryButtonWrappedView: UIView!
@IBOutlet weak private var articleStoryButton: UIButton!
//ViewControllerへ処理内容を引き渡すためのクロージャー
var showStoryAction: (() -> ())?
//MARK: - Initializer
override func awakeFromNib() {
super.awakeFromNib()
setupArticleStoryTableViewCell()
}
//MARK: - Private Function
//入力ボタンのTouchDownのタイミングで実行される処理
@objc private func onTouchDownArticleStoryButton(sender: UIButton) {
UIView.animate(withDuration: 0.16, animations: {
self.articleStoryButtonWrappedView.transform = CGAffineTransform(scaleX: 0.94, y: 0.94)
}, completion: nil)
}
//入力ボタンのTouchUpInsideのタイミングで実行される処理
@objc private func onTouchUpInsideArticleStoryButton(sender: UIButton) {
UIView.animate(withDuration: 0.16, animations: {
self.articleStoryButtonWrappedView.transform = CGAffineTransform(scaleX: 1, y: 1)
}, completion: { finished in
//ViewController側でクロージャー内に設定した処理を実行する
self.showStoryAction?()
})
}
//入力ボタンのTouchUpOutsideのタイミングで実行される処理
@objc private func onTouchUpOutsideArticleStoryButton(sender: UIButton) {
UIView.animate(withDuration: 0.16, animations: {
self.articleStoryButtonWrappedView.transform = CGAffineTransform(scaleX: 1, y: 1)
}, completion: nil)
}
private func setupArticleStoryTableViewCell() {
//セルの装飾設定をする
self.accessoryType = .none
self.selectionStyle = .none
//写真付きサムネイル枠の装飾設定
articleStoryImageWrappedView.layer.masksToBounds = false
articleStoryImageWrappedView.layer.cornerRadius = 5.0
articleStoryImageWrappedView.layer.borderColor = UIColor.init(code: "dddddd").cgColor
articleStoryImageWrappedView.layer.borderWidth = 1
articleStoryImageWrappedView.layer.shadowRadius = 2.0
articleStoryImageWrappedView.layer.shadowOpacity = 0.15
articleStoryImageWrappedView.layer.shadowOffset = CGSize(width: 0, height: 1)
articleStoryImageWrappedView.layer.shadowColor = UIColor.black.cgColor
//ボタンの丸みをつける
articleStoryButtonWrappedView.layer.cornerRadius = 5.0
articleStoryButtonWrappedView.layer.masksToBounds = true
//ボタンアクションに関する設定
//TouchDown・TouchUpInside・TouchUpOutsideの時のイベントを設定する(完了時の具体的な処理はTouchUpInside側で設定すること)
articleStoryButton.addTarget(self, action: #selector(self.onTouchDownArticleStoryButton(sender:)), for: .touchDown)
articleStoryButton.addTarget(self, action: #selector(self.onTouchUpInsideArticleStoryButton(sender:)), for: .touchUpInside)
articleStoryButton.addTarget(self, action: #selector(self.onTouchUpOutsideArticleStoryButton(sender:)), for: .touchUpOutside)
}
}
/* 修正前のSafeAreaを考慮した判定 */
//記事上の画像ヘッダーのViewの高さ(iPhoneX用に補正あり)
private let articleHeaderImageViewHeight: CGFloat = DeviceSize.sizeOfIphoneX() ? 244 : 200
//グラデーションヘッダー用のY軸方向の位置(iPhoneX用に補正あり)
private let gradientHeaderViewPositionY: CGFloat = DeviceSize.sizeOfIphoneX() ? -44 : -20
//ナビゲーションバーの高さ(iPhoneX用に補正あり)
fileprivate let navigationBarHeight: CGFloat = DeviceSize.sizeOfIphoneX() ? 88.5 : 64.0
/* 修正後のSafeAreaを考慮した判定 */
//記事上の画像ヘッダーのViewの高さ
private let articleHeaderImageViewHeight: CGFloat = {
if UIApplication.shared.statusBarFrame.height > 20 {
return 244.0
} else {
return 200.0
}
}()
//グラデーションヘッダー用のY軸方向の位置
private let gradientHeaderViewPositionY: CGFloat = -UIApplication.shared.statusBarFrame.height
//ナビゲーションバーの高さ
fileprivate let navigationBarHeight: CGFloat = {
if UIApplication.shared.statusBarFrame.height > 20 {
return 88.5
} else {
return 64.0
}
}()
import Foundation
import UIKit
//自作のXibを使用するための基底となるUIViewを継承したクラス
//参考:http://skygrid.co.jp/jojakudoctor/swift-custom-class/
class CustomViewBase: UIView {
//コンテンツ表示用のView
weak var contentView: UIView!
//このカスタムビューをコードで使用する際の初期化処理
required override init(frame: CGRect) {
super.init(frame: frame)
initContentView()
}
//このカスタムビューをInterfaceBuilderで使用する際の初期化処理
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initContentView()
}
//コンテンツ表示用Viewの初期化処理
private func initContentView() {
//追加するcontentViewのクラス名を取得する
let viewClass: AnyClass = type(of: self)
//追加するcontentViewに関する設定をする
contentView = Bundle(for: viewClass)
.loadNibNamed(String(describing: viewClass), owner: self, options: nil)?.first as? UIView
contentView.autoresizingMask = autoresizingMask
contentView.frame = bounds
contentView.translatesAutoresizingMaskIntoConstraints = false
addSubview(contentView)
//追加するcontentViewの制約を設定する ※上下左右へ0の制約を追加する
let bindings = ["view": contentView as Any]
let contentViewConstraintH = NSLayoutConstraint.constraints(
withVisualFormat: "H:|[view]|",
options: NSLayoutFormatOptions(rawValue: 0),
metrics: nil,
views: bindings
)
let contentViewConstraintV = NSLayoutConstraint.constraints(
withVisualFormat: "V:|[view]|",
options: NSLayoutFormatOptions(rawValue: 0),
metrics: nil,
views: bindings
)
addConstraints(contentViewConstraintH)
addConstraints(contentViewConstraintV)
}
}
/**
* MEMO: Xcode10以降でビルドする際はこのファイルは必要ないので、
* 「補足: Xcode10でビルドする場合におけるこのリポジトリでのSafeArea関連部分の調整」
* を参考に実装を行なってください。
*/
import Foundation
struct DeviceSize {
//CGRectを取得
static func bounds() -> CGRect {
return UIScreen.main.bounds
}
//画面の横サイズを取得
static func screenWidth() -> Int {
return Int(self.bounds().width)
}
//画面の縦サイズを取得
static func screenHeight() -> Int {
return Int(self.bounds().height)
}
//iPhoneXのサイズとマッチしているかを返す
static func sizeOfIphoneX() -> Bool {
return (self.screenWidth() == 375 && self.screenHeight() == 812)
}
}
# Uncomment the next line to define a global platform for your project
platform :ios, '9.0'
target 'InteractiveUISample' do
# Comment the next line if you're not using Swift and don't want to use dynamic frameworks
use_frameworks!
# Pods for InteractiveUISample
# Utility
pod 'Alamofire'
pod 'SwiftyJSON'
pod 'SDWebImage'
# UserInterface
pod 'FontAwesome.swift', :git => 'https://github.com/thii/FontAwesome.swift', :branch => 'swift-4.0'
end
/* 修正前のSafeAreaを考慮した判定 */
private let defaultHeaderMargin: CGFloat = DeviceSize.sizeOfIphoneX() ? 44 : 20
/* 修正後のSafeAreaを考慮した判定 */
private let defaultHeaderMargin: CGFloat = UIApplication.shared.statusBarFrame.height
import UIKit
import FontAwesome_swift
class MainListTableViewCell: UITableViewCell {
・・・(省略)・・・
//UIViewに内包したUIImageViewの上下の制約
@IBOutlet weak var topImageViewConstraint: NSLayoutConstraint!
@IBOutlet weak var bottomImageViewConstraint: NSLayoutConstraint!
//視差効果のズレを生むための定数(大きいほど視差効果が大きい)
private let imageParallaxFactor: CGFloat = 75
・・・(省略)・・・
//視差効果の計算用の変数
private var imageBackTopInitial: CGFloat!
private var imageBackBottomInitial: CGFloat!
・・・(省略)・・・
//画像にかけられているAutoLayoutの制約を再計算して制約をかけ直す
func setBackgroundOffset(_ offset: CGFloat) {
let boundOffset = max(0, min(1, offset))
let pixelOffset = (1 - boundOffset) * 2 * imageParallaxFactor
topImageViewConstraint.constant = imageBackTopInitial - pixelOffset
bottomImageViewConstraint.constant = imageBackBottomInitial + pixelOffset
}
・・・(省略)・・・
}
//MARK: - UITableViewDelegate, UIScrollViewDelegate
extension MainListViewController: UITableViewDelegate, UIScrollViewDelegate {
//MARK: - UITableViewDelegate
//セルを表示しようとする時の動作を設定する
/**
* willDisplay(UITableViewDelegateのメソッド)に関して
*
* 参考: Cocoa API解説(macOS/iOS) tableView:willDisplayCell:forRowAtIndexPath:
* https://goo.gl/Ykp30Q
*/
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
//MainListTableViewCell型へダウンキャストする
let mainListTableViewCell = cell as! MainListTableViewCell
//セル内の画像のオフセット値を変更する
setCellImageOffset(mainListTableViewCell, indexPath: indexPath)
//セルへフェードインのCoreAnimationを適用する
setCellFadeInAnimation(mainListTableViewCell)
}
//MARK: - UIScrollViewDelegate
//スクロールが検知された時に実行される処理
func scrollViewDidScroll(_ scrollView: UIScrollView) {
//パララックスをするテーブルビューの場合
if scrollView == mainListTableView {
for indexPath in mainListTableView.indexPathsForVisibleRows! {
//画面に表示されているセルの画像のオフセット値を変更する
setCellImageOffset(mainListTableView.cellForRow(at: indexPath) as! MainListTableViewCell, indexPath: indexPath)
}
}
}
//UITableViewCell内のオフセット値を再計算して視差効果をつける
private func setCellImageOffset(_ cell: MainListTableViewCell, indexPath: IndexPath) {
//MainListTableViewCellの位置関係から動かす制約の値を決定する
let cellFrame = mainListTableView.rectForRow(at: indexPath)
let cellFrameInTable = mainListTableView.convert(cellFrame, to: mainListTableView.superview)
let cellOffset = cellFrameInTable.origin.y + cellFrameInTable.size.height
let tableHeight = mainListTableView.bounds.size.height + cellFrameInTable.size.height
let cellOffsetFactor = cellOffset / tableHeight
//画面に表示されているセルの画像のオフセット値を変更する
cell.setBackgroundOffset(cellOffsetFactor)
}
//UITableViewCellが表示されるタイミングにフェードインのアニメーションをつける
private func setCellFadeInAnimation(_ cell: MainListTableViewCell) {
/**
* CoreAnimationを利用したアニメーションをセルの表示時に付与する(拡大とアルファの重ねがけ)
*
* 参考:【iOS Swift入門 #185】Core Animationでアニメーションの加速・減速をする
* http://swift-studying.com/blog/swift/?p=1162
*/
//アニメーションの作成
let groupAnimation = CAAnimationGroup()
groupAnimation.fillMode = kCAFillModeBackwards
groupAnimation.duration = 0.36
groupAnimation.beginTime = CACurrentMediaTime() + 0.08
groupAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
//透過を変更するアニメーション
let opacityAnimation = CABasicAnimation(keyPath: "opacity")
opacityAnimation.fromValue = 0.00
opacityAnimation.toValue = 1.00
//作成した個別のアニメーションをグループ化
groupAnimation.animations = [opacityAnimation]
//セルのLayerにアニメーションを追加
cell.layer.add(groupAnimation, forKey: nil)
//アニメーション終了後は元のサイズになるようにする
cell.layer.transform = CATransform3DIdentity
}
}
import UIKit
class MainViewController: UIViewController {
@IBOutlet weak var navigationScrollView: UIScrollView!
@IBOutlet weak var contentsScrollView: UIScrollView!
//ナビゲーション用のScrollViewの中に入れる動く下線用のView
fileprivate var bottomLineView: UIView = UIView()
//ナビゲーション用のScrollViewの中に入れる動く下線用のViewの高さ
fileprivate let navigationBottomLinePositionHeight: Int = 2
//ナビゲーションのボタン名
private let navigationNameList: [String] = ["新着特集", "コンテンツ紹介"]
//UIScrollView内のレイアウト決定に関する処理 ※この中でviewDidLayoutSubviewsで行うUI部品の初期配置に関する処理を行う
private lazy var setNavigationScrollView: (() -> ())? = {
setupButtonsInNavigationScrollView()
setupBottomLineInNavigationScrollView()
return nil
}()
//スクロールビューの識別用タグ定義
private enum ScrollViewIdentifier: Int {
case navigation = 0
case contents
}
override func viewDidLoad() {
super.viewDidLoad()
setupNavigationBar()
setupContentsScrollView()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
//ナビゲーション用のスクロールビューに関する設定をする
setNavigationScrollView?()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
//MARK: - Private Function
//ナビゲーションに配置されたボタンを押した時のアクション設定
@objc private func navigationScrollViewButtonTapped(button: UIButton) {
//押されたボタンのタグを取得
let page: Int = button.tag
//ナビゲーション用のボタンが押された場合は
animateBottomLineView(Double(page), actionIdentifier: .navigationButtonTapped)
animateContentScrollView(page)
}
//この画面のナビゲーションバーの設定
private func setupNavigationBar() {
//NavigationControllerのデザイン調整を行う
self.navigationController?.navigationBar.tintColor = UIColor.white
self.navigationController?.navigationBar.titleTextAttributes = [NSAttributedStringKey.foregroundColor : UIColor.white]
//タイトルを入れる
self.navigationItem.title = "海の見える風景"
}
//コンテンツ表示用のUIScrollViewの設定
private func setupContentsScrollView() {
contentsScrollView.delegate = self
contentsScrollView.isPagingEnabled = true
contentsScrollView.showsHorizontalScrollIndicator = false
}
//ボタン表示用のUIScrollViewの設定
//MEMO: private lazy var setNavigationScrollView: (() -> ())? 内に設定
private func setupButtonsInNavigationScrollView() {
//スクロールビュー内のサイズを決定する
let navigationScrollViewWidth = Int(navigationScrollView.frame.width)
let navigationScrollViewHeight = Int(navigationScrollView.frame.height)
navigationScrollView.contentSize = CGSize(width: navigationScrollViewWidth, height: navigationScrollViewHeight)
//スクロールビュー内にUIButtonを配置する
for i in 0..<navigationNameList.count {
let button = UIButton(
frame: CGRect(
x: CGFloat(navigationScrollViewWidth / 2 * i),
y: CGFloat(0),
width: CGFloat(navigationScrollViewWidth / 2),
height: CGFloat(navigationScrollViewHeight)
)
)
button.backgroundColor = UIColor.clear
button.titleLabel!.font = UIFont(name: AppConstants.BOLD_FONT_NAME, size: 14)!
button.setTitle(navigationNameList[i], for: UIControlState())
button.setTitleColor(ColorDefinition.navigationColor.getColor(), for: UIControlState())
button.tag = i
button.addTarget(self, action: #selector(self.navigationScrollViewButtonTapped(button:)), for: .touchUpInside)
navigationScrollView.addSubview(button)
}
}
//ナビゲーション用のScrollViewの中に入れる動く下線の設定
//MEMO: private lazy var setNavigationScrollView: (() -> ())? 内に設定
private func setupBottomLineInNavigationScrollView() {
let navigationScrollViewWidth = Int(navigationScrollView.frame.width)
let navigationScrollViewHeight = Int(navigationScrollView.frame.height)
bottomLineView.frame = CGRect(
x: CGFloat(0),
y: CGFloat(navigationScrollViewHeight - navigationBottomLinePositionHeight),
width: CGFloat(navigationScrollViewWidth / 2),
height: CGFloat(navigationBottomLinePositionHeight)
)
bottomLineView.backgroundColor = ColorDefinition.navigationColor.getColor()
navigationScrollView.addSubview(bottomLineView)
navigationScrollView.bringSubview(toFront: bottomLineView)
}
}
//MARK: - UIScrollViewDelegate
extension MainViewController: UIScrollViewDelegate {
//animateBottomLineViewを実行する際に行われたアクションを識別するためのenum値
fileprivate enum ActionIdentifier {
case contentsSlide
case navigationButtonTapped
//対応するアニメーションの秒数を返す
func duration() -> Double {
switch self {
case .contentsSlide:
return 0
case .navigationButtonTapped:
return 0.26
}
}
}
//スクロールが発生した際に行われる処理 (※ UIScrollViewDelegate)
func scrollViewDidScroll(_ scrollview: UIScrollView) {
//現在表示されているページ番号を判別する
let pageWidth = contentsScrollView.frame.width
let fractionalPage = Double(contentsScrollView.contentOffset.x / pageWidth)
//ボタン配置用のスクロールビューもスライドさせる
animateBottomLineView(fractionalPage, actionIdentifier: .contentsSlide)
}
//ナビゲーション用のScrollViewの中に入れる動く下線を所定位置まで動かす
fileprivate func animateBottomLineView(_ page: Double, actionIdentifier: ActionIdentifier) {
let navigationScrollViewWidth = Int(navigationScrollView.frame.width)
let navigationScrollViewHeight = Int(navigationScrollView.frame.height)
//X軸方向の動かす終点位置を決める
let positionX = Double(navigationScrollViewWidth / 2) * page
UIView.animate(withDuration: actionIdentifier.duration(), animations: {
self.bottomLineView.frame = CGRect(
x: CGFloat(positionX),
y: CGFloat(navigationScrollViewHeight - self.navigationBottomLinePositionHeight),
width: CGFloat(navigationScrollViewWidth / 2),
height: CGFloat(self.navigationBottomLinePositionHeight)
)
})
}
//コンテンツ用のScrollViewを所定位置まで動かす
fileprivate func animateContentScrollView(_ page: Int) {
UIView.animate(withDuration: 0.26, animations: {
self.contentsScrollView.contentOffset = CGPoint(
x: Int(self.contentsScrollView.frame.width) * page,
y: 0
)
})
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment