├── .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 | ![capture.jpg](https://qiita-image-store.s3.amazonaws.com/0/17400/aa4c7d31-6d45-41db-bb1f-4d2a774a972f.jpeg) 16 | 17 | __2. Storyboardの構成:__ 18 | 19 | ![infinite_tab_storyboard.png](https://qiita-image-store.s3.amazonaws.com/0/17400/d586b288-9db3-21c5-d7f4-c9d821c7cbdb.png) 20 | 21 | __3. 該当箇所の全体的なポイントをまとめた概略図:__ 22 | 23 | ![whole_relationships.png](https://camo.qiitausercontent.com/0a5fa9a8a475d1f1bdf670beff579004c60433a1/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f31373430302f62653239333833662d643935642d363564312d373937342d3539316632663766386639352e706e67) 24 | 25 | ### UICollectionViewやUIScrollViewを有効活用する 26 | 27 | このサンプルでは、UICollectionViewやUIScrollViewの性質や各種Delegateの処理を活用してUI表現をしています。特に表現を実現する前段階において押さえておくと良さそうな部分についてまとめています。 28 | 29 | __1. UICollectionViewFlowLayoutを継承したクラスを適用する:__ 30 | 31 | ![uicollectionview_layout_atrributes.png](https://qiita-image-store.s3.amazonaws.com/0/17400/b62b8cf1-3bb4-e732-b909-8b51f2a13586.png) 32 | 33 | __2. 無限スクロールを伴うタブ型UI実装する上で必要なセルのインデックス値の調整する:__ 34 | 35 | ![uicollectionview_calculate_index.png](https://qiita-image-store.s3.amazonaws.com/0/17400/fe84d355-f840-b086-344a-736e633d3753.png) 36 | 37 | ### その他コードにおいてポイントとなる部分の実装 38 | 39 | 具体的な実装においてポイントになる部分については、下図に示した部分になります。 40 | 41 | __1. インデックス値を調整するための実装:__ 42 | 43 | ![point1.png](https://qiita-image-store.s3.amazonaws.com/0/17400/d7a36833-8bb1-3fc9-221f-bda57dffaf76.png) 44 | 45 | __2. 配置したUICollectionViewのoffset値を調整するための実装:__ 46 | 47 | ![point2.png](https://qiita-image-store.s3.amazonaws.com/0/17400/58b1edbd-27b4-bc0c-5f60-4c31a17dbedf.png) 48 | 49 | __3. UICollectionViewCellのインデックス値の変更の前後状態を元にUIPageViewControllerの動き方を決定するための実装:__ 50 | 51 | ![point3.png](https://qiita-image-store.s3.amazonaws.com/0/17400/a6000f9a-192b-bfef-90c9-2c8f8f62737c.png) 52 | 53 | __4. 配置したUICollectionViewのスクロールが停止した際の表示位置を調整するための実装:__ 54 | 55 | ![point4.png](https://qiita-image-store.s3.amazonaws.com/0/17400/c7beb155-f930-2aed-61da-3e45d33e9813.png) 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 | --------------------------------------------------------------------------------