Last active
November 21, 2018 08:04
-
-
Save fumiyasac/3934ba8f44a8250d089ec5b1e945c79d to your computer and use it in GitHub Desktop.
できるだけUI系のライブラリを用いないアニメーションを盛り込んだサンプル実装まとめ(前編) ref: https://qiita.com/fumiyasac@github/items/d1b56ffc6d7d46c0a616
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ?? "" | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
}) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
} | |
) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 修正前の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 | |
} | |
}() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 修正前のSafeAreaを考慮した判定 */ | |
private let defaultHeaderMargin: CGFloat = DeviceSize.sizeOfIphoneX() ? 44 : 20 | |
/* 修正後のSafeAreaを考慮した判定 */ | |
private let defaultHeaderMargin: CGFloat = UIApplication.shared.statusBarFrame.height |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
・・・(省略)・・・ | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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 | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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