├── .DS_Store
├── ScrollAnimationShowcase
├── Assets.xcassets
│ ├── Contents.json
│ ├── sample.imageset
│ │ ├── sample.jpg
│ │ └── Contents.json
│ ├── thumb1.imageset
│ │ ├── thumb1.jpg
│ │ └── Contents.json
│ ├── thumb2.imageset
│ │ ├── thumb2.jpg
│ │ └── Contents.json
│ ├── thumb3.imageset
│ │ ├── thumb3.jpg
│ │ └── Contents.json
│ ├── ui_recipt_book.imageset
│ │ ├── ui_recipt_book.jpg
│ │ └── Contents.json
│ └── AppIcon.appiconset
│ │ └── Contents.json
├── Extension
│ ├── NSObjectProtocolExtension.swift
│ ├── IntExtension.swift
│ ├── UIColorExtension.swift
│ ├── UIViewControllerExtension.swift
│ └── UICollectionViewExtension.swift
├── Modules
│ └── AppConstant.swift
├── Entity
│ └── ArticleEntity.swift
├── Info.plist
├── Storyboard
│ └── Base.lproj
│ │ ├── LaunchScreen.storyboard
│ │ ├── Article.storyboard
│ │ └── Contents.storyboard
├── TestData
│ └── Mock
│ │ └── ArticleMock.swift
├── View
│ ├── CategoryScrollTabViewCell
│ │ ├── CategoryScrollTabViewCell.swift
│ │ └── CategoryScrollTabViewCell.xib
│ ├── CustomViewBase.swift
│ ├── CategoryScrollContentsViewCell
│ │ ├── CategoryScrollContentsViewCell.swift
│ │ └── CategoryScrollContentsViewCell.xib
│ ├── CategoryScrollTabViewFlowLayout
│ │ └── CategoryScrollTabViewFlowLayout.swift
│ └── ContentsDetailHeaderView
│ │ └── ContentsDetailHeaderView.swift
├── ViewController
│ ├── Contents
│ │ └── ContentsViewController.swift
│ └── Article
│ │ ├── ScrollContents
│ │ └── CategoryScrollContentsViewController.swift
│ │ ├── ArticleViewController.swift
│ │ └── ScrollTab
│ │ └── CategoryScrollTabViewController.swift
└── AppDelegate.swift
├── ScrollAnimationShowcase.xcodeproj
├── xcuserdata
│ └── sakaifumiya.xcuserdatad
│ │ ├── xcdebugger
│ │ └── Breakpoints_v2.xcbkptlist
│ │ └── xcschemes
│ │ └── xcschememanagement.plist
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ ├── xcuserdata
│ │ └── sakaifumiya.xcuserdatad
│ │ │ └── UserInterfaceState.xcuserstate
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
└── project.pbxproj
├── ScrollAnimationShowcaseTests
├── Info.plist
└── ScrollAnimationShowcaseTests.swift
├── ScrollAnimationShowcaseUITests
├── Info.plist
└── ScrollAnimationShowcaseUITests.swift
└── README.md
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fumiyasac/ScrollAnimationShowcase/HEAD/.DS_Store
--------------------------------------------------------------------------------
/ScrollAnimationShowcase/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/ScrollAnimationShowcase/Assets.xcassets/sample.imageset/sample.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fumiyasac/ScrollAnimationShowcase/HEAD/ScrollAnimationShowcase/Assets.xcassets/sample.imageset/sample.jpg
--------------------------------------------------------------------------------
/ScrollAnimationShowcase/Assets.xcassets/thumb1.imageset/thumb1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fumiyasac/ScrollAnimationShowcase/HEAD/ScrollAnimationShowcase/Assets.xcassets/thumb1.imageset/thumb1.jpg
--------------------------------------------------------------------------------
/ScrollAnimationShowcase/Assets.xcassets/thumb2.imageset/thumb2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fumiyasac/ScrollAnimationShowcase/HEAD/ScrollAnimationShowcase/Assets.xcassets/thumb2.imageset/thumb2.jpg
--------------------------------------------------------------------------------
/ScrollAnimationShowcase/Assets.xcassets/thumb3.imageset/thumb3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fumiyasac/ScrollAnimationShowcase/HEAD/ScrollAnimationShowcase/Assets.xcassets/thumb3.imageset/thumb3.jpg
--------------------------------------------------------------------------------
/ScrollAnimationShowcase.xcodeproj/xcuserdata/sakaifumiya.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/ScrollAnimationShowcase/Assets.xcassets/ui_recipt_book.imageset/ui_recipt_book.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fumiyasac/ScrollAnimationShowcase/HEAD/ScrollAnimationShowcase/Assets.xcassets/ui_recipt_book.imageset/ui_recipt_book.jpg
--------------------------------------------------------------------------------
/ScrollAnimationShowcase.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/ScrollAnimationShowcase.xcodeproj/project.xcworkspace/xcuserdata/sakaifumiya.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fumiyasac/ScrollAnimationShowcase/HEAD/ScrollAnimationShowcase.xcodeproj/project.xcworkspace/xcuserdata/sakaifumiya.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/ScrollAnimationShowcase.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ScrollAnimationShowcase/Assets.xcassets/sample.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "sample.jpg",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/ScrollAnimationShowcase/Assets.xcassets/thumb1.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "thumb1.jpg",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/ScrollAnimationShowcase/Assets.xcassets/thumb2.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "thumb2.jpg",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/ScrollAnimationShowcase/Assets.xcassets/thumb3.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "thumb3.jpg",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/ScrollAnimationShowcase/Assets.xcassets/ui_recipt_book.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "ui_recipt_book.jpg",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/ScrollAnimationShowcase/Extension/NSObjectProtocolExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSObjectProtocolExtension.swift
3 | // ScrollAnimationShowcase
4 | //
5 | // Created by 酒井文也 on 2018/11/10.
6 | // Copyright © 2018 酒井文也. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | // NSObjectProtocolの拡張
13 | extension NSObjectProtocol {
14 |
15 | // クラス名を返す変数"className"を返す
16 | static var className: String {
17 | return String(describing: self)
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/ScrollAnimationShowcase.xcodeproj/xcuserdata/sakaifumiya.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | ScrollAnimationShowcase.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/ScrollAnimationShowcase/Modules/AppConstant.swift:
--------------------------------------------------------------------------------
1 |
2 | //
3 | // AppConstant.swift
4 | // ScrollAnimationShowcase
5 | //
6 | // Created by 酒井文也 on 2018/11/10.
7 | // Copyright © 2018 酒井文也. All rights reserved.
8 | //
9 |
10 | import Foundation
11 | import UIKit
12 |
13 | struct AppConstant {
14 | // カテゴリー表示のセルに関する程数値
15 | static let CATEGORY_CELL_WIDTH: CGFloat = 150
16 | static let CATEGORY_CELL_HEIGHT: CGFloat = 48
17 | static let CATEGORY_FONT_NAME: String = "HiraMaruProN-W4"
18 | static let CATEGORY_FONT_SIZE: CGFloat = 14.0
19 | static let CATEGORY_FONT_HEIGHT: CGFloat = 17.0
20 | }
21 |
--------------------------------------------------------------------------------
/ScrollAnimationShowcase/Extension/IntExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IntExtension.swift
3 | // ScrollAnimationShowcase
4 | //
5 | // Created by 酒井文也 on 2018/11/25.
6 | // Copyright © 2018 酒井文也. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | extension Int {
13 |
14 | // 決まった範囲内(負数値を含む)での乱数値を作るメソッド
15 | // 参考:コーディングしてる時「こういうのあったら書きやすいな」的な Extension まとめ(主に俺得)
16 | //https://qiita.com/lovee/items/67db977a1afc80b3148d
17 | static func createRandom(range: Range) -> Int {
18 | let rangeLength = range.upperBound - range.lowerBound
19 | let random = arc4random_uniform(UInt32(rangeLength))
20 | return Int(random) + range.lowerBound
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/ScrollAnimationShowcaseTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/ScrollAnimationShowcaseUITests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/ScrollAnimationShowcase/Entity/ArticleEntity.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ArticleEntity.swift
3 | // ScrollAnimationShowcase
4 | //
5 | // Created by 酒井文也 on 2018/11/25.
6 | // Copyright © 2018 酒井文也. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | struct ArticleEntity {
13 |
14 | // メンバ変数
15 | let id: Int
16 | let title: String
17 | let catchcopy: String
18 | let category: String
19 | let imageFile: UIImage?
20 | let dateString: String
21 |
22 | // イニシャライザ
23 | init(id: Int, title: String, catchcopy: String, category: String, imageFileName: String, dateString: String) {
24 | self.id = id
25 | self.title = title
26 | self.catchcopy = catchcopy
27 | self.category = category
28 | self.imageFile = UIImage(named: imageFileName)
29 | self.dateString = dateString
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/ScrollAnimationShowcase/Extension/UIColorExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIColorExtension.swift
3 | // ScrollAnimationShowcase
4 | //
5 | // Created by 酒井文也 on 2018/11/10.
6 | // Copyright © 2018 酒井文也. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | // UIColorの拡張
13 | extension UIColor {
14 |
15 | // 16進数のカラーコードをiOSの設定に変換するメソッド
16 | // 参考:【Swift】Tips: あると便利だったextension達(UIColor編)
17 | // https://dev.classmethod.jp/smartphone/utilty-extension-uicolor/
18 | convenience init(code: String, alpha: CGFloat = 1.0) {
19 | var color: UInt32 = 0
20 | var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0
21 | if Scanner(string: code.replacingOccurrences(of: "#", with: "")).scanHexInt32(&color) {
22 | r = CGFloat((color & 0xFF0000) >> 16) / 255.0
23 | g = CGFloat((color & 0x00FF00) >> 8) / 255.0
24 | b = CGFloat( color & 0x0000FF ) / 255.0
25 | }
26 | self.init(red: r, green: g, blue: b, alpha: alpha)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/ScrollAnimationShowcaseTests/ScrollAnimationShowcaseTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScrollAnimationShowcaseTests.swift
3 | // ScrollAnimationShowcaseTests
4 | //
5 | // Created by 酒井文也 on 2018/11/09.
6 | // Copyright © 2018 酒井文也. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import ScrollAnimationShowcase
11 |
12 | class ScrollAnimationShowcaseTests: XCTestCase {
13 |
14 | override func setUp() {
15 | // Put setup code here. This method is called before the invocation of each test method in the class.
16 | }
17 |
18 | override func tearDown() {
19 | // Put teardown code here. This method is called after the invocation of each test method in the class.
20 | }
21 |
22 | func testExample() {
23 | // This is an example of a functional test case.
24 | // Use XCTAssert and related functions to verify your tests produce the correct results.
25 | }
26 |
27 | func testPerformanceExample() {
28 | // This is an example of a performance test case.
29 | self.measure {
30 | // Put the code you want to measure the time of here.
31 | }
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/ScrollAnimationShowcaseUITests/ScrollAnimationShowcaseUITests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScrollAnimationShowcaseUITests.swift
3 | // ScrollAnimationShowcaseUITests
4 | //
5 | // Created by 酒井文也 on 2018/11/09.
6 | // Copyright © 2018 酒井文也. All rights reserved.
7 | //
8 |
9 | import XCTest
10 |
11 | class ScrollAnimationShowcaseUITests: XCTestCase {
12 |
13 | override func setUp() {
14 | // Put setup code here. This method is called before the invocation of each test method in the class.
15 |
16 | // In UI tests it is usually best to stop immediately when a failure occurs.
17 | continueAfterFailure = false
18 |
19 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method.
20 | XCUIApplication().launch()
21 |
22 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
23 | }
24 |
25 | override func tearDown() {
26 | // Put teardown code here. This method is called after the invocation of each test method in the class.
27 | }
28 |
29 | func testExample() {
30 | // Use recording to get started writing UI tests.
31 | // Use XCTAssert and related functions to verify your tests produce the correct results.
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/ScrollAnimationShowcase/Extension/UIViewControllerExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIViewControllerExtension.swift
3 | // ScrollAnimationShowcase
4 | //
5 | // Created by 酒井文也 on 2018/11/09.
6 | // Copyright © 2018 酒井文也. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | // UIViewControllerの拡張
13 | extension UIViewController {
14 |
15 | // この画面のナビゲーションバーを設定するメソッド
16 | public func setupNavigationBarTitle(_ title: String) {
17 |
18 | // NavigationControllerのデザイン調整を行う
19 | var attributes = [NSAttributedString.Key : Any]()
20 | attributes[NSAttributedString.Key.font] = UIFont(name: "HiraKakuProN-W6", size: 14.0)
21 | attributes[NSAttributedString.Key.foregroundColor] = UIColor.white
22 |
23 | self.navigationController!.navigationBar.titleTextAttributes = attributes
24 |
25 | // タイトルを入れる
26 | self.navigationItem.title = title
27 | }
28 |
29 | // 戻るボタンの「戻る」テキストを削除した状態にするメソッド
30 | public func removeBackButtonText() {
31 | self.navigationController!.navigationBar.tintColor = UIColor.white
32 | if #available(iOS 14.0, *) {
33 | self.navigationItem.backButtonDisplayMode = .minimal
34 | self.navigationItem.backButtonTitle = self.navigationItem.title
35 | } else {
36 | // 戻るボタンの文言を消す
37 | let backButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
38 | self.navigationItem.backBarButtonItem = backButtonItem
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/ScrollAnimationShowcase/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UILaunchStoryboardName
24 | LaunchScreen
25 | UIMainStoryboardFile
26 | Article
27 | UIRequiredDeviceCapabilities
28 |
29 | armv7
30 |
31 | UISupportedInterfaceOrientations
32 |
33 | UIInterfaceOrientationPortrait
34 |
35 | UISupportedInterfaceOrientations~ipad
36 |
37 | UIInterfaceOrientationPortrait
38 | UIInterfaceOrientationPortraitUpsideDown
39 | UIInterfaceOrientationLandscapeLeft
40 | UIInterfaceOrientationLandscapeRight
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/ScrollAnimationShowcase/Storyboard/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/ScrollAnimationShowcase/TestData/Mock/ArticleMock.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ArticleCategoryMock.swift
3 | // ScrollAnimationShowcase
4 | //
5 | // Created by 酒井文也 on 2018/11/13.
6 | // Copyright © 2018 酒井文也. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | struct ArticleMock {
13 |
14 | private static let titleSamples: [String] = [
15 | "安いけど美味しい自炊",
16 | "まずは収入と支出から",
17 | "前に攻める貯金ライフ",
18 | ]
19 |
20 | private static let catchcopySamples: [String] = [
21 | "食生活から無駄を排除!",
22 | "家計簿は生活の基準値!",
23 | "安心と今後の為の貯金!",
24 | ]
25 |
26 | private static let dateStringSamples: [String] = [
27 | "2018.10.08",
28 | "2018.11.25",
29 | "2018.08.04",
30 | ]
31 |
32 | private static let categories: [String] = [
33 | "簡単節約TIPS",
34 | "暮らしのマネー",
35 | "経済ニュース",
36 | "お買い物情報",
37 | "時短レシピ紹介",
38 | "その他の情報",
39 | ]
40 |
41 | // 記事カテゴリーデータを表示する
42 | static func getArticleCategories() -> [String] {
43 | return categories
44 | }
45 |
46 | // 18個分のサンプルデータを作成する(引数にカテゴリーIDを渡す)
47 | static func getArticlesBy(categoryId: Int) -> [ArticleEntity] {
48 |
49 | var articles: [ArticleEntity] = []
50 |
51 | for i in 1...18 {
52 | let randomIndex = Int.createRandom(range: Range(0...2))
53 | articles.append(
54 | ArticleEntity.init(
55 | id: i,
56 | title: titleSamples[randomIndex],
57 | catchcopy: catchcopySamples[randomIndex],
58 | category: categories[categoryId],
59 | imageFileName: "thumb\(randomIndex + 1)",
60 | dateString: dateStringSamples[randomIndex]
61 | )
62 | )
63 | }
64 | return articles
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/ScrollAnimationShowcase/View/CategoryScrollTabViewCell/CategoryScrollTabViewCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CategoryScrollTabViewCell.swift
3 | // ScrollAnimationShowcase
4 | //
5 | // Created by 酒井文也 on 2018/11/10.
6 | // Copyright © 2018 酒井文也. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | final class CategoryScrollTabViewCell: UICollectionViewCell {
12 |
13 | // カテゴリー選択用セルのサイズ
14 | static let cellSize: CGSize = CGSize(
15 | width: AppConstant.CATEGORY_CELL_WIDTH,
16 | height: AppConstant.CATEGORY_CELL_HEIGHT
17 | )
18 |
19 | @IBOutlet weak private var categoryTitleLabel: UILabel!
20 |
21 | // MARK: - Class Function
22 |
23 | // カテゴリー表示用の下線の幅を算出する
24 | class func calculateCategoryUnderBarWidthBy(title: String) -> CGFloat {
25 |
26 | // テキストの属性を設定する
27 | var categoryTitleAttributes = [NSAttributedString.Key : Any]()
28 | categoryTitleAttributes[NSAttributedString.Key.font] = UIFont(
29 | name: AppConstant.CATEGORY_FONT_NAME,
30 | size: AppConstant.CATEGORY_FONT_SIZE
31 | )
32 |
33 | // 引数で渡された文字列とフォントから配置するラベルの幅を取得する
34 | let categoryTitleLabelSize = CGSize(
35 | width: .greatestFiniteMagnitude,
36 | height: AppConstant.CATEGORY_FONT_HEIGHT
37 | )
38 | let categoryTitleLabelRect = title.boundingRect(
39 | with: categoryTitleLabelSize,
40 | options: .usesLineFragmentOrigin,
41 | attributes: categoryTitleAttributes,
42 | context: nil)
43 |
44 | return ceil(categoryTitleLabelRect.width)
45 | }
46 |
47 | // MARK: - Function
48 |
49 | // タブ表示用のセルに表示する内容を設定する
50 | func setCategory(name: String, isSelected: Bool = false) {
51 | categoryTitleLabel.text = name
52 | categoryTitleLabel.textColor = isSelected ? UIColor.init(code: "#ff6060") : UIColor.gray
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/ScrollAnimationShowcase/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "20x20",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "20x20",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "29x29",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "29x29",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "40x40",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "40x40",
31 | "scale" : "3x"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "size" : "60x60",
36 | "scale" : "2x"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "size" : "60x60",
41 | "scale" : "3x"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "size" : "20x20",
46 | "scale" : "1x"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "size" : "20x20",
51 | "scale" : "2x"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "size" : "29x29",
56 | "scale" : "1x"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "size" : "29x29",
61 | "scale" : "2x"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "size" : "40x40",
66 | "scale" : "1x"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "size" : "40x40",
71 | "scale" : "2x"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "size" : "76x76",
76 | "scale" : "1x"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "size" : "76x76",
81 | "scale" : "2x"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "size" : "83.5x83.5",
86 | "scale" : "2x"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "size" : "1024x1024",
91 | "scale" : "1x"
92 | }
93 | ],
94 | "info" : {
95 | "version" : 1,
96 | "author" : "xcode"
97 | }
98 | }
--------------------------------------------------------------------------------
/ScrollAnimationShowcase/ViewController/Contents/ContentsViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentsViewController.swift
3 | // ScrollAnimationShowcase
4 | //
5 | // Created by 酒井文也 on 2018/11/09.
6 | // Copyright © 2018 酒井文也. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class ContentsViewController: UIViewController {
12 |
13 | @IBOutlet weak private var contentsScrollView: UIScrollView!
14 | @IBOutlet weak private var contentsDetailHeaderView: ContentsDetailHeaderView!
15 |
16 | // MARK: - Override
17 |
18 | override func viewDidLoad() {
19 | super.viewDidLoad()
20 |
21 | setupNavigationBarTitle("サンプル記事詳細")
22 | setupContentsScrollView()
23 | setupContentsDetailHeaderView()
24 | }
25 |
26 | // MARK: - @IBActions
27 |
28 | @IBAction func openSampleCodeLink(_ sender: Any) {
29 | if let url = URL(string: "https://github.com/fumiyasac/ScrollAnimationShowcase") {
30 | UIApplication.shared.open(url, options: [:])
31 | }
32 | }
33 |
34 | @IBAction func openDigitalBookLink(_ sender: Any) {
35 | if let url = URL(string: "https://booth.pm/ja/items/1021745") {
36 | UIApplication.shared.open(url, options: [:])
37 | }
38 | }
39 |
40 | // MARK: - Private Function
41 |
42 | private func setupContentsDetailHeaderView() {
43 | contentsDetailHeaderView.setHeaderImage(UIImage.init(named: "sample"))
44 | }
45 |
46 | private func setupContentsScrollView() {
47 |
48 | // NavigationBar分のスクロール位置がずれてしまうのでその考慮を行う
49 | if #available(iOS 11.0, *) {
50 | contentsScrollView.contentInsetAdjustmentBehavior = .never
51 | }
52 | contentsScrollView.delegate = self
53 | }
54 | }
55 |
56 | // MARK: - UIScrollViewDelegate
57 |
58 | extension ContentsViewController: UIScrollViewDelegate {
59 |
60 | func scrollViewDidScroll(_ scrollView: UIScrollView) {
61 |
62 | // 画像のパララックス効果付きのViewに付与されているAutoLayout制約を変更してパララックス効果を出す
63 | contentsDetailHeaderView.setParallaxEffectToHeaderView(scrollView)
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/ScrollAnimationShowcase/View/CustomViewBase.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CustomViewBase.swift
3 | // ScrollAnimationShowcase
4 | //
5 | // Created by 酒井文也 on 2018/11/10.
6 | // Copyright © 2018 酒井文也. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | //自作のXibを使用するための基底となるUIViewを継承したクラス
13 | //参考:http://skygrid.co.jp/jojakudoctor/swift-custom-class/
14 |
15 | class CustomViewBase: UIView {
16 |
17 | // コンテンツ表示用のView
18 | weak var contentView: UIView!
19 |
20 | // このカスタムビューをコードで使用する際の初期化処理
21 | required override init(frame: CGRect) {
22 | super.init(frame: frame)
23 | initContentView()
24 | }
25 |
26 | // このカスタムビューをInterfaceBuilderで使用する際の初期化処理
27 | required init?(coder aDecoder: NSCoder) {
28 | super.init(coder: aDecoder)
29 | initContentView()
30 | }
31 |
32 | // コンテンツ表示用Viewの初期化処理
33 | private func initContentView() {
34 |
35 | // 追加するcontentViewのクラス名を取得する
36 | let viewClass: AnyClass = type(of: self)
37 |
38 | // 追加するcontentViewに関する設定をする
39 | contentView = Bundle(for: viewClass)
40 | .loadNibNamed(String(describing: viewClass), owner: self, options: nil)?.first as? UIView
41 | contentView.autoresizingMask = autoresizingMask
42 | contentView.frame = bounds
43 | contentView.translatesAutoresizingMaskIntoConstraints = false
44 | addSubview(contentView)
45 |
46 | // 追加するcontentViewの制約を設定する ※上下左右へ0の制約を追加する
47 | let bindings = ["view": contentView as Any]
48 |
49 | let contentViewConstraintH = NSLayoutConstraint.constraints(
50 | withVisualFormat: "H:|[view]|",
51 | options: NSLayoutConstraint.FormatOptions(rawValue: 0),
52 | metrics: nil,
53 | views: bindings
54 | )
55 | let contentViewConstraintV = NSLayoutConstraint.constraints(
56 | withVisualFormat: "V:|[view]|",
57 | options: NSLayoutConstraint.FormatOptions(rawValue: 0),
58 | metrics: nil,
59 | views: bindings
60 | )
61 | addConstraints(contentViewConstraintH)
62 | addConstraints(contentViewConstraintV)
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/ScrollAnimationShowcase/Extension/UICollectionViewExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UICollectionViewExtension.swift
3 | // ScrollAnimationShowcase
4 | //
5 | // Created by 酒井文也 on 2018/11/10.
6 | // Copyright © 2018 酒井文也. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | // UICollectionReusableViewの拡張
13 | extension UICollectionReusableView {
14 |
15 | // 独自に定義したセルのクラス名を返す
16 | static var identifier: String {
17 | return className
18 | }
19 | }
20 |
21 | // UICollectionViewの拡張
22 | extension UICollectionView {
23 |
24 | // 作成した独自のカスタムセルを初期化するメソッド
25 | func registerCustomCell(_ cellType: T.Type) {
26 | register(UINib(nibName: T.identifier, bundle: nil), forCellWithReuseIdentifier: T.identifier)
27 | }
28 |
29 | // 作成した独自のカスタムヘッダー用のViewを初期化するメソッド
30 | func registerCustomReusableHeaderView(_ viewType: T.Type) {
31 | register(UINib(nibName: T.identifier, bundle: nil), forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader ,withReuseIdentifier: T.identifier)
32 | }
33 |
34 | // 作成した独自のカスタムフッター用のViewを初期化するメソッド
35 | func registerCustomReusableFooterView(_ viewType: T.Type) {
36 | register(UINib(nibName: T.identifier, bundle: nil), forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter ,withReuseIdentifier: T.identifier)
37 | }
38 |
39 | // 作成した独自のカスタムセルをインスタンス化するメソッド
40 | func dequeueReusableCustomCell(with cellType: T.Type, indexPath: IndexPath) -> T {
41 | return dequeueReusableCell(withReuseIdentifier: T.identifier, for: indexPath) as! T
42 | }
43 |
44 | // 作成した独自のカスタムヘッダー用のViewをインスタンス化するメソッド
45 | func dequeueReusableCustomHeaderView(with cellType: T.Type, indexPath: IndexPath) -> T {
46 | return dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: T.identifier, for: indexPath) as! T
47 | }
48 |
49 | // 作成した独自のカスタムフッター用のViewをインスタンス化するメソッド
50 | func dequeueReusableCustomFooterView(with cellType: T.Type, indexPath: IndexPath) -> T {
51 | return dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: T.identifier, for: indexPath) as! T
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ScrollAnimationShowcase
2 |
3 | [ING] - UIScrollViewやUICollectionViewの特性を活用した表現サンプル
4 |
5 | ### 実装機能一覧
6 |
7 | UICollectionViewとUIPageViewControllerの性質を利用した、メディアアプリでよく見る無限スクロールするタブの動きを実装したUIサンプルになります。
8 |
9 | ### 本サンプルの画面設計図
10 |
11 | 全体的なアーキテクチャや全体的な画面構成、そしてそれぞれの画面に対応するViewControllerや処理の橋渡しを行うための各種Protocolとの関連性などをまとめたものは下記の図解のような形となります。
12 |
13 | __1. 画面キャプチャ:__
14 |
15 | 
16 |
17 | __2. Storyboardの構成:__
18 |
19 | 
20 |
21 | __3. 該当箇所の全体的なポイントをまとめた概略図:__
22 |
23 | 
24 |
25 | ### UICollectionViewやUIScrollViewを有効活用する
26 |
27 | このサンプルでは、UICollectionViewやUIScrollViewの性質や各種Delegateの処理を活用してUI表現をしています。特に表現を実現する前段階において押さえておくと良さそうな部分についてまとめています。
28 |
29 | __1. UICollectionViewFlowLayoutを継承したクラスを適用する:__
30 |
31 | 
32 |
33 | __2. 無限スクロールを伴うタブ型UI実装する上で必要なセルのインデックス値の調整する:__
34 |
35 | 
36 |
37 | ### その他コードにおいてポイントとなる部分の実装
38 |
39 | 具体的な実装においてポイントになる部分については、下図に示した部分になります。
40 |
41 | __1. インデックス値を調整するための実装:__
42 |
43 | 
44 |
45 | __2. 配置したUICollectionViewのoffset値を調整するための実装:__
46 |
47 | 
48 |
49 | __3. UICollectionViewCellのインデックス値の変更の前後状態を元にUIPageViewControllerの動き方を決定するための実装:__
50 |
51 | 
52 |
53 | __4. 配置したUICollectionViewのスクロールが停止した際の表示位置を調整するための実装:__
54 |
55 | 
56 |
57 | ### その他
58 |
59 | このサンプル全体の詳細解説とポイントをまとめたものは下記に掲載しております。
60 |
61 | (Qiita) https://qiita.com/fumiyasac@github/items/af4fed8ea4d0b94e6bc4
62 |
--------------------------------------------------------------------------------
/ScrollAnimationShowcase/View/CategoryScrollContentsViewCell/CategoryScrollContentsViewCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CategoryScrollContentsViewCell.swift
3 | // ScrollAnimationShowcase
4 | //
5 | // Created by 酒井文也 on 2018/11/16.
6 | // Copyright © 2018 酒井文也. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | final class CategoryScrollContentsViewCell: UICollectionViewCell {
12 |
13 | // 各々のセル間につけるマージンの値
14 | static let cellMargin: CGFloat = 12.0
15 |
16 | @IBOutlet weak private var thumbnailImageView: UIImageView!
17 | @IBOutlet weak private var titleLabel: UILabel!
18 | @IBOutlet weak private var catchcopyLabel: UILabel!
19 | @IBOutlet weak private var categoryLabel: UILabel!
20 | @IBOutlet weak private var dateLabel: UILabel!
21 |
22 | override func awakeFromNib() {
23 | super.awakeFromNib()
24 | }
25 |
26 | // MARK: - Static Function
27 |
28 | static func getCellSize() -> CGSize {
29 |
30 | // 縦方向の隙間の個数・文字表示部分の高さ・画像の縦横比
31 | let numberOfMargin: CGFloat = 3
32 | let descriptionHeight: CGFloat = 69.0
33 | let foodImageAspectRatio: CGFloat = 0.75
34 |
35 | // セルのサイズを上記の値を利用して算出する
36 | let cellWidth = (UIScreen.main.bounds.width - cellMargin * numberOfMargin) / 2
37 | let cellHeight = cellWidth * foodImageAspectRatio + descriptionHeight
38 | return CGSize(width: cellWidth, height: cellHeight)
39 | }
40 |
41 | // MARK: - Function
42 |
43 | func setCellData(_ article: ArticleEntity) {
44 | thumbnailImageView.image = article.imageFile
45 | titleLabel.text = article.title
46 | catchcopyLabel.text = article.catchcopy
47 | categoryLabel.text = article.category
48 | dateLabel.text = article.dateString
49 | }
50 |
51 | func setCellDecoration() {
52 |
53 | // UICollectionViewのcontentViewプロパティには罫線と角丸に関する設定を行う
54 | self.contentView.layer.masksToBounds = true
55 | self.contentView.layer.cornerRadius = 8.0
56 | self.contentView.layer.borderWidth = 1.0
57 | self.contentView.layer.borderColor = UIColor.init(code: "#dddddd").cgColor
58 |
59 | // UICollectionViewのおおもとの部分にはドロップシャドウに関する設定を行う
60 | self.layer.masksToBounds = false
61 | self.layer.shadowColor = UIColor.init(code: "#aaaaaa").cgColor
62 | self.layer.shadowOffset = CGSize(width: 0.75, height: 1.75)
63 | self.layer.shadowRadius = 2.5
64 | self.layer.shadowOpacity = 0.33
65 |
66 | // ドロップシャドウの形状をcontentViewに付与した角丸を考慮するようにする
67 | self.layer.shadowPath = UIBezierPath(roundedRect: self.bounds, cornerRadius: self.contentView.layer.cornerRadius).cgPath
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/ScrollAnimationShowcase/View/CategoryScrollTabViewFlowLayout/CategoryScrollTabViewFlowLayout.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CategoryScrollTabViewFlowLayout.swift
3 | // ScrollAnimationShowcase
4 | //
5 | // Created by 酒井文也 on 2018/11/10.
6 | // Copyright © 2018 酒井文也. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | final class CategoryScrollTabViewFlowLayout: UICollectionViewFlowLayout {
12 |
13 | // 参考1: 下記のリンクで紹介されていたTIPSを元に実装しました
14 | // https://uruly.xyz/carousel-infinite-scroll-3/
15 |
16 | // 参考2: UICollectionViewのlayoutAttributeの変更タイミングに関する記事
17 | // https://qiita.com/kazuhiro4949/items/03bc3d17d3826aa197c0
18 |
19 | // 参考3: UICollectionViewFlowLayoutのサブクラスを利用したスクロールの停止位置算出に関する記事
20 | // https://dev.classmethod.jp/smartphone/iphone/collection-view-layout-cell-snap/
21 |
22 | // 該当のセルのオフセット値を計算するための値(スクリーンの幅 - UICollectionViewに配置しているセルの幅)
23 | private let horizontalTargetOffsetWidth: CGFloat = UIScreen.main.bounds.width - AppConstant.CATEGORY_CELL_WIDTH
24 |
25 | // UICollectionViewをスクロールした後の停止位置を返すためのメソッド
26 | // MEMO: UICollectionViewのLayoutAttributeを調整して、中央に表示されるように調整している
27 | override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
28 |
29 | // 配置されているUICollectionViewを取得する
30 | guard let conllectionView = self.collectionView else {
31 | assertionFailure("UICollectionViewが配置されていません。")
32 | return CGPoint.zero
33 | }
34 | // UICollectionViewのオフセット値を元に該当のセルの情報を取得する
35 | var offsetAdjustment: CGFloat = CGFloat(MAXFLOAT)
36 | let horizontalOffest: CGFloat = proposedContentOffset.x + horizontalTargetOffsetWidth / 2
37 | let targetRect = CGRect(
38 | x: proposedContentOffset.x,
39 | y: 0,
40 | width: conllectionView.bounds.size.width,
41 | height: conllectionView.bounds.size.height
42 | )
43 |
44 | // 配置されているUICollectionViewのlayoutAttributesを元にして停止させたい位置を算出する
45 | guard let layoutAttributes = super.layoutAttributesForElements(in: targetRect) else {
46 | assertionFailure("配置したUICollectionViewにおいて該当セルにおけるlayoutAttributesを取得できません。")
47 | return CGPoint.zero
48 | }
49 | for layoutAttribute in layoutAttributes {
50 | let itemOffset = layoutAttribute.frame.origin.x
51 | if abs(itemOffset - horizontalOffest) < abs(offsetAdjustment) {
52 | offsetAdjustment = itemOffset - horizontalOffest
53 | }
54 | }
55 |
56 | return CGPoint(
57 | x: proposedContentOffset.x + offsetAdjustment,
58 | y: proposedContentOffset.y
59 | )
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/ScrollAnimationShowcase/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // ScrollAnimationShowcase
4 | //
5 | // Created by 酒井文也 on 2018/11/09.
6 | // Copyright © 2018 酒井文也. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 |
14 | var window: UIWindow?
15 |
16 |
17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
18 | // Override point for customization after application launch.
19 |
20 | setupNavigationBarAppearance()
21 |
22 | return true
23 | }
24 |
25 | private func setupNavigationBarAppearance() {
26 |
27 | // iOS15以降ではUINavigationBarの配色指定方法が変化する点に注意する
28 | // https://shtnkgm.com/blog/2021-08-18-ios15.html
29 | if #available(iOS 15.0, *) {
30 | let navigationBarAppearance = UINavigationBarAppearance()
31 | navigationBarAppearance.configureWithOpaqueBackground()
32 |
33 | // MEMO: UINavigationBar内部におけるタイトル文字の装飾設定
34 | navigationBarAppearance.titleTextAttributes = [
35 | NSAttributedString.Key.font : UIFont(name: "HelveticaNeue-Bold", size: 14.0)!,
36 | NSAttributedString.Key.foregroundColor : UIColor.white
37 | ]
38 |
39 | // MEMO: UINavigationBar背景色の装飾設定
40 | navigationBarAppearance.backgroundColor = UIColor(code: "#ff6060")
41 |
42 | UINavigationBar.appearance().standardAppearance = navigationBarAppearance
43 | UINavigationBar.appearance().scrollEdgeAppearance = navigationBarAppearance
44 | }
45 | }
46 |
47 | func applicationWillResignActive(_ application: UIApplication) {
48 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
49 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
50 | }
51 |
52 | func applicationDidEnterBackground(_ application: UIApplication) {
53 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
54 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
55 | }
56 |
57 | func applicationWillEnterForeground(_ application: UIApplication) {
58 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
59 | }
60 |
61 | func applicationDidBecomeActive(_ application: UIApplication) {
62 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
63 | }
64 |
65 | func applicationWillTerminate(_ application: UIApplication) {
66 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
67 | }
68 |
69 |
70 | }
71 |
72 |
--------------------------------------------------------------------------------
/ScrollAnimationShowcase/View/CategoryScrollTabViewCell/CategoryScrollTabViewCell.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/ScrollAnimationShowcase/ViewController/Article/ScrollContents/CategoryScrollContentsViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CategoryContentsViewController.swift
3 | // ScrollAnimationShowcase
4 | //
5 | // Created by 酒井文也 on 2018/11/13.
6 | // Copyright © 2018 酒井文也. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class CategoryScrollContentsViewController: UIViewController {
12 |
13 | private var articlesByCategoryId: [ArticleEntity]! {
14 | didSet {
15 | self.categoryScrollContentsCollectionView.reloadData()
16 | }
17 | }
18 |
19 | @IBOutlet weak private var categoryScrollContentsCollectionView: UICollectionView!
20 | @IBOutlet weak private var desctiptionLabel: UILabel!
21 |
22 | // MARK: - Override
23 |
24 | override func viewDidLoad() {
25 | super.viewDidLoad()
26 |
27 | setupCategoryScrollContentsCollectionView()
28 | }
29 |
30 | // MARK: - Function
31 |
32 | func setArticlesByCategoryId(articles: [ArticleEntity]) {
33 | articlesByCategoryId = articles
34 | }
35 |
36 | func setDescription(text: String) {
37 | desctiptionLabel.text = "現在はカテゴリー「\(text)」です😄"
38 | }
39 |
40 | // MARK: - Private Function
41 |
42 | private func setupCategoryScrollContentsCollectionView() {
43 | categoryScrollContentsCollectionView.delegate = self
44 | categoryScrollContentsCollectionView.dataSource = self
45 | categoryScrollContentsCollectionView.registerCustomCell(CategoryScrollContentsViewCell.self)
46 | }
47 | }
48 |
49 | // MARK: - UICollectionViewDelegate
50 |
51 | extension CategoryScrollContentsViewController: UICollectionViewDelegate {}
52 |
53 | // MARK: - UICollectionViewDataSource
54 |
55 | extension CategoryScrollContentsViewController: UICollectionViewDataSource {
56 |
57 | // 配置するセルの個数を設定する
58 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
59 | return articlesByCategoryId.count
60 | }
61 |
62 | // 配置するセルの表示内容を設定する
63 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
64 | let cell = collectionView.dequeueReusableCustomCell(with: CategoryScrollContentsViewCell.self, indexPath: indexPath)
65 | cell.setCellData(articlesByCategoryId[indexPath.row])
66 | cell.setCellDecoration()
67 | return cell
68 | }
69 |
70 | // セル押下時の処理内容を記載する
71 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
72 | let sb = UIStoryboard(name: "Contents", bundle: nil)
73 | let vc = sb.instantiateInitialViewController() as! ContentsViewController
74 | self.navigationController?.pushViewController(vc, animated: true)
75 | }
76 | }
77 |
78 | // MARK: - UICollectionViewDelegateFlowLayout
79 |
80 | extension CategoryScrollContentsViewController: UICollectionViewDelegateFlowLayout {
81 |
82 | // セルのサイズを設定する
83 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
84 | return CategoryScrollContentsViewCell.getCellSize()
85 | }
86 |
87 | // セルの垂直方向の余白(margin)を設定する
88 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
89 | return CategoryScrollContentsViewCell.cellMargin
90 | }
91 |
92 | // セルの水平方向の余白(margin)を設定する
93 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
94 | return CategoryScrollContentsViewCell.cellMargin
95 | }
96 |
97 | // セル内のアイテム間の余白(margin)調整を行う
98 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
99 | let margin = CategoryScrollContentsViewCell.cellMargin
100 | return UIEdgeInsets(top: margin, left: margin, bottom: margin, right: margin)
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/ScrollAnimationShowcase/View/ContentsDetailHeaderView/ContentsDetailHeaderView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentsDetailHeaderView.swift
3 | // ScrollAnimationShowcase
4 | //
5 | // Created by 酒井文也 on 2018/11/24.
6 | // Copyright © 2018 酒井文也. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | class ContentsDetailHeaderView: UIView {
13 |
14 | private var imageView = UIImageView()
15 | private var imageViewHeightLayoutConstraint = NSLayoutConstraint()
16 | private var imageViewBottomLayoutConstraint = NSLayoutConstraint()
17 |
18 | private var wrappedView = UIView()
19 | private var wrappedViewHeightLayoutConstraint = NSLayoutConstraint()
20 |
21 | // MARK: - Initializer
22 |
23 | required override init(frame: CGRect) {
24 | super.init(frame: frame)
25 |
26 | setupDetailHeaderView()
27 | }
28 |
29 | required init?(coder aDecoder: NSCoder) {
30 | super.init(coder: aDecoder)
31 |
32 | setupDetailHeaderView()
33 | }
34 |
35 | // MARK: - Function
36 |
37 | // バウンス効果のあるUIImageViewに表示する画像をセットする
38 | func setHeaderImage(_ targetImage: UIImage?) {
39 | imageView.image = targetImage
40 | imageView.contentMode = .scaleAspectFill
41 | imageView.clipsToBounds = true
42 | }
43 |
44 | // UIScrollViewの変化量に応じてAutoLayoutの制約を動的に変更する
45 | func setParallaxEffectToHeaderView(_ scrollView: UIScrollView) {
46 |
47 | // UIScrollViewの上方向の余白の変化量をwrappedViewの高さに加算する
48 | // 参考:http://blogios.stack3.net/archives/1663
49 | wrappedViewHeightLayoutConstraint.constant = scrollView.contentInset.top
50 |
51 | // Y軸方向オフセット値を算出する
52 | let offsetY = -(scrollView.contentOffset.y + scrollView.contentInset.top)
53 |
54 | // Y軸方向オフセット値に応じた値をそれぞれの制約に加算する
55 | wrappedView.clipsToBounds = (offsetY <= 0)
56 | imageViewBottomLayoutConstraint.constant = (offsetY >= 0) ? 0 : -offsetY / 2
57 | imageViewHeightLayoutConstraint.constant = max(offsetY + scrollView.contentInset.top, scrollView.contentInset.top)
58 | }
59 |
60 | // MARK: - Private Function
61 |
62 | private func setupDetailHeaderView() {
63 |
64 | self.backgroundColor = UIColor.white
65 |
66 | /**
67 | * ・コードでAutoLayoutを張る場合の注意点等の参考
68 | *
69 | * (1) Auto Layoutをコードから使おう
70 | * http://blog.personal-factory.com/2016/01/11/make-auto-layout-via-code/
71 | *
72 | * (2) Visual Format Languageを使う【Swift3.0】
73 | * http://qiita.com/fromage-blanc/items/7540c6c58bf9d2f7454f
74 | *
75 | * (3) コードでAutolayout
76 | * http://qiita.com/bonegollira/items/5c973206b82f6c4d55ea
77 | */
78 |
79 | // Autosizing → AutoLayoutに変換する設定をオフにする
80 | wrappedView.translatesAutoresizingMaskIntoConstraints = false
81 | wrappedView.backgroundColor = UIColor.white
82 | self.addSubview(wrappedView)
83 |
84 | // このViewに対してwrappedViewに張るConstraint(横方向 → 左:0, 右:0)
85 | let wrappedViewConstarintH = NSLayoutConstraint.constraints(
86 | withVisualFormat: "H:|[wrappedView]|",
87 | options: NSLayoutConstraint.FormatOptions(rawValue: 0),
88 | metrics: nil,
89 | views: ["wrappedView" : wrappedView]
90 | )
91 |
92 | // このViewに対してwrappedViewに張るConstraint(縦方向 → 上:なし, 下:0)
93 | let wrappedViewConstarintV = NSLayoutConstraint.constraints(
94 | withVisualFormat: "V:[wrappedView]|",
95 | options: NSLayoutConstraint.FormatOptions(rawValue: 0),
96 | metrics: nil,
97 | views: ["wrappedView" : wrappedView]
98 | )
99 |
100 | self.addConstraints(wrappedViewConstarintH)
101 | self.addConstraints(wrappedViewConstarintV)
102 |
103 | // wrappedViewの縦幅をいっぱいにする
104 | wrappedViewHeightLayoutConstraint = NSLayoutConstraint(
105 | item: wrappedView,
106 | attribute: .height,
107 | relatedBy: .equal,
108 | toItem: self,
109 | attribute: .height,
110 | multiplier: 1.0,
111 | constant: 0.0
112 | )
113 | self.addConstraint(wrappedViewHeightLayoutConstraint)
114 |
115 | // wrappedViewの中にimageView入れる
116 | imageView.translatesAutoresizingMaskIntoConstraints = false
117 | imageView.backgroundColor = UIColor.white
118 | imageView.clipsToBounds = true
119 | imageView.contentMode = .scaleAspectFill
120 | wrappedView.addSubview(imageView)
121 |
122 | // wrappedViewに対してimageViewに張るConstraint(横方向 → 左:0, 右:0)
123 | let imageViewConstarintH = NSLayoutConstraint.constraints(
124 | withVisualFormat: "H:|[imageView]|",
125 | options: NSLayoutConstraint.FormatOptions(rawValue: 0),
126 | metrics: nil,
127 | views: ["imageView" : imageView]
128 | )
129 |
130 | // wrappedViewの下から0pxの位置に配置する
131 | imageViewBottomLayoutConstraint = NSLayoutConstraint(
132 | item: imageView,
133 | attribute: .bottom,
134 | relatedBy: .equal,
135 | toItem: wrappedView,
136 | attribute: .bottom,
137 | multiplier: 1.0,
138 | constant: 0.0
139 | )
140 |
141 | // imageViewの縦幅をいっぱいにする
142 | imageViewHeightLayoutConstraint = NSLayoutConstraint(
143 | item: imageView,
144 | attribute: .height,
145 | relatedBy: .equal,
146 | toItem: wrappedView,
147 | attribute: .height,
148 | multiplier: 1.0,
149 | constant: 0.0
150 | )
151 |
152 | wrappedView.addConstraints(imageViewConstarintH)
153 | wrappedView.addConstraint(imageViewBottomLayoutConstraint)
154 | wrappedView.addConstraint(imageViewHeightLayoutConstraint)
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/ScrollAnimationShowcase/ViewController/Article/ArticleViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ArticleViewController.swift
3 | // ScrollAnimationShowcase
4 | //
5 | // Created by 酒井文也 on 2018/11/10.
6 | // Copyright © 2018 酒井文也. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class ArticleViewController: UIViewController {
12 |
13 | // カテゴリーの一覧データ
14 | private let categoryList: [String] = ArticleMock.getArticleCategories()
15 |
16 | // 現在表示しているViewControllerのタグ番号
17 | private var currentCategoryIndex: Int = 0
18 |
19 | // ページングして表示させるViewControllerを保持する配列
20 | private var targetViewControllerLists: [UIViewController] = []
21 |
22 | // ContainerViewにEmbedしたUIPageViewControllerのインスタンスを保持する
23 | private var pageViewController: UIPageViewController?
24 |
25 | // MARK: - Override
26 |
27 | override func viewDidLoad() {
28 | super.viewDidLoad()
29 |
30 | // MEMO: InterfaceBuilderでNavigationBarの背景色を#ff6060 / Trunslucentをfalseとする
31 | setupNavigationBarTitle("サンプル記事一覧")
32 | removeBackButtonText()
33 | setupPageViewController()
34 | }
35 |
36 | // Segueに設定したIdentifierから接続されたViewControllerを取得する
37 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
38 |
39 | switch segue.identifier {
40 |
41 | // ContainerViewで接続されたViewController側に定義したプロトコルを適用する
42 | case "CategoryScrollTabViewContainer":
43 | let vc = segue.destination as! CategoryScrollTabViewController
44 | vc.delegate = self
45 |
46 | default:
47 | break
48 | }
49 | }
50 |
51 | // MARK: - Private Function
52 |
53 | private func setupPageViewController() {
54 |
55 | // UIPageViewControllerで表示させるViewControllerの一覧を配列へ格納する
56 | let _ = categoryList.enumerated().map{ (index, categoryName) in
57 | let sb = UIStoryboard(name: "Article", bundle: nil)
58 | let vc = sb.instantiateViewController(withIdentifier: "CategoryScrollContents") as! CategoryScrollContentsViewController
59 | vc.view.tag = index
60 | vc.setDescription(text: categoryName)
61 | vc.setArticlesByCategoryId(articles: ArticleMock.getArticlesBy(categoryId: index))
62 | targetViewControllerLists.append(vc)
63 | }
64 |
65 | // ContainerViewにEmbedしたUIPageViewControllerを取得する
66 | for childVC in children {
67 | if let targetVC = childVC as? UIPageViewController {
68 | pageViewController = targetVC
69 | }
70 | }
71 |
72 | // UIPageViewControllerDelegate & UIPageViewControllerDataSourceの宣言
73 | pageViewController!.delegate = self
74 | pageViewController!.dataSource = self
75 |
76 | // 最初に表示する画面として配列の先頭のViewControllerを設定する
77 | pageViewController!.setViewControllers([targetViewControllerLists[0]], direction: .forward, animated: false, completion: nil)
78 | }
79 |
80 | // 配置されているタブ表示のUICollectionViewの位置を更新する
81 | // MEMO: ContainerViewで配置しているViewControllerの親子関係を利用する
82 | private func updateCategoryScrollTabPosition(isIncrement: Bool) {
83 | for childVC in children {
84 | if let targetVC = childVC as? CategoryScrollTabViewController {
85 | targetVC.moveToCategoryScrollTab(isIncrement: isIncrement)
86 | }
87 | }
88 | }
89 | }
90 |
91 | // MARK: - UIPageViewControllerDelegate
92 |
93 | extension ArticleViewController: UIPageViewControllerDelegate {
94 |
95 | // ページが動いたタイミング(この場合はスワイプアニメーションに該当)に発動する処理を記載するメソッド
96 | // (実装例)http://c-geru.com/as_blind_side/2014/09/uipageviewcontroller.html
97 | // (実装例に関する解説)http://chaoruko-tech.hatenablog.com/entry/2014/05/15/103811
98 | // (公式ドキュメント)https://developer.apple.com/reference/uikit/uipageviewcontrollerdelegate
99 | func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
100 |
101 | // スワイプアニメーションが完了していない時には処理をさせなくする
102 | if !completed { return }
103 |
104 | // ここから先はUIPageViewControllerのスワイプアニメーション完了時に発動する
105 | if let targetViewControllers = pageViewController.viewControllers {
106 | if let targetViewController = targetViewControllers.last {
107 |
108 | // Case1: UIPageViewControllerで表示する画面のインデックス値が左スワイプで 0 → 最大インデックス値
109 | if targetViewController.view.tag - currentCategoryIndex == -categoryList.count + 1 {
110 | updateCategoryScrollTabPosition(isIncrement: true)
111 |
112 | // Case2: UIPageViewControllerで表示する画面のインデックス値が右スワイプで 最大インデックス値 → 0
113 | } else if targetViewController.view.tag - currentCategoryIndex == categoryList.count - 1 {
114 | updateCategoryScrollTabPosition(isIncrement: false)
115 |
116 | // Case3: UIPageViewControllerで表示する画面のインデックス値が +1
117 | } else if targetViewController.view.tag - currentCategoryIndex > 0 {
118 | updateCategoryScrollTabPosition(isIncrement: true)
119 |
120 | // Case4: UIPageViewControllerで表示する画面のインデックス値が -1
121 | } else if targetViewController.view.tag - currentCategoryIndex < 0 {
122 | updateCategoryScrollTabPosition(isIncrement: false)
123 | }
124 |
125 | // 受け取ったインデックス値を元にコンテンツ表示を更新する
126 | currentCategoryIndex = targetViewController.view.tag
127 | }
128 | }
129 | }
130 | }
131 |
132 | // MARK: - UIPageViewControllerDataSource
133 |
134 | extension ArticleViewController: UIPageViewControllerDataSource {
135 |
136 | // 逆方向にページ送りした時に呼ばれるメソッド
137 | func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
138 |
139 | // インデックスを取得する
140 | guard let index = targetViewControllerLists.firstIndex(of: viewController) else {
141 | return nil
142 | }
143 |
144 | // インデックスの値に応じてコンテンツを動かす
145 | if index <= 0 {
146 | return targetViewControllerLists.last
147 | } else {
148 | return targetViewControllerLists[index - 1]
149 | }
150 | }
151 |
152 | // 順方向にページ送りした時に呼ばれるメソッド
153 | func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
154 |
155 | // インデックスを取得する
156 | guard let index = targetViewControllerLists.firstIndex(of: viewController) else {
157 | return nil
158 | }
159 |
160 | // インデックスの値に応じてコンテンツを動かす
161 | if index >= targetViewControllerLists.count - 1 {
162 | return targetViewControllerLists.first
163 | } else {
164 | return targetViewControllerLists[index + 1]
165 | }
166 | }
167 | }
168 |
169 | // MARK: - CategoryScrollTabDelegate
170 |
171 | extension ArticleViewController: CategoryScrollTabDelegate {
172 |
173 | // タブ側のViewControllerで選択されたインデックス値とスクロール方向を元に表示する位置を調整する
174 | func moveToCategoryScrollContents(selectedCollectionViewIndex: Int, targetDirection: UIPageViewController.NavigationDirection, withAnimated: Bool) {
175 |
176 | // UIPageViewControllerに設定した画面の表示対象インデックス値を設定する
177 | // MEMO: タブ表示のUICollectionViewCellのインデックス値をカテゴリーの個数で割った剰余
178 | currentCategoryIndex = selectedCollectionViewIndex % categoryList.count
179 |
180 | // 表示対象インデックス値に該当する画面を表示する
181 | // MEMO: メインスレッドで実行するようにしてクラッシュを防止する対策を施している
182 | DispatchQueue.main.async {
183 | if let targetPageViewController = self.pageViewController {
184 | targetPageViewController.setViewControllers([self.targetViewControllerLists[self.currentCategoryIndex]], direction: targetDirection, animated: withAnimated, completion: nil)
185 | }
186 | }
187 | }
188 | }
189 |
--------------------------------------------------------------------------------
/ScrollAnimationShowcase/View/CategoryScrollContentsViewCell/CategoryScrollContentsViewCell.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
42 |
43 |
44 |
45 |
51 |
57 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
--------------------------------------------------------------------------------
/ScrollAnimationShowcase/ViewController/Article/ScrollTab/CategoryScrollTabViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CategoryScrollTabViewController.swift
3 | // ScrollAnimationShowcase
4 | //
5 | // Created by 酒井文也 on 2018/11/10.
6 | // Copyright © 2018 酒井文也. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | // カテゴリータブ操作時に実行されるプロトコル
12 | protocol CategoryScrollTabDelegate: NSObjectProtocol {
13 |
14 | // UIPageViewControllerで表示しているインデックスの画面へ遷移する
15 | func moveToCategoryScrollContents(selectedCollectionViewIndex: Int, targetDirection: UIPageViewController.NavigationDirection, withAnimated: Bool)
16 | }
17 |
18 | class CategoryScrollTabViewController: UIViewController {
19 |
20 | // CategoryScrollTabDelegateプロトコル
21 | weak var delegate: CategoryScrollTabDelegate?
22 |
23 | // カテゴリーの一覧データ
24 | private let categoryList: [String] = ArticleMock.getArticleCategories()
25 |
26 | // ボタン押下時の軽微な振動を追加する
27 | private let buttonFeedbackGenerator: UIImpactFeedbackGenerator = {
28 | let generator: UIImpactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light)
29 | generator.prepare()
30 | return generator
31 | }()
32 |
33 | // MEMO: UICollectionViewの一番最初のセル表示位置に関する設定
34 | // 参考: https://www.101010.fun/entry/swift-once-exec
35 | private lazy var setInitialCategoryScrollTabPosition: (() -> ())? = {
36 |
37 | // 押下した場所のインデックス値を持っておくために、実際のタブ個数の2倍の値を設定する
38 | currentSelectIndex = self.categoryList.count * 2
39 | //print("初期表示時の中央インデックス値:", currentSelectIndex)
40 |
41 | // 変数(currentSelectIndex)を基準にして位置情報を更新する
42 | updateCategoryScrollTabCollectionViewPosition(withAnimated: false)
43 | return nil
44 | }()
45 |
46 | // 配置したセル幅の合計値
47 | private var allTabViewTotalWidth: CGFloat = 0.0
48 |
49 | // 現在選択中のインデックス値を格納する変数(このクラスに配置しているUICollectionViewのIndex番号)
50 | private var currentSelectIndex = 0
51 |
52 | @IBOutlet weak private var selectedCatogoryUnderlineWidth: NSLayoutConstraint!
53 | @IBOutlet weak private var categoryScrollTabCollectionView: UICollectionView!
54 |
55 | // MARK: - Computed Properties
56 |
57 | // MEMO:
58 | // ここでは無限スクロールができるように予め、(実際の個数 × 4)のセルを配置している
59 | // またscrollViewDidScroll内の処理で所定の位置で調整をかけるので実際のUICollectionViewCellのインデックス値の範囲は下記のようになる
60 | // Ex. タブを6個設定する場合 → 6 ... 19が取り得る範囲となる
61 |
62 | // 表示するカテゴリーの個数を元にしたインデックスの最大値
63 | // 例. カテゴリーが6個の場合は5となる
64 | private var targetContentsMaxIndex: Int {
65 | return categoryList.count - 1
66 | }
67 |
68 | // 実際に配置したUICollectionViewCellが取り得るインデックスの最大値
69 | // 例. カテゴリーが6個の場合は19となる
70 | private var targetCollectionViewCellMaxIndex: Int {
71 | return categoryList.count * 4 - targetContentsMaxIndex
72 | }
73 |
74 | // 実際に配置したUICollectionViewCellが取り得るインデックスの最小値
75 | // 例. カテゴリーが6個の場合は6となる
76 | private var targetCollectionViewCellMinIndex: Int {
77 | return categoryList.count
78 | }
79 |
80 | // MARK: - Override
81 |
82 | override func viewDidLoad() {
83 | super.viewDidLoad()
84 |
85 | setupCategoryScrollTabCollectionView()
86 | }
87 |
88 | override func viewDidAppear(_ animated: Bool) {
89 | super.viewDidAppear(animated)
90 |
91 | // MEMO: この部分は一番最初に起動した時だけ発火するようにする
92 | setInitialCategoryScrollTabPosition?()
93 | }
94 |
95 | // MARK: - Function
96 |
97 | // 親(ArticleViewController)のUIPageViewControllerのスクロール方向を元にUICollectionViewの位置を設定する
98 | // MEMO: このメソッドはUIPageViewControllerを配置している親(ArticleViewController)から実行される
99 | func moveToCategoryScrollTab(isIncrement: Bool = true) {
100 |
101 | // UIPageViewControllerのスワイプ方向を元に、更新するインデックスの値を設定する
102 | var targetIndex = isIncrement ? currentSelectIndex + 1 : currentSelectIndex - 1
103 |
104 | // 取りうるべきインデックスの値が閾値(targetCollectionViewCellMaxIndex)を超えた場合は補正をする
105 | if targetIndex > targetCollectionViewCellMaxIndex {
106 | targetIndex = targetCollectionViewCellMaxIndex - targetContentsMaxIndex
107 | currentSelectIndex = targetCollectionViewCellMaxIndex
108 | }
109 |
110 | // 取りうるべきインデックスの値が閾値(targetCollectionViewCellMinIndex)を下回った場合は補正をする
111 | if targetIndex < targetCollectionViewCellMinIndex {
112 | targetIndex = targetCollectionViewCellMinIndex + targetContentsMaxIndex
113 | currentSelectIndex = targetCollectionViewCellMinIndex
114 | }
115 |
116 | // MEMO: タブがスクロールされている状態でUIPageViewControllerがスワイプされた場合の考慮
117 | // → スクロール中である場合には強制的に慣性スクロールを停止させる
118 | let isScrolling = (categoryScrollTabCollectionView.isDragging || categoryScrollTabCollectionView.isDecelerating)
119 | if isScrolling {
120 | categoryScrollTabCollectionView.setContentOffset(categoryScrollTabCollectionView.contentOffset, animated: true)
121 | }
122 |
123 | // 押下した場所のインデックス値を持っておく
124 | currentSelectIndex = targetIndex
125 | //print("コンテンツ表示側のインデックスを元にした現在のインデックス値:", currentSelectIndex)
126 |
127 | // 変数(currentSelectIndex)を基準にして位置情報を更新する
128 | updateCategoryScrollTabCollectionViewPosition(withAnimated: true)
129 |
130 | // 「コツッ」とした感じの端末フィードバックを発火する
131 | buttonFeedbackGenerator.impactOccurred()
132 | }
133 |
134 | // MARK: - Private Function
135 |
136 | // UICollectionViewに関する設定
137 | private func setupCategoryScrollTabCollectionView() {
138 | categoryScrollTabCollectionView.delegate = self
139 | categoryScrollTabCollectionView.dataSource = self
140 | categoryScrollTabCollectionView.registerCustomCell(CategoryScrollTabViewCell.self)
141 | categoryScrollTabCollectionView.showsHorizontalScrollIndicator = false
142 |
143 | // MEMO: タブ内のスクロール移動を許可する場合はtrueにし、許可しない場合はfalseとする
144 | categoryScrollTabCollectionView.isScrollEnabled = true
145 | }
146 |
147 | // 選択もしくはスクロールが止まるであろう位置にあるセルのインデックス値を元にUICollectionViewの位置を更新する
148 | private func updateCategoryScrollTabCollectionViewPosition(withAnimated: Bool = false) {
149 |
150 | // インデックス値に相当するタブを真ん中に表示させる
151 | let targetIndexPath = IndexPath(row: currentSelectIndex, section: 0)
152 | categoryScrollTabCollectionView.scrollToItem(at: targetIndexPath, at: .centeredHorizontally, animated: withAnimated)
153 |
154 | // UICollectionViewの下線の長さを設定する
155 | let categoryListIndex = currentSelectIndex % categoryList.count
156 | setUnderlineWidthFrom(categoryTitle: categoryList[categoryListIndex])
157 |
158 | // 現在選択されている位置に色を付けるためにCollectionViewをリロードする
159 | categoryScrollTabCollectionView.reloadData()
160 | }
161 |
162 | // スクロールするタブの下にある下線の幅を文字の長さに合わせて設定する
163 | private func setUnderlineWidthFrom(categoryTitle: String) {
164 |
165 | // 下線用のViewに付与したAutoLayoutの幅に関する制約値を更新する
166 | let targetWidth = CategoryScrollTabViewCell.calculateCategoryUnderBarWidthBy(title: categoryTitle)
167 | selectedCatogoryUnderlineWidth.constant = targetWidth
168 | UIView.animate(withDuration: 0.36, animations: {
169 | self.view.layoutIfNeeded()
170 | })
171 | }
172 |
173 | // UIPageViewControllerを動かす方向を受け取ったインデックス値(indexPath.row)と現在のインデックス値(currentSelectIndex)を元に算出する
174 | // MEMO: 親(ArticleViewController)のUIPageViewCotrollerの更新はCategoryScrollTabDelegateのメソッドを経由して実行する
175 | private func getCategoryScrollContentsDirection(selectedIndex: Int) -> UIPageViewController.NavigationDirection {
176 |
177 | // 下記の条件を満たす場合は例外的に進む方向とする
178 | // 1. 引数で渡されたインデックス値:
179 | // - selectedIndex が (targetCollectionViewCellMaxIndex - targetContentsMaxIndex) と等しい
180 | // 2. 現在のインデックス値:
181 | // - currentSelectIndex が targetCollectionViewCellMaxIndex と等しい
182 | if selectedIndex == targetCollectionViewCellMaxIndex - targetContentsMaxIndex && currentSelectIndex == targetCollectionViewCellMaxIndex {
183 | return UIPageViewController.NavigationDirection.forward
184 | }
185 |
186 | // 下記の条件を満たす場合は例外的に戻す方向とする
187 | // 1. 引数で渡されたインデックス値:
188 | // - selectedIndex が (targetCollectionViewCellMinIndex + targetContentsMaxIndex) と等しい
189 | // 2. 現在のインデックス値:
190 | // - currentSelectIndex が targetCollectionViewCellMinIndex と等しい
191 | if selectedIndex == targetCollectionViewCellMinIndex + targetContentsMaxIndex && currentSelectIndex == targetCollectionViewCellMinIndex {
192 | return UIPageViewController.NavigationDirection.reverse
193 | }
194 |
195 | // (現在のインデックス値 - 引数で渡されたインデックス値)を元に方向を算出する
196 | if currentSelectIndex - selectedIndex > 0 {
197 | return UIPageViewController.NavigationDirection.reverse
198 | } else {
199 | return UIPageViewController.NavigationDirection.forward
200 | }
201 | }
202 | }
203 |
204 | // MARK: - UICollectionViewDelegate
205 |
206 | extension CategoryScrollTabViewController: UICollectionViewDelegate {}
207 |
208 | // MARK: - UICollectionViewDataSource
209 |
210 | extension CategoryScrollTabViewController: UICollectionViewDataSource {
211 |
212 | // 配置するセルの個数を設定する
213 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
214 |
215 | // MEMO: 無限スクロールの対象とする場合はタブ表示要素の4倍余分に要素を表示する
216 | return categoryList.count * 4
217 | }
218 |
219 | // 配置するセルの表示内容を設定する
220 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
221 | let cell = collectionView.dequeueReusableCustomCell(with: CategoryScrollTabViewCell.self, indexPath: indexPath)
222 | let targetIndex = indexPath.row % categoryList.count
223 | let isSelectedTab = (indexPath.row % categoryList.count == currentSelectIndex % categoryList.count)
224 | cell.setCategory(name: categoryList[targetIndex], isSelected: isSelectedTab)
225 | return cell
226 | }
227 |
228 | // セル押下時の処理内容を記載する
229 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
230 |
231 | // UIPageViewControllerを動かす方向を選択したインデックス値(indexPath.row)と現在のインデックス値(currentSelectIndex)を元に算出する
232 | let targetDirection = getCategoryScrollContentsDirection(selectedIndex: indexPath.row)
233 |
234 | // 押下した場所のインデックス値を現在のインデックス値を格納している変数(currentSelectIndex)にセットする
235 | currentSelectIndex = indexPath.row
236 | //print("タブ押下時の中央インデックス値:", currentSelectIndex)
237 |
238 | // 変数(currentSelectIndex)を基準にして位置情報を更新する
239 | updateCategoryScrollTabCollectionViewPosition(withAnimated: true)
240 |
241 | // 算出した現在のインデックス値・動かす方向の値を元に、UIPageViewControllerで表示しているインデックスの画面へ遷移する
242 | self.delegate?.moveToCategoryScrollContents(
243 | selectedCollectionViewIndex: currentSelectIndex,
244 | targetDirection: targetDirection,
245 | withAnimated: true
246 | )
247 |
248 | // 「コツッ」とした感じの端末フィードバックを発火する
249 | buttonFeedbackGenerator.impactOccurred()
250 | }
251 | }
252 |
253 | // MARK: - UICollectionViewDelegateFlowLayout
254 |
255 | extension CategoryScrollTabViewController: UICollectionViewDelegateFlowLayout {
256 |
257 | // タブ用のセルにおける矩形サイズを設定する
258 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
259 | return CategoryScrollTabViewCell.cellSize
260 | }
261 | }
262 |
263 | // MARK: - UIScrollViewDelegate
264 |
265 | extension CategoryScrollTabViewController: UIScrollViewDelegate {
266 |
267 | // 配置したUICollectionViewをスクロールしている際に実行される処理
268 | func scrollViewDidScroll(_ scrollView: UIScrollView) {
269 |
270 | // 表示したいセル要素のWidthを計算する
271 | // MEMO: 実際の幅の値が欲しいのでUIScrollView内の幅を1/4したものになる
272 | if allTabViewTotalWidth == 0.0 {
273 | allTabViewTotalWidth = floor(scrollView.contentSize.width / 4.0)
274 | }
275 |
276 | // スクロールした位置が閾値を超えたら中央に戻す
277 | if (scrollView.contentOffset.x <= allTabViewTotalWidth) || (scrollView.contentOffset.x > allTabViewTotalWidth * 3.0) {
278 | scrollView.contentOffset.x = allTabViewTotalWidth * 2.0
279 | }
280 | }
281 |
282 | // 配置したUICollectionViewをスクロールが止まった際に実行される処理
283 | func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
284 |
285 | // スクロールが停止した際に見えているセルのインデックス値を格納して、真ん中にあるものを取得する
286 | // 参考: https://stackoverflow.com/questions/18649920/uicollectionview-current-visible-cell-index
287 |
288 | var visibleIndexPathList: [IndexPath] = []
289 | for cell in categoryScrollTabCollectionView.visibleCells {
290 | if let visibleIndexPath = categoryScrollTabCollectionView.indexPath(for: cell) {
291 | visibleIndexPathList.append(visibleIndexPath)
292 | //print("現在画面内に見えているセルのインデックス値:", visibleIndexPath)
293 | }
294 | }
295 | let targetIndexPath = visibleIndexPathList[1]
296 |
297 | // ※この部分は厳密には不要ではあるがdelegeteで引き渡す必要があるので設定している
298 | let targetDirection = getCategoryScrollContentsDirection(selectedIndex: targetIndexPath.row)
299 |
300 | // 押下した場所のインデックス値を現在のインデックス値を格納している変数(currentSelectIndex)にセットする
301 | currentSelectIndex = targetIndexPath.row
302 | //print("スクロールが慣性で停止した時の中央インデックス値:", currentSelectIndex)
303 |
304 | // 変数(currentSelectIndex)を基準にして位置情報を更新する
305 | updateCategoryScrollTabCollectionViewPosition(withAnimated: true)
306 |
307 | // 算出した現在のインデックス値・動かす方向の値を元に、UIPageViewControllerで表示しているインデックスの画面へ遷移する
308 | self.delegate?.moveToCategoryScrollContents(
309 | selectedCollectionViewIndex: currentSelectIndex,
310 | targetDirection: targetDirection,
311 | withAnimated: false
312 | )
313 |
314 | // 「コツッ」とした感じの端末フィードバックを発火する
315 | buttonFeedbackGenerator.impactOccurred()
316 | }
317 | }
318 |
--------------------------------------------------------------------------------
/ScrollAnimationShowcase/Storyboard/Base.lproj/Article.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
--------------------------------------------------------------------------------
/ScrollAnimationShowcase.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 50;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | DE1C6E4021959F7900BAA03C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE1C6E3F21959F7900BAA03C /* AppDelegate.swift */; };
11 | DE1C6E4221959F7900BAA03C /* ContentsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE1C6E4121959F7900BAA03C /* ContentsViewController.swift */; };
12 | DE1C6E4521959F7900BAA03C /* Contents.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DE1C6E4321959F7900BAA03C /* Contents.storyboard */; };
13 | DE1C6E4721959F8000BAA03C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DE1C6E4621959F8000BAA03C /* Assets.xcassets */; };
14 | DE1C6E4A21959F8000BAA03C /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DE1C6E4821959F8000BAA03C /* LaunchScreen.storyboard */; };
15 | DE1C6E5521959F8100BAA03C /* ScrollAnimationShowcaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE1C6E5421959F8100BAA03C /* ScrollAnimationShowcaseTests.swift */; };
16 | DE1C6E6021959F8100BAA03C /* ScrollAnimationShowcaseUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE1C6E5F21959F8100BAA03C /* ScrollAnimationShowcaseUITests.swift */; };
17 | DE1C6E782195AD4600BAA03C /* UIViewControllerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE1C6E772195AD4600BAA03C /* UIViewControllerExtension.swift */; };
18 | DE1C6E7A2196571200BAA03C /* UIColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE1C6E792196571200BAA03C /* UIColorExtension.swift */; };
19 | DE1C6E7C2196573C00BAA03C /* NSObjectProtocolExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE1C6E7B2196573C00BAA03C /* NSObjectProtocolExtension.swift */; };
20 | DE1C6E7E219657A300BAA03C /* UICollectionViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE1C6E7D219657A300BAA03C /* UICollectionViewExtension.swift */; };
21 | DE1C6E8221965FFA00BAA03C /* CustomViewBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE1C6E8121965FFA00BAA03C /* CustomViewBase.swift */; };
22 | DE1C6E88219667FA00BAA03C /* Article.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DE1C6E86219667FA00BAA03C /* Article.storyboard */; };
23 | DE1C6E8A2196681D00BAA03C /* ArticleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE1C6E892196681D00BAA03C /* ArticleViewController.swift */; };
24 | DE1C6E8F2196721F00BAA03C /* CategoryScrollTabViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE1C6E8E2196721F00BAA03C /* CategoryScrollTabViewController.swift */; };
25 | DE1C6E9221967F3900BAA03C /* AppConstant.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE1C6E9121967F3900BAA03C /* AppConstant.swift */; };
26 | DE1C6E9521967F9800BAA03C /* CategoryScrollTabViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE1C6E9321967F9800BAA03C /* CategoryScrollTabViewCell.swift */; };
27 | DE1C6E9621967F9800BAA03C /* CategoryScrollTabViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = DE1C6E9421967F9800BAA03C /* CategoryScrollTabViewCell.xib */; };
28 | DE1C6E992196C79300BAA03C /* CategoryScrollTabViewFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE1C6E982196C79300BAA03C /* CategoryScrollTabViewFlowLayout.swift */; };
29 | DE1C6EA02199D4F800BAA03C /* CategoryScrollContentsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE1C6E9F2199D4F800BAA03C /* CategoryScrollContentsViewController.swift */; };
30 | DE1C6EA22199DDE700BAA03C /* ArticleMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE1C6EA12199DDE700BAA03C /* ArticleMock.swift */; };
31 | DE1C6EA6219EFE7D00BAA03C /* CategoryScrollContentsViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE1C6EA4219EFE7C00BAA03C /* CategoryScrollContentsViewCell.swift */; };
32 | DE1C6EA7219EFE7D00BAA03C /* CategoryScrollContentsViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = DE1C6EA5219EFE7C00BAA03C /* CategoryScrollContentsViewCell.xib */; };
33 | DE2553A621A9434400D9A3C7 /* ContentsDetailHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2553A521A9434400D9A3C7 /* ContentsDetailHeaderView.swift */; };
34 | DE2553A821AA4A6200D9A3C7 /* ArticleEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2553A721AA4A6200D9A3C7 /* ArticleEntity.swift */; };
35 | DE2553AA21AA66F300D9A3C7 /* IntExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2553A921AA66F300D9A3C7 /* IntExtension.swift */; };
36 | /* End PBXBuildFile section */
37 |
38 | /* Begin PBXContainerItemProxy section */
39 | DE1C6E5121959F8100BAA03C /* PBXContainerItemProxy */ = {
40 | isa = PBXContainerItemProxy;
41 | containerPortal = DE1C6E3421959F7900BAA03C /* Project object */;
42 | proxyType = 1;
43 | remoteGlobalIDString = DE1C6E3B21959F7900BAA03C;
44 | remoteInfo = ScrollAnimationShowcase;
45 | };
46 | DE1C6E5C21959F8100BAA03C /* PBXContainerItemProxy */ = {
47 | isa = PBXContainerItemProxy;
48 | containerPortal = DE1C6E3421959F7900BAA03C /* Project object */;
49 | proxyType = 1;
50 | remoteGlobalIDString = DE1C6E3B21959F7900BAA03C;
51 | remoteInfo = ScrollAnimationShowcase;
52 | };
53 | /* End PBXContainerItemProxy section */
54 |
55 | /* Begin PBXFileReference section */
56 | DE1C6E3C21959F7900BAA03C /* ScrollAnimationShowcase.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ScrollAnimationShowcase.app; sourceTree = BUILT_PRODUCTS_DIR; };
57 | DE1C6E3F21959F7900BAA03C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
58 | DE1C6E4121959F7900BAA03C /* ContentsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentsViewController.swift; sourceTree = ""; };
59 | DE1C6E4421959F7900BAA03C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Contents.storyboard; sourceTree = ""; };
60 | DE1C6E4621959F8000BAA03C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
61 | DE1C6E4921959F8000BAA03C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
62 | DE1C6E4B21959F8000BAA03C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
63 | DE1C6E5021959F8100BAA03C /* ScrollAnimationShowcaseTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ScrollAnimationShowcaseTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
64 | DE1C6E5421959F8100BAA03C /* ScrollAnimationShowcaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollAnimationShowcaseTests.swift; sourceTree = ""; };
65 | DE1C6E5621959F8100BAA03C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
66 | DE1C6E5B21959F8100BAA03C /* ScrollAnimationShowcaseUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ScrollAnimationShowcaseUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
67 | DE1C6E5F21959F8100BAA03C /* ScrollAnimationShowcaseUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollAnimationShowcaseUITests.swift; sourceTree = ""; };
68 | DE1C6E6121959F8100BAA03C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
69 | DE1C6E772195AD4600BAA03C /* UIViewControllerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewControllerExtension.swift; sourceTree = ""; };
70 | DE1C6E792196571200BAA03C /* UIColorExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIColorExtension.swift; sourceTree = ""; };
71 | DE1C6E7B2196573C00BAA03C /* NSObjectProtocolExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSObjectProtocolExtension.swift; sourceTree = ""; };
72 | DE1C6E7D219657A300BAA03C /* UICollectionViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UICollectionViewExtension.swift; sourceTree = ""; };
73 | DE1C6E8121965FFA00BAA03C /* CustomViewBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomViewBase.swift; sourceTree = ""; };
74 | DE1C6E87219667FA00BAA03C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Article.storyboard; sourceTree = ""; };
75 | DE1C6E892196681D00BAA03C /* ArticleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleViewController.swift; sourceTree = ""; };
76 | DE1C6E8E2196721F00BAA03C /* CategoryScrollTabViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryScrollTabViewController.swift; sourceTree = ""; };
77 | DE1C6E9121967F3900BAA03C /* AppConstant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConstant.swift; sourceTree = ""; };
78 | DE1C6E9321967F9800BAA03C /* CategoryScrollTabViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryScrollTabViewCell.swift; sourceTree = ""; };
79 | DE1C6E9421967F9800BAA03C /* CategoryScrollTabViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CategoryScrollTabViewCell.xib; sourceTree = ""; };
80 | DE1C6E982196C79300BAA03C /* CategoryScrollTabViewFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryScrollTabViewFlowLayout.swift; sourceTree = ""; };
81 | DE1C6E9F2199D4F800BAA03C /* CategoryScrollContentsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryScrollContentsViewController.swift; sourceTree = ""; };
82 | DE1C6EA12199DDE700BAA03C /* ArticleMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleMock.swift; sourceTree = ""; };
83 | DE1C6EA4219EFE7C00BAA03C /* CategoryScrollContentsViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryScrollContentsViewCell.swift; sourceTree = ""; };
84 | DE1C6EA5219EFE7C00BAA03C /* CategoryScrollContentsViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CategoryScrollContentsViewCell.xib; sourceTree = ""; };
85 | DE2553A521A9434400D9A3C7 /* ContentsDetailHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentsDetailHeaderView.swift; sourceTree = ""; };
86 | DE2553A721AA4A6200D9A3C7 /* ArticleEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleEntity.swift; sourceTree = ""; };
87 | DE2553A921AA66F300D9A3C7 /* IntExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntExtension.swift; sourceTree = ""; };
88 | /* End PBXFileReference section */
89 |
90 | /* Begin PBXFrameworksBuildPhase section */
91 | DE1C6E3921959F7900BAA03C /* Frameworks */ = {
92 | isa = PBXFrameworksBuildPhase;
93 | buildActionMask = 2147483647;
94 | files = (
95 | );
96 | runOnlyForDeploymentPostprocessing = 0;
97 | };
98 | DE1C6E4D21959F8100BAA03C /* Frameworks */ = {
99 | isa = PBXFrameworksBuildPhase;
100 | buildActionMask = 2147483647;
101 | files = (
102 | );
103 | runOnlyForDeploymentPostprocessing = 0;
104 | };
105 | DE1C6E5821959F8100BAA03C /* Frameworks */ = {
106 | isa = PBXFrameworksBuildPhase;
107 | buildActionMask = 2147483647;
108 | files = (
109 | );
110 | runOnlyForDeploymentPostprocessing = 0;
111 | };
112 | /* End PBXFrameworksBuildPhase section */
113 |
114 | /* Begin PBXGroup section */
115 | DE1C6E3321959F7900BAA03C = {
116 | isa = PBXGroup;
117 | children = (
118 | DE1C6E3E21959F7900BAA03C /* ScrollAnimationShowcase */,
119 | DE1C6E5321959F8100BAA03C /* ScrollAnimationShowcaseTests */,
120 | DE1C6E5E21959F8100BAA03C /* ScrollAnimationShowcaseUITests */,
121 | DE1C6E3D21959F7900BAA03C /* Products */,
122 | );
123 | sourceTree = "";
124 | };
125 | DE1C6E3D21959F7900BAA03C /* Products */ = {
126 | isa = PBXGroup;
127 | children = (
128 | DE1C6E3C21959F7900BAA03C /* ScrollAnimationShowcase.app */,
129 | DE1C6E5021959F8100BAA03C /* ScrollAnimationShowcaseTests.xctest */,
130 | DE1C6E5B21959F8100BAA03C /* ScrollAnimationShowcaseUITests.xctest */,
131 | );
132 | name = Products;
133 | sourceTree = "";
134 | };
135 | DE1C6E3E21959F7900BAA03C /* ScrollAnimationShowcase */ = {
136 | isa = PBXGroup;
137 | children = (
138 | DE1C6E4B21959F8000BAA03C /* Info.plist */,
139 | DE1C6E3F21959F7900BAA03C /* AppDelegate.swift */,
140 | DE1C6E4621959F8000BAA03C /* Assets.xcassets */,
141 | DE1C6E6E2195A46500BAA03C /* Entity */,
142 | DE1C6E722195A4D100BAA03C /* Extension */,
143 | DE1C6E732195A4E100BAA03C /* Modules */,
144 | DE1C6E702195A47200BAA03C /* Storyboard */,
145 | DE1C6E9B21995EEC00BAA03C /* TestData */,
146 | DE1C6E752195A50200BAA03C /* View */,
147 | DE1C6E762195A50800BAA03C /* ViewController */,
148 | );
149 | path = ScrollAnimationShowcase;
150 | sourceTree = "";
151 | };
152 | DE1C6E5321959F8100BAA03C /* ScrollAnimationShowcaseTests */ = {
153 | isa = PBXGroup;
154 | children = (
155 | DE1C6E5421959F8100BAA03C /* ScrollAnimationShowcaseTests.swift */,
156 | DE1C6E5621959F8100BAA03C /* Info.plist */,
157 | );
158 | path = ScrollAnimationShowcaseTests;
159 | sourceTree = "";
160 | };
161 | DE1C6E5E21959F8100BAA03C /* ScrollAnimationShowcaseUITests */ = {
162 | isa = PBXGroup;
163 | children = (
164 | DE1C6E5F21959F8100BAA03C /* ScrollAnimationShowcaseUITests.swift */,
165 | DE1C6E6121959F8100BAA03C /* Info.plist */,
166 | );
167 | path = ScrollAnimationShowcaseUITests;
168 | sourceTree = "";
169 | };
170 | DE1C6E6E2195A46500BAA03C /* Entity */ = {
171 | isa = PBXGroup;
172 | children = (
173 | DE2553A721AA4A6200D9A3C7 /* ArticleEntity.swift */,
174 | );
175 | path = Entity;
176 | sourceTree = "";
177 | };
178 | DE1C6E702195A47200BAA03C /* Storyboard */ = {
179 | isa = PBXGroup;
180 | children = (
181 | DE1C6E86219667FA00BAA03C /* Article.storyboard */,
182 | DE1C6E4821959F8000BAA03C /* LaunchScreen.storyboard */,
183 | DE1C6E4321959F7900BAA03C /* Contents.storyboard */,
184 | );
185 | path = Storyboard;
186 | sourceTree = "";
187 | };
188 | DE1C6E722195A4D100BAA03C /* Extension */ = {
189 | isa = PBXGroup;
190 | children = (
191 | DE2553A921AA66F300D9A3C7 /* IntExtension.swift */,
192 | DE1C6E7B2196573C00BAA03C /* NSObjectProtocolExtension.swift */,
193 | DE1C6E7D219657A300BAA03C /* UICollectionViewExtension.swift */,
194 | DE1C6E792196571200BAA03C /* UIColorExtension.swift */,
195 | DE1C6E772195AD4600BAA03C /* UIViewControllerExtension.swift */,
196 | );
197 | path = Extension;
198 | sourceTree = "";
199 | };
200 | DE1C6E732195A4E100BAA03C /* Modules */ = {
201 | isa = PBXGroup;
202 | children = (
203 | DE1C6E9121967F3900BAA03C /* AppConstant.swift */,
204 | );
205 | path = Modules;
206 | sourceTree = "";
207 | };
208 | DE1C6E752195A50200BAA03C /* View */ = {
209 | isa = PBXGroup;
210 | children = (
211 | DE1C6E8121965FFA00BAA03C /* CustomViewBase.swift */,
212 | DE2553A421A942F900D9A3C7 /* ContentsDetailHeaderView */,
213 | DE1C6EA3219EFA8900BAA03C /* CategoryScrollContentsViewCell */,
214 | DE1C6E902196759000BAA03C /* CategoryScrollTabViewCell */,
215 | DE1C6E9A2196C7C100BAA03C /* CategoryScrollTabViewFlowLayout */,
216 | );
217 | path = View;
218 | sourceTree = "";
219 | };
220 | DE1C6E762195A50800BAA03C /* ViewController */ = {
221 | isa = PBXGroup;
222 | children = (
223 | DE1C6E8B219670B000BAA03C /* Article */,
224 | DE1C6E8C219670BD00BAA03C /* Contents */,
225 | );
226 | path = ViewController;
227 | sourceTree = "";
228 | };
229 | DE1C6E8B219670B000BAA03C /* Article */ = {
230 | isa = PBXGroup;
231 | children = (
232 | DE1C6E9E2199D49000BAA03C /* ScrollContents */,
233 | DE1C6E8D219671F100BAA03C /* ScrollTab */,
234 | DE1C6E892196681D00BAA03C /* ArticleViewController.swift */,
235 | );
236 | path = Article;
237 | sourceTree = "";
238 | };
239 | DE1C6E8C219670BD00BAA03C /* Contents */ = {
240 | isa = PBXGroup;
241 | children = (
242 | DE1C6E4121959F7900BAA03C /* ContentsViewController.swift */,
243 | );
244 | path = Contents;
245 | sourceTree = "";
246 | };
247 | DE1C6E8D219671F100BAA03C /* ScrollTab */ = {
248 | isa = PBXGroup;
249 | children = (
250 | DE1C6E8E2196721F00BAA03C /* CategoryScrollTabViewController.swift */,
251 | );
252 | path = ScrollTab;
253 | sourceTree = "";
254 | };
255 | DE1C6E902196759000BAA03C /* CategoryScrollTabViewCell */ = {
256 | isa = PBXGroup;
257 | children = (
258 | DE1C6E9321967F9800BAA03C /* CategoryScrollTabViewCell.swift */,
259 | DE1C6E9421967F9800BAA03C /* CategoryScrollTabViewCell.xib */,
260 | );
261 | path = CategoryScrollTabViewCell;
262 | sourceTree = "";
263 | };
264 | DE1C6E9A2196C7C100BAA03C /* CategoryScrollTabViewFlowLayout */ = {
265 | isa = PBXGroup;
266 | children = (
267 | DE1C6E982196C79300BAA03C /* CategoryScrollTabViewFlowLayout.swift */,
268 | );
269 | path = CategoryScrollTabViewFlowLayout;
270 | sourceTree = "";
271 | };
272 | DE1C6E9B21995EEC00BAA03C /* TestData */ = {
273 | isa = PBXGroup;
274 | children = (
275 | DE1C6E9C21995F0300BAA03C /* Mock */,
276 | DE1C6E9D21995F0A00BAA03C /* Stub */,
277 | );
278 | path = TestData;
279 | sourceTree = "";
280 | };
281 | DE1C6E9C21995F0300BAA03C /* Mock */ = {
282 | isa = PBXGroup;
283 | children = (
284 | DE1C6EA12199DDE700BAA03C /* ArticleMock.swift */,
285 | );
286 | path = Mock;
287 | sourceTree = "";
288 | };
289 | DE1C6E9D21995F0A00BAA03C /* Stub */ = {
290 | isa = PBXGroup;
291 | children = (
292 | );
293 | path = Stub;
294 | sourceTree = "";
295 | };
296 | DE1C6E9E2199D49000BAA03C /* ScrollContents */ = {
297 | isa = PBXGroup;
298 | children = (
299 | DE1C6E9F2199D4F800BAA03C /* CategoryScrollContentsViewController.swift */,
300 | );
301 | path = ScrollContents;
302 | sourceTree = "";
303 | };
304 | DE1C6EA3219EFA8900BAA03C /* CategoryScrollContentsViewCell */ = {
305 | isa = PBXGroup;
306 | children = (
307 | DE1C6EA4219EFE7C00BAA03C /* CategoryScrollContentsViewCell.swift */,
308 | DE1C6EA5219EFE7C00BAA03C /* CategoryScrollContentsViewCell.xib */,
309 | );
310 | path = CategoryScrollContentsViewCell;
311 | sourceTree = "";
312 | };
313 | DE2553A421A942F900D9A3C7 /* ContentsDetailHeaderView */ = {
314 | isa = PBXGroup;
315 | children = (
316 | DE2553A521A9434400D9A3C7 /* ContentsDetailHeaderView.swift */,
317 | );
318 | path = ContentsDetailHeaderView;
319 | sourceTree = "";
320 | };
321 | /* End PBXGroup section */
322 |
323 | /* Begin PBXNativeTarget section */
324 | DE1C6E3B21959F7900BAA03C /* ScrollAnimationShowcase */ = {
325 | isa = PBXNativeTarget;
326 | buildConfigurationList = DE1C6E6421959F8100BAA03C /* Build configuration list for PBXNativeTarget "ScrollAnimationShowcase" */;
327 | buildPhases = (
328 | DE1C6E3821959F7900BAA03C /* Sources */,
329 | DE1C6E3921959F7900BAA03C /* Frameworks */,
330 | DE1C6E3A21959F7900BAA03C /* Resources */,
331 | );
332 | buildRules = (
333 | );
334 | dependencies = (
335 | );
336 | name = ScrollAnimationShowcase;
337 | productName = ScrollAnimationShowcase;
338 | productReference = DE1C6E3C21959F7900BAA03C /* ScrollAnimationShowcase.app */;
339 | productType = "com.apple.product-type.application";
340 | };
341 | DE1C6E4F21959F8100BAA03C /* ScrollAnimationShowcaseTests */ = {
342 | isa = PBXNativeTarget;
343 | buildConfigurationList = DE1C6E6721959F8100BAA03C /* Build configuration list for PBXNativeTarget "ScrollAnimationShowcaseTests" */;
344 | buildPhases = (
345 | DE1C6E4C21959F8100BAA03C /* Sources */,
346 | DE1C6E4D21959F8100BAA03C /* Frameworks */,
347 | DE1C6E4E21959F8100BAA03C /* Resources */,
348 | );
349 | buildRules = (
350 | );
351 | dependencies = (
352 | DE1C6E5221959F8100BAA03C /* PBXTargetDependency */,
353 | );
354 | name = ScrollAnimationShowcaseTests;
355 | productName = ScrollAnimationShowcaseTests;
356 | productReference = DE1C6E5021959F8100BAA03C /* ScrollAnimationShowcaseTests.xctest */;
357 | productType = "com.apple.product-type.bundle.unit-test";
358 | };
359 | DE1C6E5A21959F8100BAA03C /* ScrollAnimationShowcaseUITests */ = {
360 | isa = PBXNativeTarget;
361 | buildConfigurationList = DE1C6E6A21959F8100BAA03C /* Build configuration list for PBXNativeTarget "ScrollAnimationShowcaseUITests" */;
362 | buildPhases = (
363 | DE1C6E5721959F8100BAA03C /* Sources */,
364 | DE1C6E5821959F8100BAA03C /* Frameworks */,
365 | DE1C6E5921959F8100BAA03C /* Resources */,
366 | );
367 | buildRules = (
368 | );
369 | dependencies = (
370 | DE1C6E5D21959F8100BAA03C /* PBXTargetDependency */,
371 | );
372 | name = ScrollAnimationShowcaseUITests;
373 | productName = ScrollAnimationShowcaseUITests;
374 | productReference = DE1C6E5B21959F8100BAA03C /* ScrollAnimationShowcaseUITests.xctest */;
375 | productType = "com.apple.product-type.bundle.ui-testing";
376 | };
377 | /* End PBXNativeTarget section */
378 |
379 | /* Begin PBXProject section */
380 | DE1C6E3421959F7900BAA03C /* Project object */ = {
381 | isa = PBXProject;
382 | attributes = {
383 | LastSwiftUpdateCheck = 1010;
384 | LastUpgradeCheck = 1230;
385 | ORGANIZATIONNAME = "酒井文也";
386 | TargetAttributes = {
387 | DE1C6E3B21959F7900BAA03C = {
388 | CreatedOnToolsVersion = 10.1;
389 | LastSwiftMigration = 1030;
390 | };
391 | DE1C6E4F21959F8100BAA03C = {
392 | CreatedOnToolsVersion = 10.1;
393 | LastSwiftMigration = 1030;
394 | TestTargetID = DE1C6E3B21959F7900BAA03C;
395 | };
396 | DE1C6E5A21959F8100BAA03C = {
397 | CreatedOnToolsVersion = 10.1;
398 | LastSwiftMigration = 1030;
399 | TestTargetID = DE1C6E3B21959F7900BAA03C;
400 | };
401 | };
402 | };
403 | buildConfigurationList = DE1C6E3721959F7900BAA03C /* Build configuration list for PBXProject "ScrollAnimationShowcase" */;
404 | compatibilityVersion = "Xcode 9.3";
405 | developmentRegion = en;
406 | hasScannedForEncodings = 0;
407 | knownRegions = (
408 | en,
409 | Base,
410 | );
411 | mainGroup = DE1C6E3321959F7900BAA03C;
412 | productRefGroup = DE1C6E3D21959F7900BAA03C /* Products */;
413 | projectDirPath = "";
414 | projectRoot = "";
415 | targets = (
416 | DE1C6E3B21959F7900BAA03C /* ScrollAnimationShowcase */,
417 | DE1C6E4F21959F8100BAA03C /* ScrollAnimationShowcaseTests */,
418 | DE1C6E5A21959F8100BAA03C /* ScrollAnimationShowcaseUITests */,
419 | );
420 | };
421 | /* End PBXProject section */
422 |
423 | /* Begin PBXResourcesBuildPhase section */
424 | DE1C6E3A21959F7900BAA03C /* Resources */ = {
425 | isa = PBXResourcesBuildPhase;
426 | buildActionMask = 2147483647;
427 | files = (
428 | DE1C6E9621967F9800BAA03C /* CategoryScrollTabViewCell.xib in Resources */,
429 | DE1C6E4A21959F8000BAA03C /* LaunchScreen.storyboard in Resources */,
430 | DE1C6E88219667FA00BAA03C /* Article.storyboard in Resources */,
431 | DE1C6E4721959F8000BAA03C /* Assets.xcassets in Resources */,
432 | DE1C6E4521959F7900BAA03C /* Contents.storyboard in Resources */,
433 | DE1C6EA7219EFE7D00BAA03C /* CategoryScrollContentsViewCell.xib in Resources */,
434 | );
435 | runOnlyForDeploymentPostprocessing = 0;
436 | };
437 | DE1C6E4E21959F8100BAA03C /* Resources */ = {
438 | isa = PBXResourcesBuildPhase;
439 | buildActionMask = 2147483647;
440 | files = (
441 | );
442 | runOnlyForDeploymentPostprocessing = 0;
443 | };
444 | DE1C6E5921959F8100BAA03C /* Resources */ = {
445 | isa = PBXResourcesBuildPhase;
446 | buildActionMask = 2147483647;
447 | files = (
448 | );
449 | runOnlyForDeploymentPostprocessing = 0;
450 | };
451 | /* End PBXResourcesBuildPhase section */
452 |
453 | /* Begin PBXSourcesBuildPhase section */
454 | DE1C6E3821959F7900BAA03C /* Sources */ = {
455 | isa = PBXSourcesBuildPhase;
456 | buildActionMask = 2147483647;
457 | files = (
458 | DE1C6E8221965FFA00BAA03C /* CustomViewBase.swift in Sources */,
459 | DE2553A821AA4A6200D9A3C7 /* ArticleEntity.swift in Sources */,
460 | DE1C6E9221967F3900BAA03C /* AppConstant.swift in Sources */,
461 | DE1C6E4221959F7900BAA03C /* ContentsViewController.swift in Sources */,
462 | DE1C6E4021959F7900BAA03C /* AppDelegate.swift in Sources */,
463 | DE1C6E782195AD4600BAA03C /* UIViewControllerExtension.swift in Sources */,
464 | DE1C6EA6219EFE7D00BAA03C /* CategoryScrollContentsViewCell.swift in Sources */,
465 | DE1C6EA22199DDE700BAA03C /* ArticleMock.swift in Sources */,
466 | DE1C6E8A2196681D00BAA03C /* ArticleViewController.swift in Sources */,
467 | DE1C6E7A2196571200BAA03C /* UIColorExtension.swift in Sources */,
468 | DE2553AA21AA66F300D9A3C7 /* IntExtension.swift in Sources */,
469 | DE1C6EA02199D4F800BAA03C /* CategoryScrollContentsViewController.swift in Sources */,
470 | DE1C6E8F2196721F00BAA03C /* CategoryScrollTabViewController.swift in Sources */,
471 | DE1C6E7E219657A300BAA03C /* UICollectionViewExtension.swift in Sources */,
472 | DE2553A621A9434400D9A3C7 /* ContentsDetailHeaderView.swift in Sources */,
473 | DE1C6E9521967F9800BAA03C /* CategoryScrollTabViewCell.swift in Sources */,
474 | DE1C6E992196C79300BAA03C /* CategoryScrollTabViewFlowLayout.swift in Sources */,
475 | DE1C6E7C2196573C00BAA03C /* NSObjectProtocolExtension.swift in Sources */,
476 | );
477 | runOnlyForDeploymentPostprocessing = 0;
478 | };
479 | DE1C6E4C21959F8100BAA03C /* Sources */ = {
480 | isa = PBXSourcesBuildPhase;
481 | buildActionMask = 2147483647;
482 | files = (
483 | DE1C6E5521959F8100BAA03C /* ScrollAnimationShowcaseTests.swift in Sources */,
484 | );
485 | runOnlyForDeploymentPostprocessing = 0;
486 | };
487 | DE1C6E5721959F8100BAA03C /* Sources */ = {
488 | isa = PBXSourcesBuildPhase;
489 | buildActionMask = 2147483647;
490 | files = (
491 | DE1C6E6021959F8100BAA03C /* ScrollAnimationShowcaseUITests.swift in Sources */,
492 | );
493 | runOnlyForDeploymentPostprocessing = 0;
494 | };
495 | /* End PBXSourcesBuildPhase section */
496 |
497 | /* Begin PBXTargetDependency section */
498 | DE1C6E5221959F8100BAA03C /* PBXTargetDependency */ = {
499 | isa = PBXTargetDependency;
500 | target = DE1C6E3B21959F7900BAA03C /* ScrollAnimationShowcase */;
501 | targetProxy = DE1C6E5121959F8100BAA03C /* PBXContainerItemProxy */;
502 | };
503 | DE1C6E5D21959F8100BAA03C /* PBXTargetDependency */ = {
504 | isa = PBXTargetDependency;
505 | target = DE1C6E3B21959F7900BAA03C /* ScrollAnimationShowcase */;
506 | targetProxy = DE1C6E5C21959F8100BAA03C /* PBXContainerItemProxy */;
507 | };
508 | /* End PBXTargetDependency section */
509 |
510 | /* Begin PBXVariantGroup section */
511 | DE1C6E4321959F7900BAA03C /* Contents.storyboard */ = {
512 | isa = PBXVariantGroup;
513 | children = (
514 | DE1C6E4421959F7900BAA03C /* Base */,
515 | );
516 | name = Contents.storyboard;
517 | sourceTree = "";
518 | };
519 | DE1C6E4821959F8000BAA03C /* LaunchScreen.storyboard */ = {
520 | isa = PBXVariantGroup;
521 | children = (
522 | DE1C6E4921959F8000BAA03C /* Base */,
523 | );
524 | name = LaunchScreen.storyboard;
525 | sourceTree = "";
526 | };
527 | DE1C6E86219667FA00BAA03C /* Article.storyboard */ = {
528 | isa = PBXVariantGroup;
529 | children = (
530 | DE1C6E87219667FA00BAA03C /* Base */,
531 | );
532 | name = Article.storyboard;
533 | sourceTree = "";
534 | };
535 | /* End PBXVariantGroup section */
536 |
537 | /* Begin XCBuildConfiguration section */
538 | DE1C6E6221959F8100BAA03C /* Debug */ = {
539 | isa = XCBuildConfiguration;
540 | buildSettings = {
541 | ALWAYS_SEARCH_USER_PATHS = NO;
542 | CLANG_ANALYZER_NONNULL = YES;
543 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
544 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
545 | CLANG_CXX_LIBRARY = "libc++";
546 | CLANG_ENABLE_MODULES = YES;
547 | CLANG_ENABLE_OBJC_ARC = YES;
548 | CLANG_ENABLE_OBJC_WEAK = YES;
549 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
550 | CLANG_WARN_BOOL_CONVERSION = YES;
551 | CLANG_WARN_COMMA = YES;
552 | CLANG_WARN_CONSTANT_CONVERSION = YES;
553 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
554 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
555 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
556 | CLANG_WARN_EMPTY_BODY = YES;
557 | CLANG_WARN_ENUM_CONVERSION = YES;
558 | CLANG_WARN_INFINITE_RECURSION = YES;
559 | CLANG_WARN_INT_CONVERSION = YES;
560 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
561 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
562 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
563 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
564 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
565 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
566 | CLANG_WARN_STRICT_PROTOTYPES = YES;
567 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
568 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
569 | CLANG_WARN_UNREACHABLE_CODE = YES;
570 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
571 | CODE_SIGN_IDENTITY = "iPhone Developer";
572 | COPY_PHASE_STRIP = NO;
573 | DEBUG_INFORMATION_FORMAT = dwarf;
574 | ENABLE_STRICT_OBJC_MSGSEND = YES;
575 | ENABLE_TESTABILITY = YES;
576 | GCC_C_LANGUAGE_STANDARD = gnu11;
577 | GCC_DYNAMIC_NO_PIC = NO;
578 | GCC_NO_COMMON_BLOCKS = YES;
579 | GCC_OPTIMIZATION_LEVEL = 0;
580 | GCC_PREPROCESSOR_DEFINITIONS = (
581 | "DEBUG=1",
582 | "$(inherited)",
583 | );
584 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
585 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
586 | GCC_WARN_UNDECLARED_SELECTOR = YES;
587 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
588 | GCC_WARN_UNUSED_FUNCTION = YES;
589 | GCC_WARN_UNUSED_VARIABLE = YES;
590 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
591 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
592 | MTL_FAST_MATH = YES;
593 | ONLY_ACTIVE_ARCH = YES;
594 | SDKROOT = iphoneos;
595 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
596 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
597 | };
598 | name = Debug;
599 | };
600 | DE1C6E6321959F8100BAA03C /* Release */ = {
601 | isa = XCBuildConfiguration;
602 | buildSettings = {
603 | ALWAYS_SEARCH_USER_PATHS = NO;
604 | CLANG_ANALYZER_NONNULL = YES;
605 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
606 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
607 | CLANG_CXX_LIBRARY = "libc++";
608 | CLANG_ENABLE_MODULES = YES;
609 | CLANG_ENABLE_OBJC_ARC = YES;
610 | CLANG_ENABLE_OBJC_WEAK = YES;
611 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
612 | CLANG_WARN_BOOL_CONVERSION = YES;
613 | CLANG_WARN_COMMA = YES;
614 | CLANG_WARN_CONSTANT_CONVERSION = YES;
615 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
616 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
617 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
618 | CLANG_WARN_EMPTY_BODY = YES;
619 | CLANG_WARN_ENUM_CONVERSION = YES;
620 | CLANG_WARN_INFINITE_RECURSION = YES;
621 | CLANG_WARN_INT_CONVERSION = YES;
622 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
623 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
624 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
625 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
626 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
627 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
628 | CLANG_WARN_STRICT_PROTOTYPES = YES;
629 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
630 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
631 | CLANG_WARN_UNREACHABLE_CODE = YES;
632 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
633 | CODE_SIGN_IDENTITY = "iPhone Developer";
634 | COPY_PHASE_STRIP = NO;
635 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
636 | ENABLE_NS_ASSERTIONS = NO;
637 | ENABLE_STRICT_OBJC_MSGSEND = YES;
638 | GCC_C_LANGUAGE_STANDARD = gnu11;
639 | GCC_NO_COMMON_BLOCKS = YES;
640 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
641 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
642 | GCC_WARN_UNDECLARED_SELECTOR = YES;
643 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
644 | GCC_WARN_UNUSED_FUNCTION = YES;
645 | GCC_WARN_UNUSED_VARIABLE = YES;
646 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
647 | MTL_ENABLE_DEBUG_INFO = NO;
648 | MTL_FAST_MATH = YES;
649 | SDKROOT = iphoneos;
650 | SWIFT_COMPILATION_MODE = wholemodule;
651 | SWIFT_OPTIMIZATION_LEVEL = "-O";
652 | VALIDATE_PRODUCT = YES;
653 | };
654 | name = Release;
655 | };
656 | DE1C6E6521959F8100BAA03C /* Debug */ = {
657 | isa = XCBuildConfiguration;
658 | buildSettings = {
659 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
660 | CODE_SIGN_STYLE = Automatic;
661 | DEVELOPMENT_TEAM = S5BF5553KY;
662 | INFOPLIST_FILE = ScrollAnimationShowcase/Info.plist;
663 | LD_RUNPATH_SEARCH_PATHS = (
664 | "$(inherited)",
665 | "@executable_path/Frameworks",
666 | );
667 | PRODUCT_BUNDLE_IDENTIFIER = com.just1factory.ScrollAnimationShowcase;
668 | PRODUCT_NAME = "$(TARGET_NAME)";
669 | SWIFT_VERSION = 5.0;
670 | TARGETED_DEVICE_FAMILY = "1,2";
671 | };
672 | name = Debug;
673 | };
674 | DE1C6E6621959F8100BAA03C /* Release */ = {
675 | isa = XCBuildConfiguration;
676 | buildSettings = {
677 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
678 | CODE_SIGN_STYLE = Automatic;
679 | DEVELOPMENT_TEAM = S5BF5553KY;
680 | INFOPLIST_FILE = ScrollAnimationShowcase/Info.plist;
681 | LD_RUNPATH_SEARCH_PATHS = (
682 | "$(inherited)",
683 | "@executable_path/Frameworks",
684 | );
685 | PRODUCT_BUNDLE_IDENTIFIER = com.just1factory.ScrollAnimationShowcase;
686 | PRODUCT_NAME = "$(TARGET_NAME)";
687 | SWIFT_VERSION = 5.0;
688 | TARGETED_DEVICE_FAMILY = "1,2";
689 | };
690 | name = Release;
691 | };
692 | DE1C6E6821959F8100BAA03C /* Debug */ = {
693 | isa = XCBuildConfiguration;
694 | buildSettings = {
695 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
696 | BUNDLE_LOADER = "$(TEST_HOST)";
697 | CODE_SIGN_STYLE = Automatic;
698 | DEVELOPMENT_TEAM = S5BF5553KY;
699 | INFOPLIST_FILE = ScrollAnimationShowcaseTests/Info.plist;
700 | LD_RUNPATH_SEARCH_PATHS = (
701 | "$(inherited)",
702 | "@executable_path/Frameworks",
703 | "@loader_path/Frameworks",
704 | );
705 | PRODUCT_BUNDLE_IDENTIFIER = com.just1factory.ScrollAnimationShowcaseTests;
706 | PRODUCT_NAME = "$(TARGET_NAME)";
707 | SWIFT_VERSION = 5.0;
708 | TARGETED_DEVICE_FAMILY = "1,2";
709 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ScrollAnimationShowcase.app/ScrollAnimationShowcase";
710 | };
711 | name = Debug;
712 | };
713 | DE1C6E6921959F8100BAA03C /* Release */ = {
714 | isa = XCBuildConfiguration;
715 | buildSettings = {
716 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
717 | BUNDLE_LOADER = "$(TEST_HOST)";
718 | CODE_SIGN_STYLE = Automatic;
719 | DEVELOPMENT_TEAM = S5BF5553KY;
720 | INFOPLIST_FILE = ScrollAnimationShowcaseTests/Info.plist;
721 | LD_RUNPATH_SEARCH_PATHS = (
722 | "$(inherited)",
723 | "@executable_path/Frameworks",
724 | "@loader_path/Frameworks",
725 | );
726 | PRODUCT_BUNDLE_IDENTIFIER = com.just1factory.ScrollAnimationShowcaseTests;
727 | PRODUCT_NAME = "$(TARGET_NAME)";
728 | SWIFT_VERSION = 5.0;
729 | TARGETED_DEVICE_FAMILY = "1,2";
730 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ScrollAnimationShowcase.app/ScrollAnimationShowcase";
731 | };
732 | name = Release;
733 | };
734 | DE1C6E6B21959F8100BAA03C /* Debug */ = {
735 | isa = XCBuildConfiguration;
736 | buildSettings = {
737 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
738 | CODE_SIGN_STYLE = Automatic;
739 | DEVELOPMENT_TEAM = S5BF5553KY;
740 | INFOPLIST_FILE = ScrollAnimationShowcaseUITests/Info.plist;
741 | LD_RUNPATH_SEARCH_PATHS = (
742 | "$(inherited)",
743 | "@executable_path/Frameworks",
744 | "@loader_path/Frameworks",
745 | );
746 | PRODUCT_BUNDLE_IDENTIFIER = com.just1factory.ScrollAnimationShowcaseUITests;
747 | PRODUCT_NAME = "$(TARGET_NAME)";
748 | SWIFT_VERSION = 5.0;
749 | TARGETED_DEVICE_FAMILY = "1,2";
750 | TEST_TARGET_NAME = ScrollAnimationShowcase;
751 | };
752 | name = Debug;
753 | };
754 | DE1C6E6C21959F8100BAA03C /* Release */ = {
755 | isa = XCBuildConfiguration;
756 | buildSettings = {
757 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
758 | CODE_SIGN_STYLE = Automatic;
759 | DEVELOPMENT_TEAM = S5BF5553KY;
760 | INFOPLIST_FILE = ScrollAnimationShowcaseUITests/Info.plist;
761 | LD_RUNPATH_SEARCH_PATHS = (
762 | "$(inherited)",
763 | "@executable_path/Frameworks",
764 | "@loader_path/Frameworks",
765 | );
766 | PRODUCT_BUNDLE_IDENTIFIER = com.just1factory.ScrollAnimationShowcaseUITests;
767 | PRODUCT_NAME = "$(TARGET_NAME)";
768 | SWIFT_VERSION = 5.0;
769 | TARGETED_DEVICE_FAMILY = "1,2";
770 | TEST_TARGET_NAME = ScrollAnimationShowcase;
771 | };
772 | name = Release;
773 | };
774 | /* End XCBuildConfiguration section */
775 |
776 | /* Begin XCConfigurationList section */
777 | DE1C6E3721959F7900BAA03C /* Build configuration list for PBXProject "ScrollAnimationShowcase" */ = {
778 | isa = XCConfigurationList;
779 | buildConfigurations = (
780 | DE1C6E6221959F8100BAA03C /* Debug */,
781 | DE1C6E6321959F8100BAA03C /* Release */,
782 | );
783 | defaultConfigurationIsVisible = 0;
784 | defaultConfigurationName = Release;
785 | };
786 | DE1C6E6421959F8100BAA03C /* Build configuration list for PBXNativeTarget "ScrollAnimationShowcase" */ = {
787 | isa = XCConfigurationList;
788 | buildConfigurations = (
789 | DE1C6E6521959F8100BAA03C /* Debug */,
790 | DE1C6E6621959F8100BAA03C /* Release */,
791 | );
792 | defaultConfigurationIsVisible = 0;
793 | defaultConfigurationName = Release;
794 | };
795 | DE1C6E6721959F8100BAA03C /* Build configuration list for PBXNativeTarget "ScrollAnimationShowcaseTests" */ = {
796 | isa = XCConfigurationList;
797 | buildConfigurations = (
798 | DE1C6E6821959F8100BAA03C /* Debug */,
799 | DE1C6E6921959F8100BAA03C /* Release */,
800 | );
801 | defaultConfigurationIsVisible = 0;
802 | defaultConfigurationName = Release;
803 | };
804 | DE1C6E6A21959F8100BAA03C /* Build configuration list for PBXNativeTarget "ScrollAnimationShowcaseUITests" */ = {
805 | isa = XCConfigurationList;
806 | buildConfigurations = (
807 | DE1C6E6B21959F8100BAA03C /* Debug */,
808 | DE1C6E6C21959F8100BAA03C /* Release */,
809 | );
810 | defaultConfigurationIsVisible = 0;
811 | defaultConfigurationName = Release;
812 | };
813 | /* End XCConfigurationList section */
814 | };
815 | rootObject = DE1C6E3421959F7900BAA03C /* Project object */;
816 | }
817 |
--------------------------------------------------------------------------------
/ScrollAnimationShowcase/Storyboard/Base.lproj/Contents.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
43 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
86 |
106 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
146 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
339 |
340 |
341 |
--------------------------------------------------------------------------------