├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Chausie.podspec ├── Chausie.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcshareddata │ └── xcschemes │ │ └── Chausie.xcscheme └── xcuserdata │ └── a14770.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── Chausie ├── Chausie.h ├── Info.plist ├── PageViewController.swift ├── PageViewControllerDelegate.swift ├── Pageable.swift ├── Position.swift ├── TabItemCell.swift ├── TabPageComposer.swift ├── TabPageViewController.swift ├── TabPageViewControllerDelegate.swift ├── TabView.swift └── TabViewLayout.swift ├── ChausieTests ├── Info.plist ├── Mocks │ ├── MockPagebableViewController.swift │ └── MockTabCell.swift └── Tests │ ├── PageViewControllerDelegateTests.swift │ ├── PageViewControllerTests.swift │ ├── PageableTests.swift │ ├── TabPageViewControllerDelegateTests.swift │ ├── TabPageViewControllerTests.swift │ └── TabViewTests.swift ├── Examples └── ChausieExample │ ├── ChausieExample.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ ├── ChausieExample │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── Category.swift │ ├── Chausie Components │ │ ├── ProgressBorderTabPageComposer.swift │ │ ├── ProgressBorderTabView.swift │ │ └── TabItemButton.swift │ ├── Extensions │ │ ├── TabPageViewController+Utility.swift │ │ └── TabViewLayout+Utility.swift │ ├── ImageDownloader.swift │ ├── Info.plist │ ├── URLBuilder.swift │ ├── ViewControllers │ │ ├── CollectionViewController.swift │ │ ├── HasCategory.swift │ │ ├── RootViewController.swift │ │ └── TableViewController.swift │ └── Views │ │ ├── ImageCollectionViewCell.swift │ │ ├── ImageCollectionViewCell.xib │ │ ├── ImageTableViewCell.swift │ │ └── ImageTableViewCell.xib │ ├── Makefile │ └── README.md ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Makefile ├── README.md └── codecov.yml /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Xcode 5 | build/ 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata/ 15 | *.xccheckout 16 | profile 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.ipa 21 | 22 | # Bundler 23 | .bundle 24 | vendor/bundle 25 | 26 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 27 | # Carthage/Checkouts 28 | 29 | Carthage 30 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | os: osx 3 | osx_image: xcode10.2 4 | before_install: 5 | - gem install xcpretty 6 | before_script: 7 | - set -o pipefail 8 | script: make test 9 | after_success: 10 | - bash <(curl -s https://codecov.io/bash) 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Master 2 | 3 | ##### Breaking 4 | 5 | * None. 6 | 7 | ##### Enhancements 8 | 9 | * None. 10 | 11 | ##### Bug Fixes 12 | 13 | * None. 14 | 15 | ## [0.1.8](https://github.com/cats-oss/Chausie/releases/tag/0.1.8) 16 | 17 | ##### Breaking 18 | 19 | * None. 20 | 21 | ##### Enhancements 22 | 23 | * [Adds condition](https://github.com/cats-oss/Chausie/commit/330676049092bf96ed601e92966613b9ceaf5f1a) 24 | 25 | ##### Bug Fixes 26 | 27 | * None. 28 | 29 | ## [0.1.7](https://github.com/cats-oss/Chausie/releases/tag/0.1.7) 30 | 31 | ##### Breaking 32 | 33 | * None. 34 | 35 | ##### Enhancements 36 | 37 | * [Fix typo](https://github.com/cats-oss/Chausie/commit/3a70c1039a246f0ee3ee2b797454dcfdbe301da6) 38 | 39 | ##### Bug Fixes 40 | 41 | * None. 42 | 43 | ## [0.1.6](https://github.com/cats-oss/Chausie/releases/tag/0.1.6) 44 | 45 | ##### Breaking 46 | 47 | * None. 48 | 49 | ##### Enhancements 50 | 51 | * [Adds 'visibleViewControllers'](https://github.com/cats-oss/Chausie/commit/079f2a4f487ec020923f3a89a74faa588124e447) 52 | 53 | ##### Bug Fixes 54 | 55 | * None. 56 | 57 | ## [0.1.5](https://github.com/cats-oss/Chausie/releases/tag/0.1.5) 58 | 59 | ##### Breaking 60 | 61 | * None. 62 | 63 | ##### Enhancements 64 | 65 | * None. 66 | 67 | ##### Bug Fixes 68 | 69 | * [Fix layout bug after device rotation](https://github.com/cats-oss/Chausie/pull/6) 70 | 71 | ## [0.1.4](https://github.com/cats-oss/Chausie/releases/tag/0.1.4) 72 | 73 | ##### Breaking 74 | 75 | * None. 76 | 77 | ##### Enhancements 78 | 79 | * [Adds 'panGestureRecognizer'](https://github.com/cats-oss/Chausie/pull/5/commits/5e9945263ab61840d19cd26c749e87933c70e3d4) 80 | * [Adds 'TabPageViewControllerDelegate'](https://github.com/cats-oss/Chausie/pull/5/commits/f0a568483627bf83f8457936d52082ad5ff87b91) 81 | 82 | ##### Bug Fixes 83 | 84 | * None. 85 | 86 | ## [0.1.3](https://github.com/cats-oss/Chausie/releases/tag/0.1.3) 87 | 88 | ##### Breaking 89 | 90 | * None. 91 | 92 | ##### Enhancements 93 | 94 | * None. 95 | 96 | ##### Bug Fixes 97 | 98 | * [Fix pagebable method behavior](https://github.com/cats-oss/Chausie/commit/120d55f5a52dd914a273c4846a19c2c4da9cfa9f) 99 | 100 | ## [0.1.2](https://github.com/cats-oss/Chausie/releases/tag/0.1.2) 101 | 102 | ##### Breaking 103 | 104 | * None. 105 | 106 | ##### Enhancements 107 | 108 | * [Change access level ](https://github.com/cats-oss/Chausie/commit/ddf88edbe8470653792e20b5ca127dabcafda3c1) 109 | * [Update cocoapods](https://github.com/cats-oss/Chausie/commit/f0088a7f3dbbf9c67d2f65497dbc6154277a568d) 110 | 111 | ##### Bug Fixes 112 | 113 | * None. 114 | 115 | ## [0.1.1](https://github.com/cats-oss/Chausie/releases/tag/0.1.1) 116 | 117 | ##### Breaking 118 | 119 | * None. 120 | 121 | ##### Enhancements 122 | 123 | * Support CocoaPod #3 124 | 125 | ##### Bug Fixes 126 | 127 | * None. 128 | -------------------------------------------------------------------------------- /Chausie.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'Chausie' 3 | s.version = '0.1.8' 4 | s.summary = 'Chausie provides a customizable view containers that manages navigation between pages of content. :cat:' 5 | s.homepage = 'https://github.com/cats-oss/Chausie' 6 | s.license = { :type => 'MIT', :file => 'LICENSE' } 7 | s.author = { 'shoheiyokoyama' => 'shohei.yok0602@gmail.com' } 8 | s.source = { :git => 'https://github.com/cats-oss/Chausie.git', :tag => s.version.to_s } 9 | s.ios.deployment_target = '11.0' 10 | s.source_files = 'Chausie/**/*.swift' 11 | s.swift_version = '5.0' 12 | end 13 | -------------------------------------------------------------------------------- /Chausie.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | E04FEB4322781C4A0084666D /* Chausie.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E04FEB3922781C490084666D /* Chausie.framework */; }; 11 | E04FEB4A22781C4A0084666D /* Chausie.h in Headers */ = {isa = PBXBuildFile; fileRef = E04FEB3C22781C490084666D /* Chausie.h */; settings = {ATTRIBUTES = (Public, ); }; }; 12 | E04FEB5C22781CD00084666D /* TabPageComposer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E04FEB5322781CD00084666D /* TabPageComposer.swift */; }; 13 | E04FEB5D22781CD00084666D /* TabPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E04FEB5422781CD00084666D /* TabPageViewController.swift */; }; 14 | E04FEB5E22781CD00084666D /* Position.swift in Sources */ = {isa = PBXBuildFile; fileRef = E04FEB5522781CD00084666D /* Position.swift */; }; 15 | E04FEB5F22781CD00084666D /* PageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E04FEB5622781CD00084666D /* PageViewController.swift */; }; 16 | E04FEB6022781CD00084666D /* TabViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = E04FEB5722781CD00084666D /* TabViewLayout.swift */; }; 17 | E04FEB6122781CD00084666D /* TabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E04FEB5822781CD00084666D /* TabView.swift */; }; 18 | E04FEB6222781CD00084666D /* PageViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E04FEB5922781CD00084666D /* PageViewControllerDelegate.swift */; }; 19 | E04FEB6322781CD00084666D /* Pageable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E04FEB5A22781CD00084666D /* Pageable.swift */; }; 20 | E04FEB6422781CD00084666D /* TabItemCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E04FEB5B22781CD00084666D /* TabItemCell.swift */; }; 21 | E04FEB6722781DCF0084666D /* MockPagebableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E04FEB6622781DCF0084666D /* MockPagebableViewController.swift */; }; 22 | E04FEB78227821F30084666D /* PageableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E04FEB73227821F30084666D /* PageableTests.swift */; }; 23 | E04FEB79227821F30084666D /* TabViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E04FEB74227821F30084666D /* TabViewTests.swift */; }; 24 | E04FEB7A227821F30084666D /* PageViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E04FEB75227821F30084666D /* PageViewControllerTests.swift */; }; 25 | E04FEB7B227821F30084666D /* PageViewControllerDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E04FEB76227821F30084666D /* PageViewControllerDelegateTests.swift */; }; 26 | E04FEB7C227821F30084666D /* TabPageViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E04FEB77227821F30084666D /* TabPageViewControllerTests.swift */; }; 27 | E09C252A22F800D800D29AC9 /* TabPageViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E09C252922F800D800D29AC9 /* TabPageViewControllerDelegate.swift */; }; 28 | E09C252C22F802FF00D29AC9 /* TabPageViewControllerDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E09C252B22F802FF00D29AC9 /* TabPageViewControllerDelegateTests.swift */; }; 29 | E09C252E22F8062F00D29AC9 /* MockTabCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E09C252D22F8062F00D29AC9 /* MockTabCell.swift */; }; 30 | /* End PBXBuildFile section */ 31 | 32 | /* Begin PBXContainerItemProxy section */ 33 | E04FEB4422781C4A0084666D /* PBXContainerItemProxy */ = { 34 | isa = PBXContainerItemProxy; 35 | containerPortal = E04FEB3022781C490084666D /* Project object */; 36 | proxyType = 1; 37 | remoteGlobalIDString = E04FEB3822781C490084666D; 38 | remoteInfo = Chausie; 39 | }; 40 | /* End PBXContainerItemProxy section */ 41 | 42 | /* Begin PBXFileReference section */ 43 | E04FEB3922781C490084666D /* Chausie.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Chausie.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 44 | E04FEB3C22781C490084666D /* Chausie.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Chausie.h; sourceTree = ""; }; 45 | E04FEB3D22781C490084666D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 46 | E04FEB4222781C4A0084666D /* ChausieTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ChausieTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 47 | E04FEB4922781C4A0084666D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 48 | E04FEB5322781CD00084666D /* TabPageComposer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabPageComposer.swift; sourceTree = ""; }; 49 | E04FEB5422781CD00084666D /* TabPageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabPageViewController.swift; sourceTree = ""; }; 50 | E04FEB5522781CD00084666D /* Position.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Position.swift; sourceTree = ""; }; 51 | E04FEB5622781CD00084666D /* PageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PageViewController.swift; sourceTree = ""; }; 52 | E04FEB5722781CD00084666D /* TabViewLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabViewLayout.swift; sourceTree = ""; }; 53 | E04FEB5822781CD00084666D /* TabView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabView.swift; sourceTree = ""; }; 54 | E04FEB5922781CD00084666D /* PageViewControllerDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PageViewControllerDelegate.swift; sourceTree = ""; }; 55 | E04FEB5A22781CD00084666D /* Pageable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Pageable.swift; sourceTree = ""; }; 56 | E04FEB5B22781CD00084666D /* TabItemCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabItemCell.swift; sourceTree = ""; }; 57 | E04FEB6622781DCF0084666D /* MockPagebableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockPagebableViewController.swift; sourceTree = ""; }; 58 | E04FEB73227821F30084666D /* PageableTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PageableTests.swift; sourceTree = ""; }; 59 | E04FEB74227821F30084666D /* TabViewTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabViewTests.swift; sourceTree = ""; }; 60 | E04FEB75227821F30084666D /* PageViewControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PageViewControllerTests.swift; sourceTree = ""; }; 61 | E04FEB76227821F30084666D /* PageViewControllerDelegateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PageViewControllerDelegateTests.swift; sourceTree = ""; }; 62 | E04FEB77227821F30084666D /* TabPageViewControllerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabPageViewControllerTests.swift; sourceTree = ""; }; 63 | E09C252922F800D800D29AC9 /* TabPageViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabPageViewControllerDelegate.swift; sourceTree = ""; }; 64 | E09C252B22F802FF00D29AC9 /* TabPageViewControllerDelegateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabPageViewControllerDelegateTests.swift; sourceTree = ""; }; 65 | E09C252D22F8062F00D29AC9 /* MockTabCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTabCell.swift; sourceTree = ""; }; 66 | /* End PBXFileReference section */ 67 | 68 | /* Begin PBXFrameworksBuildPhase section */ 69 | E04FEB3622781C490084666D /* Frameworks */ = { 70 | isa = PBXFrameworksBuildPhase; 71 | buildActionMask = 2147483647; 72 | files = ( 73 | ); 74 | runOnlyForDeploymentPostprocessing = 0; 75 | }; 76 | E04FEB3F22781C4A0084666D /* Frameworks */ = { 77 | isa = PBXFrameworksBuildPhase; 78 | buildActionMask = 2147483647; 79 | files = ( 80 | E04FEB4322781C4A0084666D /* Chausie.framework in Frameworks */, 81 | ); 82 | runOnlyForDeploymentPostprocessing = 0; 83 | }; 84 | /* End PBXFrameworksBuildPhase section */ 85 | 86 | /* Begin PBXGroup section */ 87 | E04FEB2F22781C490084666D = { 88 | isa = PBXGroup; 89 | children = ( 90 | E04FEB3B22781C490084666D /* Chausie */, 91 | E04FEB4622781C4A0084666D /* ChausieTests */, 92 | E04FEB3A22781C490084666D /* Products */, 93 | ); 94 | sourceTree = ""; 95 | }; 96 | E04FEB3A22781C490084666D /* Products */ = { 97 | isa = PBXGroup; 98 | children = ( 99 | E04FEB3922781C490084666D /* Chausie.framework */, 100 | E04FEB4222781C4A0084666D /* ChausieTests.xctest */, 101 | ); 102 | name = Products; 103 | sourceTree = ""; 104 | }; 105 | E04FEB3B22781C490084666D /* Chausie */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | E04FEB5A22781CD00084666D /* Pageable.swift */, 109 | E04FEB5622781CD00084666D /* PageViewController.swift */, 110 | E04FEB5922781CD00084666D /* PageViewControllerDelegate.swift */, 111 | E04FEB5522781CD00084666D /* Position.swift */, 112 | E04FEB5B22781CD00084666D /* TabItemCell.swift */, 113 | E04FEB5322781CD00084666D /* TabPageComposer.swift */, 114 | E04FEB5422781CD00084666D /* TabPageViewController.swift */, 115 | E09C252922F800D800D29AC9 /* TabPageViewControllerDelegate.swift */, 116 | E04FEB5822781CD00084666D /* TabView.swift */, 117 | E04FEB5722781CD00084666D /* TabViewLayout.swift */, 118 | E04FEB3C22781C490084666D /* Chausie.h */, 119 | E04FEB3D22781C490084666D /* Info.plist */, 120 | ); 121 | path = Chausie; 122 | sourceTree = ""; 123 | }; 124 | E04FEB4622781C4A0084666D /* ChausieTests */ = { 125 | isa = PBXGroup; 126 | children = ( 127 | E04FEB72227821D30084666D /* Tests */, 128 | E04FEB6522781D870084666D /* Mocks */, 129 | E04FEB4922781C4A0084666D /* Info.plist */, 130 | ); 131 | path = ChausieTests; 132 | sourceTree = ""; 133 | }; 134 | E04FEB6522781D870084666D /* Mocks */ = { 135 | isa = PBXGroup; 136 | children = ( 137 | E04FEB6622781DCF0084666D /* MockPagebableViewController.swift */, 138 | E09C252D22F8062F00D29AC9 /* MockTabCell.swift */, 139 | ); 140 | path = Mocks; 141 | sourceTree = ""; 142 | }; 143 | E04FEB72227821D30084666D /* Tests */ = { 144 | isa = PBXGroup; 145 | children = ( 146 | E04FEB73227821F30084666D /* PageableTests.swift */, 147 | E04FEB76227821F30084666D /* PageViewControllerDelegateTests.swift */, 148 | E04FEB75227821F30084666D /* PageViewControllerTests.swift */, 149 | E04FEB77227821F30084666D /* TabPageViewControllerTests.swift */, 150 | E04FEB74227821F30084666D /* TabViewTests.swift */, 151 | E09C252B22F802FF00D29AC9 /* TabPageViewControllerDelegateTests.swift */, 152 | ); 153 | path = Tests; 154 | sourceTree = ""; 155 | }; 156 | /* End PBXGroup section */ 157 | 158 | /* Begin PBXHeadersBuildPhase section */ 159 | E04FEB3422781C490084666D /* Headers */ = { 160 | isa = PBXHeadersBuildPhase; 161 | buildActionMask = 2147483647; 162 | files = ( 163 | E04FEB4A22781C4A0084666D /* Chausie.h in Headers */, 164 | ); 165 | runOnlyForDeploymentPostprocessing = 0; 166 | }; 167 | /* End PBXHeadersBuildPhase section */ 168 | 169 | /* Begin PBXNativeTarget section */ 170 | E04FEB3822781C490084666D /* Chausie */ = { 171 | isa = PBXNativeTarget; 172 | buildConfigurationList = E04FEB4D22781C4A0084666D /* Build configuration list for PBXNativeTarget "Chausie" */; 173 | buildPhases = ( 174 | E04FEB3422781C490084666D /* Headers */, 175 | E04FEB3522781C490084666D /* Sources */, 176 | E04FEB3622781C490084666D /* Frameworks */, 177 | E04FEB3722781C490084666D /* Resources */, 178 | ); 179 | buildRules = ( 180 | ); 181 | dependencies = ( 182 | ); 183 | name = Chausie; 184 | productName = Chausie; 185 | productReference = E04FEB3922781C490084666D /* Chausie.framework */; 186 | productType = "com.apple.product-type.framework"; 187 | }; 188 | E04FEB4122781C4A0084666D /* ChausieTests */ = { 189 | isa = PBXNativeTarget; 190 | buildConfigurationList = E04FEB5022781C4A0084666D /* Build configuration list for PBXNativeTarget "ChausieTests" */; 191 | buildPhases = ( 192 | E04FEB3E22781C4A0084666D /* Sources */, 193 | E04FEB3F22781C4A0084666D /* Frameworks */, 194 | E04FEB4022781C4A0084666D /* Resources */, 195 | ); 196 | buildRules = ( 197 | ); 198 | dependencies = ( 199 | E04FEB4522781C4A0084666D /* PBXTargetDependency */, 200 | ); 201 | name = ChausieTests; 202 | productName = ChausieTests; 203 | productReference = E04FEB4222781C4A0084666D /* ChausieTests.xctest */; 204 | productType = "com.apple.product-type.bundle.unit-test"; 205 | }; 206 | /* End PBXNativeTarget section */ 207 | 208 | /* Begin PBXProject section */ 209 | E04FEB3022781C490084666D /* Project object */ = { 210 | isa = PBXProject; 211 | attributes = { 212 | LastSwiftUpdateCheck = 1020; 213 | LastUpgradeCheck = 1020; 214 | ORGANIZATIONNAME = shoheiyokoyama; 215 | TargetAttributes = { 216 | E04FEB3822781C490084666D = { 217 | CreatedOnToolsVersion = 10.2.1; 218 | LastSwiftMigration = 1020; 219 | }; 220 | E04FEB4122781C4A0084666D = { 221 | CreatedOnToolsVersion = 10.2.1; 222 | LastSwiftMigration = 1020; 223 | }; 224 | }; 225 | }; 226 | buildConfigurationList = E04FEB3322781C490084666D /* Build configuration list for PBXProject "Chausie" */; 227 | compatibilityVersion = "Xcode 9.3"; 228 | developmentRegion = en; 229 | hasScannedForEncodings = 0; 230 | knownRegions = ( 231 | en, 232 | ); 233 | mainGroup = E04FEB2F22781C490084666D; 234 | productRefGroup = E04FEB3A22781C490084666D /* Products */; 235 | projectDirPath = ""; 236 | projectRoot = ""; 237 | targets = ( 238 | E04FEB3822781C490084666D /* Chausie */, 239 | E04FEB4122781C4A0084666D /* ChausieTests */, 240 | ); 241 | }; 242 | /* End PBXProject section */ 243 | 244 | /* Begin PBXResourcesBuildPhase section */ 245 | E04FEB3722781C490084666D /* Resources */ = { 246 | isa = PBXResourcesBuildPhase; 247 | buildActionMask = 2147483647; 248 | files = ( 249 | ); 250 | runOnlyForDeploymentPostprocessing = 0; 251 | }; 252 | E04FEB4022781C4A0084666D /* Resources */ = { 253 | isa = PBXResourcesBuildPhase; 254 | buildActionMask = 2147483647; 255 | files = ( 256 | ); 257 | runOnlyForDeploymentPostprocessing = 0; 258 | }; 259 | /* End PBXResourcesBuildPhase section */ 260 | 261 | /* Begin PBXSourcesBuildPhase section */ 262 | E04FEB3522781C490084666D /* Sources */ = { 263 | isa = PBXSourcesBuildPhase; 264 | buildActionMask = 2147483647; 265 | files = ( 266 | E04FEB5C22781CD00084666D /* TabPageComposer.swift in Sources */, 267 | E04FEB5D22781CD00084666D /* TabPageViewController.swift in Sources */, 268 | E04FEB6322781CD00084666D /* Pageable.swift in Sources */, 269 | E04FEB6222781CD00084666D /* PageViewControllerDelegate.swift in Sources */, 270 | E04FEB6022781CD00084666D /* TabViewLayout.swift in Sources */, 271 | E04FEB5E22781CD00084666D /* Position.swift in Sources */, 272 | E09C252A22F800D800D29AC9 /* TabPageViewControllerDelegate.swift in Sources */, 273 | E04FEB6122781CD00084666D /* TabView.swift in Sources */, 274 | E04FEB6422781CD00084666D /* TabItemCell.swift in Sources */, 275 | E04FEB5F22781CD00084666D /* PageViewController.swift in Sources */, 276 | ); 277 | runOnlyForDeploymentPostprocessing = 0; 278 | }; 279 | E04FEB3E22781C4A0084666D /* Sources */ = { 280 | isa = PBXSourcesBuildPhase; 281 | buildActionMask = 2147483647; 282 | files = ( 283 | E04FEB79227821F30084666D /* TabViewTests.swift in Sources */, 284 | E04FEB7B227821F30084666D /* PageViewControllerDelegateTests.swift in Sources */, 285 | E09C252C22F802FF00D29AC9 /* TabPageViewControllerDelegateTests.swift in Sources */, 286 | E04FEB6722781DCF0084666D /* MockPagebableViewController.swift in Sources */, 287 | E04FEB7C227821F30084666D /* TabPageViewControllerTests.swift in Sources */, 288 | E09C252E22F8062F00D29AC9 /* MockTabCell.swift in Sources */, 289 | E04FEB7A227821F30084666D /* PageViewControllerTests.swift in Sources */, 290 | E04FEB78227821F30084666D /* PageableTests.swift in Sources */, 291 | ); 292 | runOnlyForDeploymentPostprocessing = 0; 293 | }; 294 | /* End PBXSourcesBuildPhase section */ 295 | 296 | /* Begin PBXTargetDependency section */ 297 | E04FEB4522781C4A0084666D /* PBXTargetDependency */ = { 298 | isa = PBXTargetDependency; 299 | target = E04FEB3822781C490084666D /* Chausie */; 300 | targetProxy = E04FEB4422781C4A0084666D /* PBXContainerItemProxy */; 301 | }; 302 | /* End PBXTargetDependency section */ 303 | 304 | /* Begin XCBuildConfiguration section */ 305 | E04FEB4B22781C4A0084666D /* Debug */ = { 306 | isa = XCBuildConfiguration; 307 | buildSettings = { 308 | ALWAYS_SEARCH_USER_PATHS = NO; 309 | CLANG_ANALYZER_NONNULL = YES; 310 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 311 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 312 | CLANG_CXX_LIBRARY = "libc++"; 313 | CLANG_ENABLE_MODULES = YES; 314 | CLANG_ENABLE_OBJC_ARC = YES; 315 | CLANG_ENABLE_OBJC_WEAK = YES; 316 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 317 | CLANG_WARN_BOOL_CONVERSION = YES; 318 | CLANG_WARN_COMMA = YES; 319 | CLANG_WARN_CONSTANT_CONVERSION = YES; 320 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 321 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 322 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 323 | CLANG_WARN_EMPTY_BODY = YES; 324 | CLANG_WARN_ENUM_CONVERSION = YES; 325 | CLANG_WARN_INFINITE_RECURSION = YES; 326 | CLANG_WARN_INT_CONVERSION = YES; 327 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 328 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 329 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 330 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 331 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 332 | CLANG_WARN_STRICT_PROTOTYPES = YES; 333 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 334 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 335 | CLANG_WARN_UNREACHABLE_CODE = YES; 336 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 337 | CODE_SIGN_IDENTITY = "iPhone Developer"; 338 | COPY_PHASE_STRIP = NO; 339 | CURRENT_PROJECT_VERSION = 1; 340 | DEBUG_INFORMATION_FORMAT = dwarf; 341 | ENABLE_STRICT_OBJC_MSGSEND = YES; 342 | ENABLE_TESTABILITY = YES; 343 | GCC_C_LANGUAGE_STANDARD = gnu11; 344 | GCC_DYNAMIC_NO_PIC = NO; 345 | GCC_NO_COMMON_BLOCKS = YES; 346 | GCC_OPTIMIZATION_LEVEL = 0; 347 | GCC_PREPROCESSOR_DEFINITIONS = ( 348 | "DEBUG=1", 349 | "$(inherited)", 350 | ); 351 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 352 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 353 | GCC_WARN_UNDECLARED_SELECTOR = YES; 354 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 355 | GCC_WARN_UNUSED_FUNCTION = YES; 356 | GCC_WARN_UNUSED_VARIABLE = YES; 357 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 358 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 359 | MTL_FAST_MATH = YES; 360 | ONLY_ACTIVE_ARCH = YES; 361 | SDKROOT = iphoneos; 362 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 363 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 364 | VERSIONING_SYSTEM = "apple-generic"; 365 | VERSION_INFO_PREFIX = ""; 366 | }; 367 | name = Debug; 368 | }; 369 | E04FEB4C22781C4A0084666D /* Release */ = { 370 | isa = XCBuildConfiguration; 371 | buildSettings = { 372 | ALWAYS_SEARCH_USER_PATHS = NO; 373 | CLANG_ANALYZER_NONNULL = YES; 374 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 375 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 376 | CLANG_CXX_LIBRARY = "libc++"; 377 | CLANG_ENABLE_MODULES = YES; 378 | CLANG_ENABLE_OBJC_ARC = YES; 379 | CLANG_ENABLE_OBJC_WEAK = YES; 380 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 381 | CLANG_WARN_BOOL_CONVERSION = YES; 382 | CLANG_WARN_COMMA = YES; 383 | CLANG_WARN_CONSTANT_CONVERSION = YES; 384 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 385 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 386 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 387 | CLANG_WARN_EMPTY_BODY = YES; 388 | CLANG_WARN_ENUM_CONVERSION = YES; 389 | CLANG_WARN_INFINITE_RECURSION = YES; 390 | CLANG_WARN_INT_CONVERSION = YES; 391 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 392 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 393 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 394 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 395 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 396 | CLANG_WARN_STRICT_PROTOTYPES = YES; 397 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 398 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 399 | CLANG_WARN_UNREACHABLE_CODE = YES; 400 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 401 | CODE_SIGN_IDENTITY = "iPhone Developer"; 402 | COPY_PHASE_STRIP = NO; 403 | CURRENT_PROJECT_VERSION = 1; 404 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 405 | ENABLE_NS_ASSERTIONS = NO; 406 | ENABLE_STRICT_OBJC_MSGSEND = YES; 407 | GCC_C_LANGUAGE_STANDARD = gnu11; 408 | GCC_NO_COMMON_BLOCKS = YES; 409 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 410 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 411 | GCC_WARN_UNDECLARED_SELECTOR = YES; 412 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 413 | GCC_WARN_UNUSED_FUNCTION = YES; 414 | GCC_WARN_UNUSED_VARIABLE = YES; 415 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 416 | MTL_ENABLE_DEBUG_INFO = NO; 417 | MTL_FAST_MATH = YES; 418 | SDKROOT = iphoneos; 419 | SWIFT_COMPILATION_MODE = wholemodule; 420 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 421 | VALIDATE_PRODUCT = YES; 422 | VERSIONING_SYSTEM = "apple-generic"; 423 | VERSION_INFO_PREFIX = ""; 424 | }; 425 | name = Release; 426 | }; 427 | E04FEB4E22781C4A0084666D /* Debug */ = { 428 | isa = XCBuildConfiguration; 429 | buildSettings = { 430 | CLANG_ENABLE_MODULES = YES; 431 | CODE_SIGN_IDENTITY = ""; 432 | CODE_SIGN_STYLE = Automatic; 433 | DEFINES_MODULE = YES; 434 | DYLIB_COMPATIBILITY_VERSION = 1; 435 | DYLIB_CURRENT_VERSION = 1; 436 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 437 | INFOPLIST_FILE = Chausie/Info.plist; 438 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 439 | LD_RUNPATH_SEARCH_PATHS = ( 440 | "$(inherited)", 441 | "@executable_path/Frameworks", 442 | "@loader_path/Frameworks", 443 | ); 444 | PRODUCT_BUNDLE_IDENTIFIER = jp.co.shoheiyokoyama.Chausie; 445 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 446 | SKIP_INSTALL = YES; 447 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 448 | SWIFT_VERSION = 5.0; 449 | TARGETED_DEVICE_FAMILY = "1,2"; 450 | }; 451 | name = Debug; 452 | }; 453 | E04FEB4F22781C4A0084666D /* Release */ = { 454 | isa = XCBuildConfiguration; 455 | buildSettings = { 456 | CLANG_ENABLE_MODULES = YES; 457 | CODE_SIGN_IDENTITY = ""; 458 | CODE_SIGN_STYLE = Automatic; 459 | DEFINES_MODULE = YES; 460 | DYLIB_COMPATIBILITY_VERSION = 1; 461 | DYLIB_CURRENT_VERSION = 1; 462 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 463 | INFOPLIST_FILE = Chausie/Info.plist; 464 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 465 | LD_RUNPATH_SEARCH_PATHS = ( 466 | "$(inherited)", 467 | "@executable_path/Frameworks", 468 | "@loader_path/Frameworks", 469 | ); 470 | PRODUCT_BUNDLE_IDENTIFIER = jp.co.shoheiyokoyama.Chausie; 471 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 472 | SKIP_INSTALL = YES; 473 | SWIFT_VERSION = 5.0; 474 | TARGETED_DEVICE_FAMILY = "1,2"; 475 | }; 476 | name = Release; 477 | }; 478 | E04FEB5122781C4A0084666D /* Debug */ = { 479 | isa = XCBuildConfiguration; 480 | buildSettings = { 481 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 482 | CLANG_ENABLE_MODULES = YES; 483 | CODE_SIGN_STYLE = Automatic; 484 | INFOPLIST_FILE = ChausieTests/Info.plist; 485 | LD_RUNPATH_SEARCH_PATHS = ( 486 | "$(inherited)", 487 | "@executable_path/Frameworks", 488 | "@loader_path/Frameworks", 489 | ); 490 | PRODUCT_BUNDLE_IDENTIFIER = jp.co.shoheiyokoyama.ChausieTests; 491 | PRODUCT_NAME = "$(TARGET_NAME)"; 492 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 493 | SWIFT_VERSION = 5.0; 494 | TARGETED_DEVICE_FAMILY = "1,2"; 495 | }; 496 | name = Debug; 497 | }; 498 | E04FEB5222781C4A0084666D /* Release */ = { 499 | isa = XCBuildConfiguration; 500 | buildSettings = { 501 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 502 | CLANG_ENABLE_MODULES = YES; 503 | CODE_SIGN_STYLE = Automatic; 504 | INFOPLIST_FILE = ChausieTests/Info.plist; 505 | LD_RUNPATH_SEARCH_PATHS = ( 506 | "$(inherited)", 507 | "@executable_path/Frameworks", 508 | "@loader_path/Frameworks", 509 | ); 510 | PRODUCT_BUNDLE_IDENTIFIER = jp.co.shoheiyokoyama.ChausieTests; 511 | PRODUCT_NAME = "$(TARGET_NAME)"; 512 | SWIFT_VERSION = 5.0; 513 | TARGETED_DEVICE_FAMILY = "1,2"; 514 | }; 515 | name = Release; 516 | }; 517 | /* End XCBuildConfiguration section */ 518 | 519 | /* Begin XCConfigurationList section */ 520 | E04FEB3322781C490084666D /* Build configuration list for PBXProject "Chausie" */ = { 521 | isa = XCConfigurationList; 522 | buildConfigurations = ( 523 | E04FEB4B22781C4A0084666D /* Debug */, 524 | E04FEB4C22781C4A0084666D /* Release */, 525 | ); 526 | defaultConfigurationIsVisible = 0; 527 | defaultConfigurationName = Release; 528 | }; 529 | E04FEB4D22781C4A0084666D /* Build configuration list for PBXNativeTarget "Chausie" */ = { 530 | isa = XCConfigurationList; 531 | buildConfigurations = ( 532 | E04FEB4E22781C4A0084666D /* Debug */, 533 | E04FEB4F22781C4A0084666D /* Release */, 534 | ); 535 | defaultConfigurationIsVisible = 0; 536 | defaultConfigurationName = Release; 537 | }; 538 | E04FEB5022781C4A0084666D /* Build configuration list for PBXNativeTarget "ChausieTests" */ = { 539 | isa = XCConfigurationList; 540 | buildConfigurations = ( 541 | E04FEB5122781C4A0084666D /* Debug */, 542 | E04FEB5222781C4A0084666D /* Release */, 543 | ); 544 | defaultConfigurationIsVisible = 0; 545 | defaultConfigurationName = Release; 546 | }; 547 | /* End XCConfigurationList section */ 548 | }; 549 | rootObject = E04FEB3022781C490084666D /* Project object */; 550 | } 551 | -------------------------------------------------------------------------------- /Chausie.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Chausie.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Chausie.xcodeproj/xcshareddata/xcschemes/Chausie.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 50 | 51 | 52 | 53 | 54 | 55 | 65 | 66 | 72 | 73 | 74 | 75 | 76 | 77 | 83 | 84 | 90 | 91 | 92 | 93 | 95 | 96 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /Chausie.xcodeproj/xcuserdata/a14770.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Chausie.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | E04FEB3822781C490084666D 16 | 17 | primary 18 | 19 | 20 | E04FEB4122781C4A0084666D 21 | 22 | primary 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Chausie/Chausie.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | //! Project version number for Chausie. 4 | FOUNDATION_EXPORT double ChausieVersionNumber; 5 | 6 | //! Project version string for Chausie. 7 | FOUNDATION_EXPORT const unsigned char ChausieVersionString[]; 8 | 9 | // In this header, you should import all the public headers of your framework using statements like #import 10 | 11 | 12 | -------------------------------------------------------------------------------- /Chausie/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 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /Chausie/PageViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// A type that represents index of content. 4 | public typealias PageIndex = Int 5 | 6 | /// A customizable container view controller that manages navigation between pages of content. 7 | public final class PageViewController: UIViewController { 8 | /// A type that represents a child view controller. 9 | public typealias Child = UIViewController & Pageable 10 | 11 | /// The delegate object. methods of the delegate are called in response to scroll changes. 12 | public weak var delegate: PageViewControllerDelegate? 13 | 14 | /// The ratio of offset position to width of content in horizontal direction. 15 | public var scrollRatio: CGFloat { 16 | return scrollView.contentOffset.x / scrollView.contentSize.width 17 | } 18 | 19 | /// The underlying gesture recognizer for pan gestures of inner scrollview. 20 | /// Access this property when you want to more precisely control which pan gestures are 21 | /// recognized by the inner scroll view. 22 | public var panGestureRecognizer: UIPanGestureRecognizer { 23 | return scrollView.panGestureRecognizer 24 | } 25 | 26 | /// The visible view controllers 27 | public var visibleViewControllers: [Child] { 28 | return Set([visibleIndices.backword, visibleIndices.forward]).map { viewControllers[$0] } 29 | } 30 | 31 | /// The view controllers displayed by the page view controller. 32 | public let viewControllers: ContiguousArray 33 | 34 | /// A index set of child view controller added to the container. 35 | private struct ChildrenIndices { 36 | var forward: PageIndex 37 | var backward: PageIndex 38 | } 39 | 40 | /// A index set of child view controller displayed in the scrollView. 41 | private struct VisibleIndices { 42 | var forward: PageIndex 43 | var backword: PageIndex 44 | 45 | static func make(initialPageIndex: PageIndex) -> VisibleIndices { 46 | return VisibleIndices(forward: initialPageIndex, backword: initialPageIndex) 47 | } 48 | } 49 | 50 | private let scrollView = PageScrollView() 51 | 52 | /// A minimum index of content. 53 | private let minPageIndex: PageIndex = 0 54 | 55 | /// A maximum count of child view controller to add to the container. 56 | private let maxChildrenCount: Int 57 | 58 | /// The distance of index of child view controller added in the forward direction. 59 | private let forwardDistance: Int 60 | 61 | /// The distance of index of child view controller added in the backward direction. 62 | private let backwardDistance: Int 63 | 64 | /// A Boolean value indicating whether the size for the container’s view is changing. 65 | private var isTransitioning = false 66 | 67 | /// A maximum index of content. 68 | private var maxPageIndex: PageIndex { 69 | return viewControllers.count - 1 70 | } 71 | 72 | /// Indices of child view controller added to the container. 73 | private var childrenIndices: ChildrenIndices 74 | 75 | /// Indices of child view controller displayed in the scrollView. 76 | private var visibleIndices: VisibleIndices 77 | 78 | /// - Parameters: 79 | /// - viewControllers: The view controllers displayed by the page view controller. 80 | /// - maxChildrenCount: A maximum count of child view controller to add to the container. 81 | /// child view controllers added to the container is always changed 82 | /// by the scroll position. the default value is 1. 83 | /// - initialPageIndex: A index of content to be displayed when the view controller 84 | /// loads the view hierarchy into memory. the default value is 0. 85 | public required init( 86 | viewControllers: S, 87 | maxChildrenCount: Int = 1, 88 | initialPageIndex: PageIndex = 0 89 | ) where S.Element == Child { 90 | self.viewControllers = ContiguousArray(viewControllers) 91 | self.maxChildrenCount = min(maxChildrenCount, self.viewControllers.count) 92 | 93 | forwardDistance = Int(ceil(CGFloat(self.maxChildrenCount / 2))) 94 | backwardDistance = self.maxChildrenCount.isEven 95 | ? Int(CGFloat(self.maxChildrenCount) / 2 - 1) 96 | : Int(floor(CGFloat(self.maxChildrenCount / 2))) 97 | 98 | let forward = (initialPageIndex - forwardDistance).isNegated 99 | ? (initialPageIndex + maxChildrenCount).next(to: .backward) 100 | : min(initialPageIndex + forwardDistance, self.viewControllers.count - 1) 101 | let backword = initialPageIndex + self.maxChildrenCount > self.viewControllers.count - 1 102 | ? self.viewControllers.count - maxChildrenCount 103 | : PageIndex(max(minPageIndex, initialPageIndex - backwardDistance)) 104 | 105 | childrenIndices = ChildrenIndices(forward: forward, backward: backword) 106 | visibleIndices = VisibleIndices.make(initialPageIndex: min(initialPageIndex, self.viewControllers.count - 1)) 107 | 108 | super.init(nibName: nil, bundle: nil) 109 | 110 | precondition( 111 | 0...self.viewControllers.count ~= maxChildrenCount, 112 | """ 113 | Invalid value: `maxChildrenCount` (\(maxChildrenCount)) out of range. 114 | Check `viewControllers.count` (\(self.viewControllers.count)) or `maxChildrenCount` (\(maxChildrenCount)). 115 | """ 116 | ) 117 | 118 | precondition( 119 | 0...self.viewControllers.count - 1 ~= initialPageIndex, 120 | """ 121 | Invalid value: `initialPageIndex (\(initialPageIndex))` out of range. 122 | Check `viewControllers.count` (\(self.viewControllers.count)) or `initialIndex` (\(initialPageIndex)). 123 | """ 124 | ) 125 | } 126 | 127 | @available(*, unavailable) 128 | public required init?(coder aDecoder: NSCoder) { 129 | fatalError("init(coder:) has not been implemented") 130 | } 131 | 132 | public override func viewDidLoad() { 133 | super.viewDidLoad() 134 | 135 | view.addSubview(scrollView) 136 | 137 | scrollView.translatesAutoresizingMaskIntoConstraints = false 138 | NSLayoutConstraint.activate([ 139 | view.widthAnchor.constraint(equalTo: scrollView.widthAnchor, multiplier: 1), 140 | view.heightAnchor.constraint(equalTo: scrollView.heightAnchor, multiplier: 1), 141 | view.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), 142 | view.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), 143 | view.topAnchor.constraint(equalTo: scrollView.topAnchor), 144 | view.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor) 145 | ]) 146 | scrollView.layoutIfNeeded() 147 | 148 | let initialPageIndex = visibleIndices.forward 149 | scrollView.delegate = self 150 | scrollView.contentSize.width = scrollView.bounds.width * CGFloat(viewControllers.count) 151 | scrollView.contentOffset.x = scrollView.bounds.width * CGFloat(initialPageIndex) 152 | delegate?.pageViewController(self, didScrollAtRatio: scrollRatio) 153 | 154 | let initailIndices: CountableClosedRange = { 155 | let lowerBound = initialPageIndex + forwardDistance > maxPageIndex 156 | ? viewControllers.count - maxChildrenCount 157 | : initialPageIndex - backwardDistance 158 | let upperBound = lowerBound - backwardDistance < minPageIndex 159 | ? maxChildrenCount - 1 160 | : initialPageIndex + forwardDistance 161 | return max(minPageIndex, lowerBound)...min(maxPageIndex, upperBound) 162 | }() 163 | 164 | viewControllers 165 | .lazy 166 | .enumerated() 167 | .filter { initailIndices.contains($0.0) } 168 | .prefix(maxChildrenCount) 169 | .forEach { offset, viewController in 170 | viewController.view.frame = CGRect( 171 | origin: CGPoint(x: CGFloat(offset) * scrollView.bounds.width, y: 0), 172 | size: scrollView.bounds.size 173 | ) 174 | notifySafeAreaInsets(viewController) 175 | addChildToScrollView(viewController) 176 | } 177 | } 178 | 179 | public override func viewDidAppear(_ animated: Bool) { 180 | super.viewDidAppear(animated) 181 | 182 | (visibleIndices.backword...visibleIndices.forward).forEach { index in 183 | viewControllers[index].pageViewController(self, didAppearAt: index) 184 | } 185 | } 186 | 187 | public override func viewDidDisappear(_ animated: Bool) { 188 | super.viewDidDisappear(animated) 189 | 190 | (visibleIndices.backword...visibleIndices.forward).forEach { index in 191 | viewControllers[index].pageViewController(self, didDisappearAt: index) 192 | } 193 | } 194 | 195 | public override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { 196 | super.viewWillTransition(to: size, with: coordinator) 197 | 198 | guard viewIfLoaded?.window != nil && scrollView.bounds.width > 0 else { return } 199 | 200 | isTransitioning = true 201 | 202 | let currentScrollRatio: CGFloat = { 203 | let offsetX = min(scrollView.contentOffset.x, scrollView.bounds.width * CGFloat(viewControllers.count)) 204 | let ratio = Int(offsetX / scrollView.bounds.width) 205 | return max(0, CGFloat(ratio)) 206 | }() 207 | 208 | coordinator.animate(alongsideTransition: { _ in 209 | let contentWidth = size.width 210 | 211 | if self.scrollView.contentSize.width != contentWidth * CGFloat(self.viewControllers.count) { 212 | self.scrollView.contentSize.width = contentWidth * CGFloat(self.viewControllers.count) 213 | 214 | // - Workaround: The offset position shift if use `scrollView.contentOffset.x = x` 215 | // instead of `setContentOffset(_:animated:)`. 216 | self.scrollView.setContentOffset(CGPoint(x: contentWidth * CGFloat(currentScrollRatio), y: 0), animated: false) 217 | } 218 | 219 | self.viewControllers 220 | .enumerated() 221 | .forEach { offset, viewController in 222 | 223 | self.notifySafeAreaInsets(viewController) 224 | 225 | if self.scrollView.subviews.contains(where: { $0 == viewController.view }) { 226 | viewController.view.frame.origin.x = contentWidth * CGFloat(offset) 227 | } 228 | else { 229 | 230 | // only child view controller added to the container updates the frame. 231 | viewController.view.frame.size = self.scrollView.bounds.size 232 | } 233 | } 234 | }, completion: { _ in 235 | self.isTransitioning = false 236 | }) 237 | } 238 | 239 | public override func viewDidLayoutSubviews() { 240 | super.viewDidLayoutSubviews() 241 | 242 | if !isTransitioning && !scrollView.isInteracting { 243 | scrollView.contentOffset.x = scrollView.bounds.width * CGFloat(visibleIndices.forward) 244 | } 245 | } 246 | 247 | /// Scrolls the page view contents until the specified index of content is visible. 248 | /// 249 | /// - Parameters: 250 | /// - index: A index of content to scroll into view. 251 | /// - animated: Specify true to animate the scrolling behavior or false to adjust 252 | /// the scroll view’s visible content immediately. 253 | public func scrollToPage(at index: PageIndex, animated: Bool) { 254 | precondition(index <= maxPageIndex, "`index` out of range. Please set `index` within the range of `viewControllers`") 255 | 256 | let contentOffset = CGPoint(x: scrollView.bounds.width * CGFloat(index), y: 0) 257 | scrollView.setContentOffset(contentOffset, animated: animated) 258 | 259 | if !animated { 260 | _scrollViewDidScroll(scrollView) 261 | } 262 | } 263 | 264 | /// Adds the specified view controller as a child of the current view controller 265 | /// and a view of child view controller to scrollView. The specified view controller's view 266 | /// is added to the scrollView. 267 | private func addChildToScrollView(_ childController: UIViewController) { 268 | addChild(childController) 269 | scrollView.addSubview(childController.view) 270 | childController.didMove(toParent: self) 271 | } 272 | 273 | /// Removes the view controller from its parent and unlinks the view from its superview 274 | /// and its window, and removes it from the responder chain. 275 | private func removeFromScrollView(_ childController: UIViewController) { 276 | childController.willMove(toParent: nil) 277 | childController.view.removeFromSuperview() 278 | childController.removeFromParent() 279 | } 280 | 281 | /// Notifies the specified view controller that the insets that you use to determine 282 | /// the safe area for this view. 283 | private func notifySafeAreaInsets(_ viewController: Child) { 284 | viewController.pageViewController(self, actualSafeAreaInsets: view.safeAreaInsets) 285 | } 286 | } 287 | 288 | extension PageViewController: UIScrollViewDelegate { 289 | public func scrollViewDidScroll(_ scrollView: UIScrollView) { 290 | _scrollViewDidScroll(scrollView) 291 | } 292 | 293 | private func _scrollViewDidScroll(_ scrollView: UIScrollView) { 294 | guard scrollView.contentSize.width > 0 else { return } 295 | 296 | delegate?.pageViewController(self, didScrollAtRatio: scrollRatio) 297 | 298 | guard !isTransitioning else { return } 299 | 300 | sendLifecycleEvent() 301 | layoutChildren() 302 | } 303 | 304 | /// Notifies the certain view controllers that event when child view controller 305 | /// about to appeared or disappeared in the scrollView. 306 | private func sendLifecycleEvent() { 307 | enum LifeSycle { 308 | case didAppear 309 | case didDisappear 310 | } 311 | 312 | let flooredPageIndex = PageIndex(scrollView.contentOffset.x / scrollView.bounds.width) 313 | let ceiledPageIndex = PageIndex(ceil(scrollView.contentOffset.x / scrollView.bounds.width)) 314 | 315 | guard visibleIndices.forward != ceiledPageIndex || visibleIndices.backword != flooredPageIndex else { 316 | return 317 | } 318 | 319 | if ceiledPageIndex != visibleIndices.forward && minPageIndex...maxPageIndex ~= ceiledPageIndex { 320 | defer { 321 | visibleIndices.forward = ceiledPageIndex 322 | } 323 | 324 | let lifesycle: LifeSycle = visibleIndices.forward < ceiledPageIndex ? .didAppear : .didDisappear 325 | 326 | switch lifesycle { 327 | case .didAppear: 328 | viewControllers[ceiledPageIndex].pageViewController(self, didAppearAt: ceiledPageIndex) 329 | case .didDisappear: 330 | viewControllers[visibleIndices.forward].pageViewController(self, didDisappearAt: visibleIndices.forward) 331 | } 332 | } 333 | 334 | if flooredPageIndex != visibleIndices.backword && minPageIndex...maxPageIndex ~= flooredPageIndex { 335 | defer { 336 | visibleIndices.backword = flooredPageIndex 337 | } 338 | 339 | let lifesycle: LifeSycle = visibleIndices.backword > flooredPageIndex ? .didAppear : .didDisappear 340 | 341 | switch lifesycle { 342 | case .didAppear: 343 | viewControllers[flooredPageIndex].pageViewController(self, didAppearAt: flooredPageIndex) 344 | case .didDisappear: 345 | viewControllers[visibleIndices.backword].pageViewController(self, didDisappearAt: visibleIndices.backword) 346 | } 347 | } 348 | } 349 | 350 | /// Adds and removes a certain view controller as a child depending on the scroll offset. 351 | private func layoutChildren() { 352 | guard maxChildrenCount < viewControllers.count else { return } 353 | 354 | enum Action { 355 | case addChild 356 | case removeFromParent 357 | } 358 | 359 | let forwardOffset = scrollView.contentOffset.x + scrollView.bounds.width * CGFloat(forwardDistance) 360 | let forwardIndex = PageIndex(ceil(forwardOffset / scrollView.bounds.width)) 361 | let newForwardIndex = max(minPageIndex, min(maxPageIndex, forwardIndex)) 362 | 363 | if newForwardIndex != childrenIndices.forward { 364 | defer { 365 | childrenIndices.forward = newForwardIndex 366 | } 367 | 368 | let action: Action = newForwardIndex > childrenIndices.forward ? .addChild : .removeFromParent 369 | switch action { 370 | case .addChild: 371 | let backword = (newForwardIndex - maxChildrenCount).next(to: .forward) 372 | let lowerBound = max(backword, childrenIndices.forward.next(to: .forward)) 373 | (max(minPageIndex, lowerBound)...newForwardIndex).forEach(addChild) 374 | 375 | case .removeFromParent: 376 | (newForwardIndex.next(to: .forward)...childrenIndices.forward).forEach { removeFromScrollView(viewControllers[$0]) } 377 | } 378 | } 379 | 380 | let backwardOffset = scrollView.contentOffset.x - scrollView.bounds.width * CGFloat(backwardDistance) 381 | let backwardIndex = PageIndex((backwardOffset / scrollView.bounds.width)) 382 | let ajustedIndex = backwardIndex + maxChildrenCount > viewControllers.count 383 | ? self.viewControllers.count - maxChildrenCount 384 | : backwardIndex 385 | let newBackwardIndex = max(minPageIndex, ajustedIndex) 386 | 387 | if newBackwardIndex != childrenIndices.backward { 388 | defer { 389 | childrenIndices.backward = newBackwardIndex 390 | } 391 | 392 | let action: Action = newBackwardIndex < childrenIndices.backward ? .addChild : .removeFromParent 393 | switch action { 394 | case .addChild: 395 | let forward = (childrenIndices.backward + maxChildrenCount).next(to: .backward) 396 | let upperBound = min(forward, childrenIndices.backward.next(to: .backward)) 397 | (newBackwardIndex...upperBound).forEach(addChild) 398 | 399 | case .removeFromParent: 400 | let currentForward = min((childrenIndices.backward + maxChildrenCount).next(to: .backward), maxPageIndex) 401 | let upperBound = min(currentForward, newBackwardIndex.next(to: .backward)) 402 | (childrenIndices.backward...upperBound).forEach { removeFromScrollView(viewControllers[$0]) } 403 | } 404 | } 405 | } 406 | 407 | private func addChild(at index: PageIndex) { 408 | let viewController = viewControllers[index] 409 | viewController.view.frame = CGRect( 410 | origin: CGPoint(x: scrollView.bounds.width * CGFloat(index), y: 0), 411 | size: scrollView.bounds.size 412 | ) 413 | notifySafeAreaInsets(viewController) 414 | addChildToScrollView(viewController) 415 | } 416 | } 417 | 418 | private final class PageScrollView: UIScrollView { 419 | override init(frame: CGRect) { 420 | super.init(frame: frame) 421 | isPagingEnabled = true 422 | showsHorizontalScrollIndicator = false 423 | showsVerticalScrollIndicator = false 424 | contentInsetAdjustmentBehavior = .never 425 | delaysContentTouches = false 426 | } 427 | 428 | @available(*, unavailable) 429 | required init?(coder aDecoder: NSCoder) { 430 | fatalError("init(coder:) has not been implemented") 431 | } 432 | 433 | override func touchesShouldCancel(in view: UIView) -> Bool { 434 | return true 435 | } 436 | } 437 | 438 | private extension PageIndex { 439 | enum Direction: Int { 440 | case forward = 1 441 | case backward = -1 442 | } 443 | 444 | func next(to direction: Direction) -> PageIndex { 445 | return self + direction.rawValue 446 | } 447 | } 448 | 449 | private extension BinaryInteger { 450 | var isEven: Bool { 451 | return self % 2 == 0 452 | } 453 | } 454 | 455 | private extension Comparable where Self: Numeric { 456 | var isNegated: Bool { 457 | return self < 0 458 | } 459 | } 460 | 461 | private extension UIScrollView { 462 | var isInteracting: Bool { 463 | return isDragging && !isDecelerating || isTracking 464 | } 465 | } 466 | -------------------------------------------------------------------------------- /Chausie/PageViewControllerDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// The methods declared by the `PageViewControllerDelegate` protocol allow the adopting 4 | /// delegate to respond to messages from the `UIScrollView` class in `PageViewController`. 5 | /// The method of this protocol is optional. 6 | public protocol PageViewControllerDelegate: class { 7 | /// Tells the delegate when the user scrolls the page view. 8 | /// 9 | /// - Parameters: 10 | /// - pageViewController: The page view object in which the scrolling occurred. 11 | /// - ratio: The ratio of offset position to width of content. 12 | func pageViewController(_ pageViewController: PageViewController, didScrollAtRatio ratio: CGFloat) 13 | } 14 | 15 | public extension PageViewControllerDelegate { 16 | func pageViewController(_ pageViewController: PageViewController, didScrollAtRatio ratio: CGFloat) {} 17 | } 18 | -------------------------------------------------------------------------------- /Chausie/Pageable.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// The methods declared by the `Pageable` protocol allow the adopting delegate to respond 4 | /// to messages from the `PageViewController` class. 5 | /// The methods of this protocol are all optional. 6 | public protocol Pageable: class { 7 | /// Tells the delegate that the specified content of index is displayed in 8 | /// the page view controller. 9 | /// 10 | /// - Parameters: 11 | /// - pageViewController: The page view object that is adding the content of index. 12 | /// - index: The index of content that is displayed in the page view controller. 13 | func pageViewController(_ pageViewController: PageViewController, didAppearAt index: PageIndex) 14 | 15 | /// Tells the delegate that the specified content of index is disappeared from 16 | /// the page view controller. 17 | /// 18 | /// - Parameters: 19 | /// - pageViewController: The page view object that is adding the content of index. 20 | /// - index: The index of content that is disappeared from the page view controller. 21 | func pageViewController(_ pageViewController: PageViewController, didDisappearAt index: PageIndex) 22 | 23 | /// Tells the delegate that when the view controller is added. 24 | /// 25 | /// - Parameters: 26 | /// - pageViewController: The page view object that is adding the content of index. 27 | /// - safeAreaInsets: The insets that you use to determine the safe area for this view. 28 | func pageViewController(_ pageViewController: PageViewController, actualSafeAreaInsets safeAreaInsets: UIEdgeInsets) 29 | } 30 | 31 | public extension Pageable { 32 | func pageViewController(_ pageViewController: PageViewController, didAppearAt index: PageIndex) {} 33 | func pageViewController(_ pageViewController: PageViewController, didDisappearAt index: PageIndex) {} 34 | func pageViewController(_ pageViewController: PageViewController, actualSafeAreaInsets safeAreaInsets: UIEdgeInsets) {} 35 | } 36 | -------------------------------------------------------------------------------- /Chausie/Position.swift: -------------------------------------------------------------------------------- 1 | import CoreGraphics 2 | 3 | /// The position composed of `origin` and `distance`. 4 | public struct Position { 5 | /// The origin position 6 | public enum Origin { 7 | case layoutMargin 8 | case safeArea 9 | case view 10 | } 11 | 12 | /// the origin of the position. 13 | public let origin: Origin 14 | 15 | /// The distance from the origin. 16 | public let distance: CGFloat 17 | 18 | public init(origin: Origin, distance: CGFloat) { 19 | self.origin = origin 20 | self.distance = distance 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Chausie/TabItemCell.swift: -------------------------------------------------------------------------------- 1 | import CoreGraphics 2 | 3 | /// A protocol that a class must adopt allow customization for a item within the tab 4 | /// view’s visible bounds. 5 | public protocol TabItemCell: class { 6 | /// A type to customize self. 7 | associatedtype Model 8 | 9 | /// A factory method that make a customized item within tab view by using model. 10 | static func make(model: Model) -> Self 11 | 12 | /// This method is called when highlight state changes. 13 | /// This method is optional. 14 | /// 15 | /// - Parameters: 16 | /// - ratio: The ratio of scroll view's offset position in the range 0.0 to 1.0. 17 | func updateHighlightRatio(_ ratio: CGFloat) 18 | } 19 | 20 | public extension TabItemCell { 21 | func updateHighlightRatio(_ ratio: CGFloat) {} 22 | } 23 | -------------------------------------------------------------------------------- /Chausie/TabPageComposer.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// A composer that generates tab view and page view controller in 4 | /// tab page view controller. 5 | open class TabPageComposer { 6 | /// The dataset composed of child view controller and tab view's model. 7 | public struct Component { 8 | /// A view controller that conforms to `Pageable`. 9 | public var child: PageViewController.Child 10 | 11 | /// A model used to make a item in tab view. 12 | public var cellModel: TabCell.Model 13 | 14 | public init(child: C, cellModel: TabCell.Model) { 15 | self.child = child 16 | self.cellModel = cellModel 17 | } 18 | } 19 | 20 | /// An array of component composed of child view controller and 21 | /// tab view's model. 22 | public var components: [Component] 23 | 24 | /// A generator of tab view. 25 | public var tabViewGenerator: () -> TabView 26 | 27 | /// - Parameters: 28 | /// - components: An array of component composed of child view 29 | /// controller and tab view's model. 30 | /// - tabViewGenerator: A generator of tab view. 31 | public init( 32 | components: [Component], 33 | tabViewGenerator: @escaping ([TabCell.Model]) -> TabView = { cellModels in 34 | let tabView = TabView() 35 | tabView.renderCells( 36 | with: cellModels, 37 | cellGenerator: { TabCell.make(model: $0) } 38 | ) 39 | return tabView 40 | } 41 | ) { 42 | self.components = components 43 | self.tabViewGenerator = { 44 | tabViewGenerator(components.map { $0.cellModel }) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Chausie/TabPageViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// A customizable container view controller that manages tab view and page view controller. 4 | open class TabPageViewController: UIViewController, PageViewControllerDelegate { 5 | /// The delegate object. methods of the delegate are called in response to scroll changes. 6 | public weak var delegate: TabPageViewControllerDelegate? 7 | 8 | /// The tab view that the controller manages. 9 | public let tabView: TabView 10 | 11 | /// The page view controller that the controller manages. 12 | public let pageViewController: PageViewController 13 | 14 | /// The layout of inner tab view. 15 | private let tabViewLayout: TabViewLayout 16 | 17 | /// - Parameters: 18 | /// - composer: A composer that generates tab view and page view controller. 19 | /// - tabViewLayout: The layout of tab view. 20 | /// - maxChildrenCount: A maximum count of child view controller to add to the container. 21 | /// child view controllers added to the container is always changed 22 | /// by the scroll position. the default value is 1. 23 | /// - initialPageIndex: A index of content to be displayed when the view controller 24 | /// loads the view hierarchy into memory. the default value is 0. 25 | public required init( 26 | composer: TabPageComposer, 27 | tabViewLayout: TabViewLayout, 28 | maxChildrenCount: Int = 1, 29 | initialPageIndex: PageIndex = 0 30 | ) { 31 | 32 | precondition( 33 | 0...composer.components.count ~= maxChildrenCount, 34 | """ 35 | Invalid value: `maxChildrenCount` (\(maxChildrenCount)) out of range. 36 | Check `composer.components.count` (\(composer.components.count)) or `maxChildrenCount` (\(maxChildrenCount)). 37 | """ 38 | ) 39 | 40 | precondition( 41 | 0...composer.components.count - 1 ~= initialPageIndex, 42 | """ 43 | Invalid value: `initialPageIndex (\(initialPageIndex))` out of range. 44 | Check `composer.components.count` (\(composer.components.count)) or `initialIndex` (\(initialPageIndex)). 45 | """ 46 | ) 47 | 48 | self.pageViewController = PageViewController( 49 | viewControllers: composer.components.map { $0.child }, 50 | maxChildrenCount: maxChildrenCount, 51 | initialPageIndex: initialPageIndex 52 | ) 53 | self.tabView = composer.tabViewGenerator() 54 | self.tabViewLayout = tabViewLayout 55 | 56 | super.init(nibName: nil, bundle: nil) 57 | 58 | tabView.translatesAutoresizingMaskIntoConstraints = false 59 | view.addSubview(tabView) 60 | tabView.selectedCell = { [weak pageViewController] _, index in 61 | pageViewController?.scrollToPage(at: index, animated: true) 62 | } 63 | 64 | NSLayoutConstraint.activate([ 65 | tabView.topAnchor.constraint( 66 | equalTo: view.topAnchor(for: tabViewLayout.top.origin), 67 | constant: tabViewLayout.top.distance 68 | ), 69 | tabView.trailingAnchor.constraint( 70 | equalTo: view.trailingAnchor(for: tabViewLayout.trailing.origin), 71 | constant: -tabViewLayout.trailing.distance 72 | ), 73 | tabView.leadingAnchor.constraint( 74 | equalTo: view.leadingAnchor(for: tabViewLayout.leading.origin), 75 | constant: tabViewLayout.leading.distance 76 | ), 77 | tabView.heightAnchor.constraint( 78 | equalToConstant: tabViewLayout.height 79 | ) 80 | ]) 81 | 82 | // Needs to lays out before `pageViewController` starts to scrolling. 83 | tabView.layoutIfNeeded() 84 | 85 | pageViewController.delegate = self 86 | pageViewController.view.translatesAutoresizingMaskIntoConstraints = false 87 | addChild(pageViewController) 88 | pageViewController.view.frame = view.bounds 89 | view.addSubview(pageViewController.view) 90 | pageViewController.didMove(toParent: self) 91 | 92 | let pageview: UIView = pageViewController.view 93 | NSLayoutConstraint.activate([ 94 | pageview.topAnchor.constraint( 95 | equalTo: tabView.bottomAnchor, 96 | constant: 0 97 | ), 98 | pageview.trailingAnchor.constraint( 99 | equalTo: view.trailingAnchor, 100 | constant: 0 101 | ), 102 | pageview.leadingAnchor.constraint( 103 | equalTo: view.leadingAnchor, 104 | constant: 0 105 | ), 106 | pageview.bottomAnchor.constraint( 107 | equalTo: view.bottomAnchor, 108 | constant: 0 109 | ) 110 | ]) 111 | } 112 | 113 | @available(*, unavailable) 114 | public required init?(coder aDecoder: NSCoder) { 115 | fatalError("`init(coder:)` has not been implemented") 116 | } 117 | 118 | open override func viewDidLayoutSubviews() { 119 | super.viewDidLayoutSubviews() 120 | 121 | let tabViewWidth = view.bounds.size.width 122 | - inset(for: tabViewLayout.leading).left - tabViewLayout.leading.distance 123 | - inset(for: tabViewLayout.trailing).right - tabViewLayout.trailing.distance 124 | let tabViewSize = CGSize(width: tabViewWidth, height: tabViewLayout.height) 125 | if tabViewSize != tabView.bounds.size { 126 | tabView.layoutIfNeeded() 127 | tabView.highlightCells(withRatio: pageViewController.scrollRatio) 128 | } 129 | } 130 | 131 | open func pageViewController(_ pageViewController: PageViewController, didScrollAtRatio ratio: CGFloat) { 132 | tabView.highlightCells(withRatio: ratio) 133 | delegate?.tabPageViewController(self, didScrollAtRatio: ratio) 134 | } 135 | 136 | private func inset(for position: Position) -> UIEdgeInsets { 137 | switch position.origin { 138 | case .layoutMargin: 139 | return view.layoutMargins 140 | 141 | case .safeArea: 142 | return view.safeAreaInsets 143 | 144 | case .view: 145 | return .zero 146 | } 147 | } 148 | } 149 | 150 | private extension UIView { 151 | /// Returns `NSLayoutYAxisAnchor` by using `Position.Origin`. 152 | func topAnchor(for origin: Position.Origin) -> NSLayoutYAxisAnchor { 153 | switch origin { 154 | case .layoutMargin: 155 | return layoutMarginsGuide.topAnchor 156 | 157 | case .safeArea: 158 | return safeAreaLayoutGuide.topAnchor 159 | 160 | case .view: 161 | return topAnchor 162 | } 163 | } 164 | 165 | /// Returns `NSLayoutXAxisAnchor` by using `Position.Origin`. 166 | func trailingAnchor(for origin: Position.Origin) -> NSLayoutXAxisAnchor { 167 | switch origin { 168 | case .layoutMargin: 169 | return layoutMarginsGuide.trailingAnchor 170 | 171 | case .safeArea: 172 | return safeAreaLayoutGuide.trailingAnchor 173 | 174 | case .view: 175 | return trailingAnchor 176 | } 177 | } 178 | 179 | /// Returns `NSLayoutYAxisAnchor` by using `Position.Origin`. 180 | func bottomAnchor(for origin: Position.Origin) -> NSLayoutYAxisAnchor { 181 | switch origin { 182 | case .layoutMargin: 183 | return layoutMarginsGuide.bottomAnchor 184 | 185 | case .safeArea: 186 | return safeAreaLayoutGuide.bottomAnchor 187 | 188 | case .view: 189 | return bottomAnchor 190 | } 191 | } 192 | 193 | /// Returns `NSLayoutXAxisAnchor` by using `Position.Origin`. 194 | func leadingAnchor(for origin: Position.Origin) -> NSLayoutXAxisAnchor { 195 | switch origin { 196 | case .layoutMargin: 197 | return layoutMarginsGuide.leadingAnchor 198 | 199 | case .safeArea: 200 | return safeAreaLayoutGuide.leadingAnchor 201 | 202 | case .view: 203 | return leadingAnchor 204 | } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /Chausie/TabPageViewControllerDelegate.swift: -------------------------------------------------------------------------------- 1 | import CoreGraphics 2 | 3 | /// The methods declared by the `TabPageViewControllerDelegate` protocol allow the adopting 4 | /// delegate to respond to messages from the `UIScrollView` class in `TabPageViewController`. 5 | /// The method of this protocol is optional. 6 | public protocol TabPageViewControllerDelegate: class { 7 | /// Tells the delegate when the user scrolls the inner page view. 8 | /// 9 | /// - Parameters: 10 | /// - tabPageViewController: A customizable container view controller that manages 11 | /// tab view and page view controller. 12 | /// - ratio: The ratio of offset position to width of content. 13 | func tabPageViewController(_ tabPageViewController: TabPageViewController, didScrollAtRatio ratio: CGFloat) 14 | } 15 | 16 | public extension PageViewControllerDelegate { 17 | func tabPageViewController(_ tabPageViewController: TabPageViewController, didScrollAtRatio ratio: CGFloat) {} 18 | } 19 | -------------------------------------------------------------------------------- /Chausie/TabView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /// An object that manages tab items. 4 | open class TabView: UIView { 5 | private struct Component { 6 | var cell: UIControl 7 | var updateHighlightRatio: (CGFloat) -> Void 8 | } 9 | 10 | /// This closure is called when a touch-up event occurs in the item where the finger is 11 | /// inside the bounds of the control. 12 | public var selectedCell: ((UIControl, Int) -> Void)? 13 | 14 | /// An array of item in tab view. 15 | public var cells: [UIControl] { 16 | return components.map { $0.cell } 17 | } 18 | 19 | /// An array of component. 20 | private var components: [Component] = [] 21 | 22 | /// Changes highlight state of tab items. 23 | /// 24 | /// - Parameters: 25 | /// - ratio: The ratio of scroll view's offset position in the range 0.0 to 1.0. 26 | open func highlightCells(withRatio ratio: CGFloat) { 27 | let cellsRatio = 1 / CGFloat(components.count) 28 | zip(components, components.indices).forEach { component, index in 29 | let highlightRatio = 1 - (abs(CGFloat(index) * cellsRatio - ratio) / cellsRatio) 30 | component.updateHighlightRatio(max(0, highlightRatio)) 31 | } 32 | } 33 | 34 | /// Makes and adds items to the end of the receiver’s list of subviews. 35 | /// Generic type-`C` is must conform to `TabItemCell` and be a subclass of `UIControl`. 36 | /// 37 | /// - Parameters: 38 | /// - cellModels: An array of model used to make items. 39 | /// - cellGenerator: A factory closure that make a customized `C`-type object. 40 | open func renderCells( 41 | with cellModels: [C.Model], 42 | cellGenerator: ((C.Model) -> C) 43 | ) { 44 | 45 | components.forEach { $0.cell.removeFromSuperview() } 46 | components.removeAll() 47 | 48 | var constraints: [NSLayoutConstraint] = [] 49 | var leading = leadingAnchor 50 | cellModels.forEach { model in 51 | let cell = cellGenerator(model) 52 | 53 | components.append( 54 | Component( 55 | cell: cell, 56 | updateHighlightRatio: { ratio in 57 | cell.updateHighlightRatio(ratio) 58 | } 59 | ) 60 | ) 61 | 62 | addSubview(cell) 63 | cell.translatesAutoresizingMaskIntoConstraints = false 64 | constraints += [ 65 | cell.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 1 / CGFloat(cellModels.count)), 66 | cell.heightAnchor.constraint(equalTo: heightAnchor), 67 | cell.topAnchor.constraint(equalTo: topAnchor), 68 | cell.leadingAnchor.constraint(equalTo: leading) 69 | ] 70 | leading = cell.trailingAnchor 71 | 72 | cell.addTarget(self, action: #selector(selectedCell(_:)), for: .touchUpInside) 73 | } 74 | 75 | NSLayoutConstraint.activate(constraints) 76 | } 77 | 78 | @objc func selectedCell(_ sender: UIControl) { 79 | components.firstIndex(where: { $0.cell == sender }).map { selectedCell?(sender, $0) } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Chausie/TabViewLayout.swift: -------------------------------------------------------------------------------- 1 | import CoreGraphics 2 | 3 | /// The layout of tab view. 4 | public struct TabViewLayout { 5 | public let top: Position 6 | public let trailing: Position 7 | public let leading: Position 8 | public let height: CGFloat 9 | 10 | public init( 11 | top: Position, 12 | trailing: Position, 13 | leading: Position, 14 | height: CGFloat 15 | ) { 16 | self.top = top 17 | self.trailing = trailing 18 | self.leading = leading 19 | self.height = height 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ChausieTests/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 | -------------------------------------------------------------------------------- /ChausieTests/Mocks/MockPagebableViewController.swift: -------------------------------------------------------------------------------- 1 | import Chausie 2 | 3 | final class MockPagebableViewController: UIViewController, Pageable {} 4 | -------------------------------------------------------------------------------- /ChausieTests/Mocks/MockTabCell.swift: -------------------------------------------------------------------------------- 1 | import Chausie 2 | 3 | final class MockTabCell: UIButton, TabItemCell { 4 | typealias Model = String 5 | 6 | private(set) var ratio: CGFloat? 7 | 8 | static func make(model: Model) -> MockTabCell { 9 | let cell = MockTabCell() 10 | cell.setTitle(model, for: .normal) 11 | return cell 12 | } 13 | 14 | func updateHighlightRatio(_ ratio: CGFloat) { 15 | self.ratio = ratio 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ChausieTests/Tests/PageViewControllerDelegateTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Chausie 3 | 4 | private final class PageViewControllerDelegateTests: XCTestCase { 5 | func testScrollToPage() { 6 | let viewControllers = ContiguousArray((0...5).map { _ in MockPagebableViewController() }) 7 | let pageViewController = PageViewController( 8 | viewControllers: viewControllers, 9 | maxChildrenCount: 3, 10 | initialPageIndex: 0 11 | ) 12 | 13 | let delegate = MockDelegate() 14 | pageViewController.delegate = delegate 15 | pageViewController.view.layoutIfNeeded() 16 | 17 | pageViewController.scrollToPage(at: 0, animated: false) 18 | XCTAssertEqual(delegate.ratio, 0) 19 | 20 | pageViewController.scrollToPage(at: 5, animated: false) 21 | XCTAssertEqual(delegate.ratio, 1 - (CGFloat(1) / 6)) 22 | 23 | pageViewController.scrollToPage(at: 3, animated: false) 24 | XCTAssertEqual(delegate.ratio, 1 - (CGFloat(1) / 2)) 25 | } 26 | } 27 | 28 | private final class MockDelegate: PageViewControllerDelegate { 29 | private(set) var ratio: CGFloat? 30 | 31 | func pageViewController(_ pageViewController: PageViewController, didScrollAtRatio ratio: CGFloat) { 32 | self.ratio = ratio 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ChausieTests/Tests/PageViewControllerTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Chausie 3 | 4 | private final class PageViewControllerTests: XCTestCase { 5 | var viewControllers: ContiguousArray! 6 | var pageViewController: PageViewController! 7 | var addedViews: [UIView]! 8 | 9 | override func setUp() { 10 | viewControllers = ContiguousArray((0...4).map { _ in MockPagebableViewController() }) 11 | pageViewController = PageViewController( 12 | viewControllers: viewControllers, 13 | maxChildrenCount: 3, 14 | initialPageIndex: 0 15 | ) 16 | pageViewController.view.layoutIfNeeded() 17 | 18 | updateViews() 19 | } 20 | 21 | func testSubviews() { 22 | XCTAssertEqual(addedViews.count, 3) 23 | 24 | XCTAssertTrue(addedViews.contains(viewControllers[0].view)) 25 | XCTAssertTrue(addedViews.contains(viewControllers[1].view)) 26 | XCTAssertTrue(addedViews.contains(viewControllers[2].view)) 27 | XCTAssertFalse(addedViews.contains(viewControllers[3].view)) 28 | XCTAssertFalse(addedViews.contains(viewControllers[4].view)) 29 | } 30 | 31 | func testSubviewsWhenScrollToSecondIndex() { 32 | pageViewController.scrollToPage(at: 2, animated: false) 33 | updateViews() 34 | 35 | XCTAssertFalse(addedViews.contains(viewControllers[0].view)) 36 | XCTAssertTrue(addedViews.contains(viewControllers[1].view)) 37 | XCTAssertTrue(addedViews.contains(viewControllers[2].view)) 38 | XCTAssertTrue(addedViews.contains(viewControllers[3].view)) 39 | XCTAssertFalse(addedViews.contains(viewControllers[4].view)) 40 | } 41 | 42 | func testSubviewsWhenScrollToLastIndex() { 43 | pageViewController.scrollToPage(at: 4, animated: false) 44 | updateViews() 45 | 46 | XCTAssertFalse(addedViews.contains(viewControllers[0].view)) 47 | XCTAssertFalse(addedViews.contains(viewControllers[1].view)) 48 | XCTAssertTrue(addedViews.contains(viewControllers[2].view)) 49 | XCTAssertTrue(addedViews.contains(viewControllers[3].view)) 50 | XCTAssertTrue(addedViews.contains(viewControllers[4].view)) 51 | } 52 | 53 | func testViewFrame() { 54 | let size = pageViewController.view.bounds.size 55 | XCTAssertEqual( 56 | pageViewController.viewControllers[0].view.frame, 57 | CGRect( 58 | origin: CGPoint(x: size.width * 0, y: 0), 59 | size: size 60 | ) 61 | ) 62 | XCTAssertEqual( 63 | pageViewController.viewControllers[1].view.frame, 64 | CGRect( 65 | origin: CGPoint(x: size.width * 1, y: 0), 66 | size: size 67 | ) 68 | ) 69 | XCTAssertEqual( 70 | pageViewController.viewControllers[2].view.frame, 71 | CGRect( 72 | origin: CGPoint(x: size.width * 2, y: 0), 73 | size: size 74 | ) 75 | ) 76 | XCTAssertEqual( 77 | pageViewController.viewControllers[3].view.frame.size, 78 | size 79 | ) 80 | } 81 | 82 | func testViewFrameAfterRotation() { 83 | let window = UIWindow() 84 | window.addSubview(pageViewController.view) 85 | let currentSize = pageViewController.view.bounds.size 86 | let newSize = CGSize(width: currentSize.height, height: currentSize.width) 87 | 88 | pageViewController.view.bounds.size = newSize 89 | pageViewController.view.layoutIfNeeded() 90 | 91 | pageViewController.viewWillTransition(to: newSize, with: MockTransitionCoordinator()) 92 | 93 | XCTAssertEqual( 94 | pageViewController.viewControllers[0].view.frame, 95 | CGRect( 96 | origin: CGPoint(x: newSize.width * 0, y: 0), 97 | size: newSize 98 | ) 99 | ) 100 | XCTAssertEqual( 101 | pageViewController.viewControllers[1].view.frame, 102 | CGRect( 103 | origin: CGPoint(x: newSize.width * 1, y: 0), 104 | size: newSize 105 | ) 106 | ) 107 | XCTAssertEqual( 108 | pageViewController.viewControllers[2].view.frame, 109 | CGRect( 110 | origin: CGPoint(x: newSize.width * 2, y: 0), 111 | size: newSize 112 | ) 113 | ) 114 | XCTAssertEqual( 115 | pageViewController.viewControllers[3].view.frame.size, 116 | newSize 117 | ) 118 | } 119 | 120 | private func updateViews() { 121 | addedViews = pageViewController.view.subviews 122 | .first(where: { $0 is UIScrollView })? 123 | .subviews 124 | .filter { (v: UIView) -> Bool in 125 | viewControllers.contains(where: { (vc: PageViewController.Child) in vc.view == v }) 126 | } 127 | } 128 | } 129 | 130 | private final class MockTransitionCoordinator: NSObject, UIViewControllerTransitionCoordinator { 131 | var isAnimated: Bool = false 132 | var presentationStyle: UIModalPresentationStyle = .none 133 | var initiallyInteractive: Bool = false 134 | var isInterruptible: Bool = false 135 | var isInteractive: Bool = false 136 | var isCancelled: Bool = false 137 | var transitionDuration: TimeInterval = 0 138 | var percentComplete: CGFloat = 1 139 | var completionVelocity: CGFloat = 0 140 | var completionCurve: UIView.AnimationCurve = .linear 141 | var containerView: UIView = UIView() 142 | var targetTransform: CGAffineTransform = .identity 143 | 144 | func viewController(forKey key: UITransitionContextViewControllerKey) -> UIViewController? { 145 | return nil 146 | } 147 | 148 | func view(forKey key: UITransitionContextViewKey) -> UIView? { 149 | return nil 150 | } 151 | 152 | func animate(alongsideTransition animation: ((UIViewControllerTransitionCoordinatorContext) -> Void)?, completion: ((UIViewControllerTransitionCoordinatorContext) -> Void)? = nil) -> Bool { 153 | animation?(self) 154 | completion?(self) 155 | return false 156 | } 157 | 158 | func animateAlongsideTransition(in view: UIView?, animation: ((UIViewControllerTransitionCoordinatorContext) -> Void)?, completion: ((UIViewControllerTransitionCoordinatorContext) -> Void)? = nil) -> Bool { 159 | animation?(self) 160 | completion?(self) 161 | return false 162 | } 163 | 164 | func notifyWhenInteractionEnds(_ handler: @escaping (UIViewControllerTransitionCoordinatorContext) -> Void) {} 165 | 166 | func notifyWhenInteractionChanges(_ handler: @escaping (UIViewControllerTransitionCoordinatorContext) -> Void) {} 167 | } 168 | -------------------------------------------------------------------------------- /ChausieTests/Tests/PageableTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Chausie 3 | 4 | private final class PageableTests: XCTestCase { 5 | var didAppearEvents: [PageIndex]! 6 | var didDisappearEvents: [PageIndex]! 7 | var viewControllers: ContiguousArray! 8 | var pageViewController: PageViewController! 9 | 10 | override func setUp() { 11 | didAppearEvents = [] 12 | didDisappearEvents = [] 13 | } 14 | 15 | override func tearDown() { 16 | didAppearEvents.removeAll() 17 | didDisappearEvents.removeAll() 18 | } 19 | 20 | func testIniaialPageableMethodBehavior() { 21 | let mocks = (0...2).map { [weak self] _ -> MockViewController in 22 | let controller = MockViewController() 23 | controller.didAppearAt = { self?.didAppearEvents.append($0) } 24 | controller.didDisappearAt = { self?.didDisappearEvents.append($0) } 25 | return controller 26 | } 27 | viewControllers = ContiguousArray(mocks) 28 | pageViewController = PageViewController( 29 | viewControllers: viewControllers, 30 | maxChildrenCount: 1, 31 | initialPageIndex: 0 32 | ) 33 | pageViewController.view.layoutIfNeeded() 34 | 35 | pageViewController.viewDidAppear(true) 36 | pageViewController.viewDidDisappear(true) 37 | 38 | XCTAssertEqual(didAppearEvents, [0]) 39 | XCTAssertEqual(didDisappearEvents, [0]) 40 | } 41 | 42 | func testPageableMethodBehaviorAfterScroll() { 43 | let mocks = (0...2).map { [weak self] _ -> MockViewController in 44 | let controller = MockViewController() 45 | controller.didAppearAt = { self?.didAppearEvents.append($0) } 46 | controller.didDisappearAt = { self?.didDisappearEvents.append($0) } 47 | return controller 48 | } 49 | viewControllers = ContiguousArray(mocks) 50 | pageViewController = PageViewController( 51 | viewControllers: viewControllers, 52 | maxChildrenCount: 1, 53 | initialPageIndex: 0 54 | ) 55 | pageViewController.view.layoutIfNeeded() 56 | 57 | pageViewController.viewDidAppear(true) 58 | 59 | pageViewController.scrollToPage(at: 1, animated: false) 60 | 61 | XCTAssertEqual(didAppearEvents, [0, 1]) 62 | XCTAssertEqual(didDisappearEvents, [0]) 63 | 64 | pageViewController.scrollToPage(at: 2, animated: false) 65 | XCTAssertEqual(didAppearEvents, [0, 1, 2]) 66 | XCTAssertEqual(didDisappearEvents, [0, 1]) 67 | 68 | pageViewController.viewDidDisappear(true) 69 | XCTAssertEqual(didAppearEvents, [0, 1, 2]) 70 | XCTAssertEqual(didDisappearEvents, [0, 1, 2]) 71 | } 72 | 73 | func testPageableMethodBehaviorAfterScrollForMaxChildrenCount() { 74 | let mocks = (0...2).map { [weak self] _ -> MockViewController in 75 | let controller = MockViewController() 76 | controller.didAppearAt = { self?.didAppearEvents.append($0) } 77 | controller.didDisappearAt = { self?.didDisappearEvents.append($0) } 78 | return controller 79 | } 80 | viewControllers = ContiguousArray(mocks) 81 | pageViewController = PageViewController( 82 | viewControllers: viewControllers, 83 | maxChildrenCount: mocks.count, 84 | initialPageIndex: 0 85 | ) 86 | pageViewController.view.layoutIfNeeded() 87 | 88 | pageViewController.viewDidAppear(true) 89 | 90 | pageViewController.scrollToPage(at: 1, animated: false) 91 | 92 | XCTAssertEqual(didAppearEvents, [0, 1]) 93 | XCTAssertEqual(didDisappearEvents, [0]) 94 | 95 | pageViewController.scrollToPage(at: 2, animated: false) 96 | XCTAssertEqual(didAppearEvents, [0, 1, 2]) 97 | XCTAssertEqual(didDisappearEvents, [0, 1]) 98 | 99 | pageViewController.viewDidDisappear(true) 100 | XCTAssertEqual(didAppearEvents, [0, 1, 2]) 101 | XCTAssertEqual(didDisappearEvents, [0, 1, 2]) 102 | } 103 | } 104 | 105 | private final class MockViewController: UIViewController, Pageable { 106 | var didAppearAt: ((PageIndex) -> Void)? 107 | var didDisappearAt: ((PageIndex) -> Void)? 108 | 109 | func pageViewController(_ pageViewController: PageViewController, didAppearAt index: PageIndex) { 110 | didAppearAt?(index) 111 | } 112 | 113 | func pageViewController(_ pageViewController: PageViewController, didDisappearAt index: PageIndex) { 114 | didDisappearAt?(index) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /ChausieTests/Tests/TabPageViewControllerDelegateTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Chausie 3 | 4 | private final class TabPageViewControllerDelegateTests: XCTestCase { 5 | func testScrollToPage() { 6 | typealias Composer = TabPageComposer 7 | typealias Component = Composer.Component 8 | 9 | let components = [ 10 | Component(child: MockPagebableViewController(), cellModel: "0"), 11 | Component(child: MockPagebableViewController(), cellModel: "1"), 12 | Component(child: MockPagebableViewController(), cellModel: "2"), 13 | Component(child: MockPagebableViewController(), cellModel: "3"), 14 | ] 15 | 16 | let tabPageViewController = TabPageViewController( 17 | composer: Composer(components: components), 18 | tabViewLayout: TabViewLayout( 19 | top: Position(origin: .safeArea, distance: 0), 20 | trailing: Position(origin: .view, distance: 10), 21 | leading: Position(origin: .view, distance: 10), 22 | height: 100 23 | ) 24 | ) 25 | 26 | let delegate = MockDelegate() 27 | tabPageViewController.delegate = delegate 28 | tabPageViewController.pageViewController.view.layoutIfNeeded() 29 | tabPageViewController.view.layoutIfNeeded() 30 | 31 | tabPageViewController.pageViewController.scrollToPage(at: 0, animated: false) 32 | XCTAssertEqual(delegate.ratio, 0) 33 | 34 | tabPageViewController.pageViewController.scrollToPage(at: 1, animated: false) 35 | XCTAssertEqual(delegate.ratio, 0.25) 36 | 37 | tabPageViewController.pageViewController.scrollToPage(at: 2, animated: false) 38 | XCTAssertEqual(delegate.ratio, 0.5) 39 | 40 | tabPageViewController.pageViewController.scrollToPage(at: 3, animated: false) 41 | XCTAssertEqual(delegate.ratio, 0.75) 42 | } 43 | } 44 | 45 | private final class MockDelegate: TabPageViewControllerDelegate { 46 | private(set) var ratio: CGFloat? 47 | 48 | func tabPageViewController(_ tabPageViewController: TabPageViewController, didScrollAtRatio ratio: CGFloat) { 49 | self.ratio = ratio 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /ChausieTests/Tests/TabPageViewControllerTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Chausie 3 | 4 | private final class TabPageViewControllerTests: XCTestCase { 5 | var tabPageViewController: TabPageViewController! 6 | 7 | override func setUp() { 8 | typealias Composer = TabPageComposer 9 | typealias Component = Composer.Component 10 | 11 | let components = [ 12 | Component(child: MockPagebableViewController(), cellModel: "0"), 13 | Component(child: MockPagebableViewController(), cellModel: "1"), 14 | Component(child: MockPagebableViewController(), cellModel: "2"), 15 | Component(child: MockPagebableViewController(), cellModel: "3"), 16 | ] 17 | 18 | tabPageViewController = TabPageViewController( 19 | composer: Composer(components: components), 20 | tabViewLayout: TabViewLayout( 21 | top: Position(origin: .safeArea, distance: 0), 22 | trailing: Position(origin: .view, distance: 10), 23 | leading: Position(origin: .view, distance: 10), 24 | height: 100 25 | ) 26 | ) 27 | } 28 | 29 | func testLayout() { 30 | tabPageViewController.view.frame.size = CGSize(width: 300, height: 500) 31 | tabPageViewController.view.layoutIfNeeded() 32 | XCTAssertEqual( 33 | tabPageViewController.tabView.frame, 34 | CGRect(x: 10, y: 0, width: 280, height: 100) 35 | ) 36 | XCTAssertEqual( 37 | tabPageViewController.pageViewController.view.frame, 38 | CGRect(x: 0, y: 100, width: 300, height: 400) 39 | ) 40 | } 41 | 42 | func testHighlightRatio() { 43 | tabPageViewController.pageViewController.scrollToPage(at: 2, animated: false) 44 | let subviews = tabPageViewController.tabView.subviews.compactMap { $0 as? MockTabCell } 45 | XCTAssertEqual(subviews[2].ratio, 1) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ChausieTests/Tests/TabViewTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Chausie 3 | 4 | private final class TabViewTests: XCTestCase { 5 | var tabView: TabView! 6 | let models = ["a", "b", "c"] 7 | 8 | override func setUp() { 9 | tabView = TabView() 10 | tabView.renderCells(with: models, cellGenerator: { MockTabCell.make(model: $0) }) 11 | tabView.frame.size = CGSize(width: 200, height: 100) 12 | tabView.layoutIfNeeded() 13 | } 14 | 15 | func testCellCount() { 16 | let cells = tabView.cells.filter { $0 is MockTabCell } 17 | XCTAssertEqual(cells.count, models.count) 18 | } 19 | 20 | func testCellModel() { 21 | let cells = tabView.cells.compactMap { $0 as? MockTabCell } 22 | XCTAssertEqual(cells[0].titleLabel?.text, models[0]) 23 | XCTAssertEqual(cells[1].titleLabel?.text, models[1]) 24 | XCTAssertEqual(cells[2].titleLabel?.text, models[2]) 25 | } 26 | 27 | func testCellHighlightRatio() { 28 | let cells = tabView.cells.compactMap { $0 as? MockTabCell } 29 | 30 | tabView.highlightCells(withRatio: 0) 31 | XCTAssertEqual(cells[0].ratio, 1) 32 | XCTAssertEqual(cells[1].ratio, 0) 33 | XCTAssertEqual(cells[2].ratio, 0) 34 | 35 | tabView.highlightCells(withRatio: 1 / CGFloat(models.count)) 36 | XCTAssertEqual(cells[0].ratio, 0) 37 | XCTAssertEqual(cells[1].ratio, 1) 38 | XCTAssertEqual(cells[2].ratio, 0) 39 | 40 | tabView.highlightCells(withRatio: 2 / CGFloat(models.count)) 41 | XCTAssertEqual(cells[0].ratio, 0) 42 | XCTAssertEqual(cells[1].ratio, 0) 43 | XCTAssertEqual(cells[2].ratio, 1) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Examples/ChausieExample/ChausieExample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | E04FEB8A2278228C0084666D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E04FEB892278228C0084666D /* AppDelegate.swift */; }; 11 | E04FEB8F2278228C0084666D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E04FEB8D2278228C0084666D /* Main.storyboard */; }; 12 | E04FEB912278228D0084666D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E04FEB902278228D0084666D /* Assets.xcassets */; }; 13 | E04FEB942278228D0084666D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E04FEB922278228D0084666D /* LaunchScreen.storyboard */; }; 14 | E04FEBA3227823BA0084666D /* RootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E04FEB9F227823BA0084666D /* RootViewController.swift */; }; 15 | E04FEBA4227823BA0084666D /* TableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E04FEBA0227823BA0084666D /* TableViewController.swift */; }; 16 | E04FEBA5227823BA0084666D /* HasCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E04FEBA1227823BA0084666D /* HasCategory.swift */; }; 17 | E04FEBA6227823BA0084666D /* CollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E04FEBA2227823BA0084666D /* CollectionViewController.swift */; }; 18 | E04FEBAA227823C30084666D /* ProgressBorderTabPageComposer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E04FEBA7227823C30084666D /* ProgressBorderTabPageComposer.swift */; }; 19 | E04FEBAB227823C30084666D /* ProgressBorderTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E04FEBA8227823C30084666D /* ProgressBorderTabView.swift */; }; 20 | E04FEBAC227823C30084666D /* TabItemButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E04FEBA9227823C30084666D /* TabItemButton.swift */; }; 21 | E04FEBB1227823CD0084666D /* ImageTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E04FEBAD227823CD0084666D /* ImageTableViewCell.swift */; }; 22 | E04FEBB2227823CD0084666D /* ImageCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E04FEBAE227823CD0084666D /* ImageCollectionViewCell.swift */; }; 23 | E04FEBB3227823CD0084666D /* ImageCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = E04FEBAF227823CD0084666D /* ImageCollectionViewCell.xib */; }; 24 | E04FEBB4227823CD0084666D /* ImageTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = E04FEBB0227823CD0084666D /* ImageTableViewCell.xib */; }; 25 | E04FEBB7227823D50084666D /* TabViewLayout+Utility.swift in Sources */ = {isa = PBXBuildFile; fileRef = E04FEBB5227823D50084666D /* TabViewLayout+Utility.swift */; }; 26 | E04FEBB8227823D50084666D /* TabPageViewController+Utility.swift in Sources */ = {isa = PBXBuildFile; fileRef = E04FEBB6227823D50084666D /* TabPageViewController+Utility.swift */; }; 27 | E04FEC192278428F0084666D /* Chausie.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E04FEC182278428F0084666D /* Chausie.framework */; }; 28 | E04FEC1D227842B40084666D /* Category.swift in Sources */ = {isa = PBXBuildFile; fileRef = E04FEC1A227842B40084666D /* Category.swift */; }; 29 | E04FEC1E227842B40084666D /* ImageDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = E04FEC1B227842B40084666D /* ImageDownloader.swift */; }; 30 | E04FEC1F227842B40084666D /* URLBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = E04FEC1C227842B40084666D /* URLBuilder.swift */; }; 31 | /* End PBXBuildFile section */ 32 | 33 | /* Begin PBXFileReference section */ 34 | E04FEB862278228C0084666D /* ChausieExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ChausieExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 35 | E04FEB892278228C0084666D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 36 | E04FEB8E2278228C0084666D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 37 | E04FEB902278228D0084666D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 38 | E04FEB932278228D0084666D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 39 | E04FEB952278228D0084666D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 40 | E04FEB9F227823BA0084666D /* RootViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RootViewController.swift; sourceTree = ""; }; 41 | E04FEBA0227823BA0084666D /* TableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableViewController.swift; sourceTree = ""; }; 42 | E04FEBA1227823BA0084666D /* HasCategory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HasCategory.swift; sourceTree = ""; }; 43 | E04FEBA2227823BA0084666D /* CollectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionViewController.swift; sourceTree = ""; }; 44 | E04FEBA7227823C30084666D /* ProgressBorderTabPageComposer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgressBorderTabPageComposer.swift; sourceTree = ""; }; 45 | E04FEBA8227823C30084666D /* ProgressBorderTabView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgressBorderTabView.swift; sourceTree = ""; }; 46 | E04FEBA9227823C30084666D /* TabItemButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabItemButton.swift; sourceTree = ""; }; 47 | E04FEBAD227823CD0084666D /* ImageTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageTableViewCell.swift; sourceTree = ""; }; 48 | E04FEBAE227823CD0084666D /* ImageCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageCollectionViewCell.swift; sourceTree = ""; }; 49 | E04FEBAF227823CD0084666D /* ImageCollectionViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ImageCollectionViewCell.xib; sourceTree = ""; }; 50 | E04FEBB0227823CD0084666D /* ImageTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ImageTableViewCell.xib; sourceTree = ""; }; 51 | E04FEBB5227823D50084666D /* TabViewLayout+Utility.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "TabViewLayout+Utility.swift"; sourceTree = ""; }; 52 | E04FEBB6227823D50084666D /* TabPageViewController+Utility.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "TabPageViewController+Utility.swift"; sourceTree = ""; }; 53 | E04FEBBA22782A7A0084666D /* Chausie.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Chausie.framework; path = ../Carthage/Build/iOS/Chausie.framework; sourceTree = ""; }; 54 | E04FEC182278428F0084666D /* Chausie.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Chausie.framework; path = Carthage/Build/iOS/Chausie.framework; sourceTree = ""; }; 55 | E04FEC1A227842B40084666D /* Category.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Category.swift; sourceTree = ""; }; 56 | E04FEC1B227842B40084666D /* ImageDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageDownloader.swift; sourceTree = ""; }; 57 | E04FEC1C227842B40084666D /* URLBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLBuilder.swift; sourceTree = ""; }; 58 | /* End PBXFileReference section */ 59 | 60 | /* Begin PBXFrameworksBuildPhase section */ 61 | E04FEB832278228C0084666D /* Frameworks */ = { 62 | isa = PBXFrameworksBuildPhase; 63 | buildActionMask = 2147483647; 64 | files = ( 65 | E04FEC192278428F0084666D /* Chausie.framework in Frameworks */, 66 | ); 67 | runOnlyForDeploymentPostprocessing = 0; 68 | }; 69 | /* End PBXFrameworksBuildPhase section */ 70 | 71 | /* Begin PBXGroup section */ 72 | E04FEB7D2278228C0084666D = { 73 | isa = PBXGroup; 74 | children = ( 75 | E04FEB882278228C0084666D /* ChausieExample */, 76 | E04FEB872278228C0084666D /* Products */, 77 | E04FEBB922782A7A0084666D /* Frameworks */, 78 | ); 79 | sourceTree = ""; 80 | }; 81 | E04FEB872278228C0084666D /* Products */ = { 82 | isa = PBXGroup; 83 | children = ( 84 | E04FEB862278228C0084666D /* ChausieExample.app */, 85 | ); 86 | name = Products; 87 | sourceTree = ""; 88 | }; 89 | E04FEB882278228C0084666D /* ChausieExample */ = { 90 | isa = PBXGroup; 91 | children = ( 92 | E04FEB9E227823700084666D /* ViewControllers */, 93 | E04FEB9D2278232B0084666D /* Chausie Components */, 94 | E04FEB9C227823240084666D /* Views */, 95 | E04FEB9B2278231C0084666D /* Extensions */, 96 | E04FEB892278228C0084666D /* AppDelegate.swift */, 97 | E04FEC1A227842B40084666D /* Category.swift */, 98 | E04FEC1B227842B40084666D /* ImageDownloader.swift */, 99 | E04FEC1C227842B40084666D /* URLBuilder.swift */, 100 | E04FEB8D2278228C0084666D /* Main.storyboard */, 101 | E04FEB902278228D0084666D /* Assets.xcassets */, 102 | E04FEB922278228D0084666D /* LaunchScreen.storyboard */, 103 | E04FEB952278228D0084666D /* Info.plist */, 104 | ); 105 | path = ChausieExample; 106 | sourceTree = ""; 107 | }; 108 | E04FEB9B2278231C0084666D /* Extensions */ = { 109 | isa = PBXGroup; 110 | children = ( 111 | E04FEBB6227823D50084666D /* TabPageViewController+Utility.swift */, 112 | E04FEBB5227823D50084666D /* TabViewLayout+Utility.swift */, 113 | ); 114 | path = Extensions; 115 | sourceTree = ""; 116 | }; 117 | E04FEB9C227823240084666D /* Views */ = { 118 | isa = PBXGroup; 119 | children = ( 120 | E04FEBAE227823CD0084666D /* ImageCollectionViewCell.swift */, 121 | E04FEBAF227823CD0084666D /* ImageCollectionViewCell.xib */, 122 | E04FEBAD227823CD0084666D /* ImageTableViewCell.swift */, 123 | E04FEBB0227823CD0084666D /* ImageTableViewCell.xib */, 124 | ); 125 | path = Views; 126 | sourceTree = ""; 127 | }; 128 | E04FEB9D2278232B0084666D /* Chausie Components */ = { 129 | isa = PBXGroup; 130 | children = ( 131 | E04FEBA7227823C30084666D /* ProgressBorderTabPageComposer.swift */, 132 | E04FEBA8227823C30084666D /* ProgressBorderTabView.swift */, 133 | E04FEBA9227823C30084666D /* TabItemButton.swift */, 134 | ); 135 | path = "Chausie Components"; 136 | sourceTree = ""; 137 | }; 138 | E04FEB9E227823700084666D /* ViewControllers */ = { 139 | isa = PBXGroup; 140 | children = ( 141 | E04FEBA2227823BA0084666D /* CollectionViewController.swift */, 142 | E04FEBA1227823BA0084666D /* HasCategory.swift */, 143 | E04FEBA0227823BA0084666D /* TableViewController.swift */, 144 | E04FEB9F227823BA0084666D /* RootViewController.swift */, 145 | ); 146 | path = ViewControllers; 147 | sourceTree = ""; 148 | }; 149 | E04FEBB922782A7A0084666D /* Frameworks */ = { 150 | isa = PBXGroup; 151 | children = ( 152 | E04FEC182278428F0084666D /* Chausie.framework */, 153 | E04FEBBA22782A7A0084666D /* Chausie.framework */, 154 | ); 155 | name = Frameworks; 156 | sourceTree = ""; 157 | }; 158 | /* End PBXGroup section */ 159 | 160 | /* Begin PBXNativeTarget section */ 161 | E04FEB852278228C0084666D /* ChausieExample */ = { 162 | isa = PBXNativeTarget; 163 | buildConfigurationList = E04FEB982278228D0084666D /* Build configuration list for PBXNativeTarget "ChausieExample" */; 164 | buildPhases = ( 165 | E04FEB822278228C0084666D /* Sources */, 166 | E04FEB832278228C0084666D /* Frameworks */, 167 | E04FEB842278228C0084666D /* Resources */, 168 | E04FEBBC22782A8C0084666D /* Carthage */, 169 | ); 170 | buildRules = ( 171 | ); 172 | dependencies = ( 173 | ); 174 | name = ChausieExample; 175 | productName = ChausieExample; 176 | productReference = E04FEB862278228C0084666D /* ChausieExample.app */; 177 | productType = "com.apple.product-type.application"; 178 | }; 179 | /* End PBXNativeTarget section */ 180 | 181 | /* Begin PBXProject section */ 182 | E04FEB7E2278228C0084666D /* Project object */ = { 183 | isa = PBXProject; 184 | attributes = { 185 | LastSwiftUpdateCheck = 1020; 186 | LastUpgradeCheck = 1020; 187 | ORGANIZATIONNAME = shoheiyokoyama; 188 | TargetAttributes = { 189 | E04FEB852278228C0084666D = { 190 | CreatedOnToolsVersion = 10.2.1; 191 | }; 192 | }; 193 | }; 194 | buildConfigurationList = E04FEB812278228C0084666D /* Build configuration list for PBXProject "ChausieExample" */; 195 | compatibilityVersion = "Xcode 9.3"; 196 | developmentRegion = en; 197 | hasScannedForEncodings = 0; 198 | knownRegions = ( 199 | en, 200 | Base, 201 | ); 202 | mainGroup = E04FEB7D2278228C0084666D; 203 | productRefGroup = E04FEB872278228C0084666D /* Products */; 204 | projectDirPath = ""; 205 | projectRoot = ""; 206 | targets = ( 207 | E04FEB852278228C0084666D /* ChausieExample */, 208 | ); 209 | }; 210 | /* End PBXProject section */ 211 | 212 | /* Begin PBXResourcesBuildPhase section */ 213 | E04FEB842278228C0084666D /* Resources */ = { 214 | isa = PBXResourcesBuildPhase; 215 | buildActionMask = 2147483647; 216 | files = ( 217 | E04FEBB4227823CD0084666D /* ImageTableViewCell.xib in Resources */, 218 | E04FEB942278228D0084666D /* LaunchScreen.storyboard in Resources */, 219 | E04FEB912278228D0084666D /* Assets.xcassets in Resources */, 220 | E04FEBB3227823CD0084666D /* ImageCollectionViewCell.xib in Resources */, 221 | E04FEB8F2278228C0084666D /* Main.storyboard in Resources */, 222 | ); 223 | runOnlyForDeploymentPostprocessing = 0; 224 | }; 225 | /* End PBXResourcesBuildPhase section */ 226 | 227 | /* Begin PBXShellScriptBuildPhase section */ 228 | E04FEBBC22782A8C0084666D /* Carthage */ = { 229 | isa = PBXShellScriptBuildPhase; 230 | buildActionMask = 2147483647; 231 | files = ( 232 | ); 233 | inputFileListPaths = ( 234 | ); 235 | inputPaths = ( 236 | "$(SRCROOT)/Carthage/Build/iOS/Chausie.framework", 237 | ); 238 | name = Carthage; 239 | outputFileListPaths = ( 240 | ); 241 | outputPaths = ( 242 | "$(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/Chausie.framework", 243 | ); 244 | runOnlyForDeploymentPostprocessing = 0; 245 | shellPath = /bin/sh; 246 | shellScript = "/usr/local/bin/carthage copy-frameworks\n"; 247 | }; 248 | /* End PBXShellScriptBuildPhase section */ 249 | 250 | /* Begin PBXSourcesBuildPhase section */ 251 | E04FEB822278228C0084666D /* Sources */ = { 252 | isa = PBXSourcesBuildPhase; 253 | buildActionMask = 2147483647; 254 | files = ( 255 | E04FEBAC227823C30084666D /* TabItemButton.swift in Sources */, 256 | E04FEBA3227823BA0084666D /* RootViewController.swift in Sources */, 257 | E04FEC1E227842B40084666D /* ImageDownloader.swift in Sources */, 258 | E04FEBB7227823D50084666D /* TabViewLayout+Utility.swift in Sources */, 259 | E04FEBAA227823C30084666D /* ProgressBorderTabPageComposer.swift in Sources */, 260 | E04FEC1F227842B40084666D /* URLBuilder.swift in Sources */, 261 | E04FEBB1227823CD0084666D /* ImageTableViewCell.swift in Sources */, 262 | E04FEBA4227823BA0084666D /* TableViewController.swift in Sources */, 263 | E04FEBA5227823BA0084666D /* HasCategory.swift in Sources */, 264 | E04FEBB2227823CD0084666D /* ImageCollectionViewCell.swift in Sources */, 265 | E04FEBB8227823D50084666D /* TabPageViewController+Utility.swift in Sources */, 266 | E04FEC1D227842B40084666D /* Category.swift in Sources */, 267 | E04FEB8A2278228C0084666D /* AppDelegate.swift in Sources */, 268 | E04FEBAB227823C30084666D /* ProgressBorderTabView.swift in Sources */, 269 | E04FEBA6227823BA0084666D /* CollectionViewController.swift in Sources */, 270 | ); 271 | runOnlyForDeploymentPostprocessing = 0; 272 | }; 273 | /* End PBXSourcesBuildPhase section */ 274 | 275 | /* Begin PBXVariantGroup section */ 276 | E04FEB8D2278228C0084666D /* Main.storyboard */ = { 277 | isa = PBXVariantGroup; 278 | children = ( 279 | E04FEB8E2278228C0084666D /* Base */, 280 | ); 281 | name = Main.storyboard; 282 | sourceTree = ""; 283 | }; 284 | E04FEB922278228D0084666D /* LaunchScreen.storyboard */ = { 285 | isa = PBXVariantGroup; 286 | children = ( 287 | E04FEB932278228D0084666D /* Base */, 288 | ); 289 | name = LaunchScreen.storyboard; 290 | sourceTree = ""; 291 | }; 292 | /* End PBXVariantGroup section */ 293 | 294 | /* Begin XCBuildConfiguration section */ 295 | E04FEB962278228D0084666D /* Debug */ = { 296 | isa = XCBuildConfiguration; 297 | buildSettings = { 298 | ALWAYS_SEARCH_USER_PATHS = NO; 299 | CLANG_ANALYZER_NONNULL = YES; 300 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 301 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 302 | CLANG_CXX_LIBRARY = "libc++"; 303 | CLANG_ENABLE_MODULES = YES; 304 | CLANG_ENABLE_OBJC_ARC = YES; 305 | CLANG_ENABLE_OBJC_WEAK = YES; 306 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 307 | CLANG_WARN_BOOL_CONVERSION = YES; 308 | CLANG_WARN_COMMA = YES; 309 | CLANG_WARN_CONSTANT_CONVERSION = YES; 310 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 311 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 312 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 313 | CLANG_WARN_EMPTY_BODY = YES; 314 | CLANG_WARN_ENUM_CONVERSION = YES; 315 | CLANG_WARN_INFINITE_RECURSION = YES; 316 | CLANG_WARN_INT_CONVERSION = YES; 317 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 318 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 319 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 320 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 321 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 322 | CLANG_WARN_STRICT_PROTOTYPES = YES; 323 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 324 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 325 | CLANG_WARN_UNREACHABLE_CODE = YES; 326 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 327 | CODE_SIGN_IDENTITY = "iPhone Developer"; 328 | COPY_PHASE_STRIP = NO; 329 | DEBUG_INFORMATION_FORMAT = dwarf; 330 | ENABLE_STRICT_OBJC_MSGSEND = YES; 331 | ENABLE_TESTABILITY = YES; 332 | GCC_C_LANGUAGE_STANDARD = gnu11; 333 | GCC_DYNAMIC_NO_PIC = NO; 334 | GCC_NO_COMMON_BLOCKS = YES; 335 | GCC_OPTIMIZATION_LEVEL = 0; 336 | GCC_PREPROCESSOR_DEFINITIONS = ( 337 | "DEBUG=1", 338 | "$(inherited)", 339 | ); 340 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 341 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 342 | GCC_WARN_UNDECLARED_SELECTOR = YES; 343 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 344 | GCC_WARN_UNUSED_FUNCTION = YES; 345 | GCC_WARN_UNUSED_VARIABLE = YES; 346 | IPHONEOS_DEPLOYMENT_TARGET = 12.2; 347 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 348 | MTL_FAST_MATH = YES; 349 | ONLY_ACTIVE_ARCH = YES; 350 | SDKROOT = iphoneos; 351 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 352 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 353 | }; 354 | name = Debug; 355 | }; 356 | E04FEB972278228D0084666D /* Release */ = { 357 | isa = XCBuildConfiguration; 358 | buildSettings = { 359 | ALWAYS_SEARCH_USER_PATHS = NO; 360 | CLANG_ANALYZER_NONNULL = YES; 361 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 362 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 363 | CLANG_CXX_LIBRARY = "libc++"; 364 | CLANG_ENABLE_MODULES = YES; 365 | CLANG_ENABLE_OBJC_ARC = YES; 366 | CLANG_ENABLE_OBJC_WEAK = YES; 367 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 368 | CLANG_WARN_BOOL_CONVERSION = YES; 369 | CLANG_WARN_COMMA = YES; 370 | CLANG_WARN_CONSTANT_CONVERSION = YES; 371 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 372 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 373 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 374 | CLANG_WARN_EMPTY_BODY = YES; 375 | CLANG_WARN_ENUM_CONVERSION = YES; 376 | CLANG_WARN_INFINITE_RECURSION = YES; 377 | CLANG_WARN_INT_CONVERSION = YES; 378 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 379 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 380 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 381 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 382 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 383 | CLANG_WARN_STRICT_PROTOTYPES = YES; 384 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 385 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 386 | CLANG_WARN_UNREACHABLE_CODE = YES; 387 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 388 | CODE_SIGN_IDENTITY = "iPhone Developer"; 389 | COPY_PHASE_STRIP = NO; 390 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 391 | ENABLE_NS_ASSERTIONS = NO; 392 | ENABLE_STRICT_OBJC_MSGSEND = YES; 393 | GCC_C_LANGUAGE_STANDARD = gnu11; 394 | GCC_NO_COMMON_BLOCKS = YES; 395 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 396 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 397 | GCC_WARN_UNDECLARED_SELECTOR = YES; 398 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 399 | GCC_WARN_UNUSED_FUNCTION = YES; 400 | GCC_WARN_UNUSED_VARIABLE = YES; 401 | IPHONEOS_DEPLOYMENT_TARGET = 12.2; 402 | MTL_ENABLE_DEBUG_INFO = NO; 403 | MTL_FAST_MATH = YES; 404 | SDKROOT = iphoneos; 405 | SWIFT_COMPILATION_MODE = wholemodule; 406 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 407 | VALIDATE_PRODUCT = YES; 408 | }; 409 | name = Release; 410 | }; 411 | E04FEB992278228D0084666D /* Debug */ = { 412 | isa = XCBuildConfiguration; 413 | buildSettings = { 414 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 415 | CODE_SIGN_STYLE = Automatic; 416 | FRAMEWORK_SEARCH_PATHS = ( 417 | "$(inherited)", 418 | "$(PROJECT_DIR)/Carthage/Build/iOS", 419 | ); 420 | INFOPLIST_FILE = ChausieExample/Info.plist; 421 | LD_RUNPATH_SEARCH_PATHS = ( 422 | "$(inherited)", 423 | "@executable_path/Frameworks", 424 | ); 425 | PRODUCT_BUNDLE_IDENTIFIER = jp.co.shoheiyokoyama.ChausieExample; 426 | PRODUCT_NAME = "$(TARGET_NAME)"; 427 | SWIFT_VERSION = 5.0; 428 | TARGETED_DEVICE_FAMILY = "1,2"; 429 | }; 430 | name = Debug; 431 | }; 432 | E04FEB9A2278228D0084666D /* Release */ = { 433 | isa = XCBuildConfiguration; 434 | buildSettings = { 435 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 436 | CODE_SIGN_STYLE = Automatic; 437 | FRAMEWORK_SEARCH_PATHS = ( 438 | "$(inherited)", 439 | "$(PROJECT_DIR)/Carthage/Build/iOS", 440 | ); 441 | INFOPLIST_FILE = ChausieExample/Info.plist; 442 | LD_RUNPATH_SEARCH_PATHS = ( 443 | "$(inherited)", 444 | "@executable_path/Frameworks", 445 | ); 446 | PRODUCT_BUNDLE_IDENTIFIER = jp.co.shoheiyokoyama.ChausieExample; 447 | PRODUCT_NAME = "$(TARGET_NAME)"; 448 | SWIFT_VERSION = 5.0; 449 | TARGETED_DEVICE_FAMILY = "1,2"; 450 | }; 451 | name = Release; 452 | }; 453 | /* End XCBuildConfiguration section */ 454 | 455 | /* Begin XCConfigurationList section */ 456 | E04FEB812278228C0084666D /* Build configuration list for PBXProject "ChausieExample" */ = { 457 | isa = XCConfigurationList; 458 | buildConfigurations = ( 459 | E04FEB962278228D0084666D /* Debug */, 460 | E04FEB972278228D0084666D /* Release */, 461 | ); 462 | defaultConfigurationIsVisible = 0; 463 | defaultConfigurationName = Release; 464 | }; 465 | E04FEB982278228D0084666D /* Build configuration list for PBXNativeTarget "ChausieExample" */ = { 466 | isa = XCConfigurationList; 467 | buildConfigurations = ( 468 | E04FEB992278228D0084666D /* Debug */, 469 | E04FEB9A2278228D0084666D /* Release */, 470 | ); 471 | defaultConfigurationIsVisible = 0; 472 | defaultConfigurationName = Release; 473 | }; 474 | /* End XCConfigurationList section */ 475 | }; 476 | rootObject = E04FEB7E2278228C0084666D /* Project object */; 477 | } 478 | -------------------------------------------------------------------------------- /Examples/ChausieExample/ChausieExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/ChausieExample/ChausieExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/ChausieExample/ChausieExample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @UIApplicationMain 4 | final class AppDelegate: UIResponder, UIApplicationDelegate { 5 | 6 | var window: UIWindow? 7 | 8 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 9 | return true 10 | } 11 | } 12 | 13 | -------------------------------------------------------------------------------- /Examples/ChausieExample/ChausieExample/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 | } -------------------------------------------------------------------------------- /Examples/ChausieExample/ChausieExample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Examples/ChausieExample/ChausieExample/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 | -------------------------------------------------------------------------------- /Examples/ChausieExample/ChausieExample/Base.lproj/Main.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 | -------------------------------------------------------------------------------- /Examples/ChausieExample/ChausieExample/Category.swift: -------------------------------------------------------------------------------- 1 | enum Category: String { 2 | case sports 3 | case food 4 | case cats 5 | case technics 6 | case fashion 7 | } 8 | -------------------------------------------------------------------------------- /Examples/ChausieExample/ChausieExample/Chausie Components/ProgressBorderTabPageComposer.swift: -------------------------------------------------------------------------------- 1 | import Chausie 2 | 3 | final class ProgressBorderTabPageComposer: TabPageComposer { 4 | init(components: [TabPageComposer.Component]) { 5 | super.init( 6 | components: components, 7 | tabViewGenerator: { cellModels in 8 | let tabView = ProgressBorderTabView() 9 | tabView.renderCells(with: cellModels, cellGenerator: { TabCell.make(model: $0) }) 10 | return tabView 11 | } 12 | ) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Examples/ChausieExample/ChausieExample/Chausie Components/ProgressBorderTabView.swift: -------------------------------------------------------------------------------- 1 | import Chausie 2 | import UIKit 3 | 4 | final class ProgressBorderTabView: TabView { 5 | private let progressBorderView: UIView = { 6 | let view = UIView() 7 | view.backgroundColor = UIColor(red: 43 / 255, green: 87 / 255, blue: 151 / 255, alpha: 1) 8 | view.translatesAutoresizingMaskIntoConstraints = false 9 | return view 10 | }() 11 | 12 | private var boraderLeading: NSLayoutConstraint? 13 | 14 | override func highlightCells(withRatio ratio: CGFloat) { 15 | super.highlightCells(withRatio: ratio) 16 | 17 | boraderLeading?.constant = bounds.width * ratio 18 | } 19 | 20 | override func renderCells( 21 | with cellModels: [C.Model], 22 | cellGenerator: (C.Model) -> C 23 | ) { 24 | super.renderCells(with: cellModels, cellGenerator: cellGenerator) 25 | 26 | addSubview(progressBorderView) 27 | 28 | let constraints = [ 29 | progressBorderView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 1 / CGFloat(cellModels.count)), 30 | progressBorderView.bottomAnchor.constraint(equalTo: bottomAnchor), 31 | progressBorderView.heightAnchor.constraint(equalToConstant: 3) 32 | ] 33 | 34 | let leading = progressBorderView.leadingAnchor.constraint(equalTo: leadingAnchor) 35 | boraderLeading = leading 36 | 37 | NSLayoutConstraint.activate(constraints + [leading]) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Examples/ChausieExample/ChausieExample/Chausie Components/TabItemButton.swift: -------------------------------------------------------------------------------- 1 | import Chausie 2 | import UIKit 3 | 4 | final class TabItemButton: UIButton, TabItemCell { 5 | typealias Model = Category 6 | 7 | static func make(model: Model) -> Self { 8 | let button = self.init() 9 | button.setTitle(model.rawValue, for: .normal) 10 | button.setTitleColor(.lightGray, for: .normal) 11 | button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 14) 12 | return button 13 | } 14 | 15 | func updateHighlightRatio(_ ratio: CGFloat) { 16 | let highlightColor = UIColor(red: 43 / 255, green: 87 / 255, blue: 151 / 255, alpha: 1) 17 | let color = UIColor.lightGray.blend(with: highlightColor, multiplier: ratio) 18 | setTitleColor(color, for: .normal) 19 | 20 | let scale = ratio * 0.2 + 0.8 21 | let transfrom = CGAffineTransform(scaleX: scale, y: scale) 22 | self.transform = transfrom 23 | } 24 | } 25 | 26 | private extension UIColor { 27 | struct Components { 28 | let red, green, blue, alpha: CGFloat 29 | } 30 | 31 | var rgbaComponents: Components { 32 | var red: CGFloat = 0 33 | var green: CGFloat = 0 34 | var blue: CGFloat = 0 35 | var alpha: CGFloat = 0 36 | getRed(&red, green: &green, blue: &blue, alpha: &alpha) 37 | return Components(red: red, green: green, blue: blue, alpha: alpha) 38 | } 39 | 40 | func blend(with color: UIColor, multiplier: CGFloat) -> UIColor { 41 | let multiplier = min(max(multiplier, 0), 1) 42 | let components = rgbaComponents 43 | let blendComponents = color.rgbaComponents 44 | 45 | let blended: (KeyPath) -> CGFloat = { keyPath in 46 | let delta = blendComponents[keyPath: keyPath] - components[keyPath: keyPath] 47 | return delta * multiplier + components[keyPath: keyPath] 48 | } 49 | 50 | let red = blended(\.red) 51 | let green = blended(\.green) 52 | let blue = blended(\.blue) 53 | let alpha = blended(\.alpha) 54 | 55 | return UIColor(red: red, green: green, blue: blue, alpha: alpha) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Examples/ChausieExample/ChausieExample/Extensions/TabPageViewController+Utility.swift: -------------------------------------------------------------------------------- 1 | import Chausie 2 | 3 | typealias TabPageComponent = TabPageComposer.Component 4 | 5 | extension TabPageViewController { 6 | convenience init( 7 | components: [TabPageComposer.Component], 8 | maxChildrenCount: Int = 1, 9 | initialPageIndex: PageIndex = 0, 10 | tabViewLayout: TabViewLayout = .default 11 | ) { 12 | self.init( 13 | composer: ProgressBorderTabPageComposer(components: components), 14 | tabViewLayout: tabViewLayout, 15 | maxChildrenCount: maxChildrenCount, 16 | initialPageIndex: initialPageIndex 17 | ) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Examples/ChausieExample/ChausieExample/Extensions/TabViewLayout+Utility.swift: -------------------------------------------------------------------------------- 1 | import Chausie 2 | 3 | extension TabViewLayout { 4 | static var `default`: TabViewLayout { 5 | return TabViewLayout( 6 | top: Position(origin: .safeArea, distance: 0), 7 | trailing: Position(origin: .view, distance: 16), 8 | leading: Position(origin: .view, distance: 16), 9 | height: 44 10 | ) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Examples/ChausieExample/ChausieExample/ImageDownloader.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class ImageDownloader { 4 | private var currentTask: URLSessionDataTask? 5 | 6 | func download(url: URL, completionHandler: @escaping (_ data: Data?, _ error: Error?) -> Void) { 7 | currentTask = URLSession.shared.dataTask(with: url) { data, _, error in 8 | completionHandler(data, error) 9 | } 10 | currentTask?.resume() 11 | } 12 | 13 | func cancel() { 14 | currentTask?.cancel() 15 | currentTask = nil 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Examples/ChausieExample/ChausieExample/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 | Main 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 | -------------------------------------------------------------------------------- /Examples/ChausieExample/ChausieExample/URLBuilder.swift: -------------------------------------------------------------------------------- 1 | import CoreGraphics 2 | import Foundation 3 | 4 | enum URLBuilder { 5 | /// - SeeAlso: https://lorempixel.com/ 6 | static func build(category: Category, width: CGFloat, height: CGFloat) -> URL { 7 | let string = "https://lorempixel.com" 8 | + "/\(Int(width))" 9 | + "/\(Int(height))/" 10 | + "\(category)/" 11 | return URL(string: string)! 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Examples/ChausieExample/ChausieExample/ViewControllers/CollectionViewController.swift: -------------------------------------------------------------------------------- 1 | import Chausie 2 | import UIKit 3 | 4 | final class CollectionViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout, Pageable, HasCategory { 5 | 6 | private let cellIdentifier = String(describing: ImageCollectionViewCell.self) 7 | private let itemSize = CGSize(width: 150, height: 200) 8 | 9 | var category = Category.cats 10 | 11 | private var margin: CGFloat { 12 | let columnCount: CGFloat = 2 13 | let totalMargin = collectionView.bounds.width - itemSize.width * columnCount 14 | let marginCounbt = columnCount + 1 15 | return totalMargin / marginCounbt 16 | } 17 | 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | 21 | collectionView.register( 22 | UINib(nibName: cellIdentifier, bundle: nil), 23 | forCellWithReuseIdentifier: cellIdentifier 24 | ) 25 | 26 | if let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout { 27 | layout.scrollDirection = .vertical 28 | collectionView.collectionViewLayout = layout 29 | } 30 | 31 | collectionView.delegate = self 32 | } 33 | 34 | override func numberOfSections(in collectionView: UICollectionView) -> Int { 35 | return 1 36 | } 37 | 38 | override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 39 | return 10 40 | } 41 | 42 | override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 43 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath) as! ImageCollectionViewCell 44 | let url = URLBuilder.build(category: category, width: itemSize.width, height: itemSize.height) 45 | cell.render(with: url) 46 | return cell 47 | } 48 | 49 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 50 | return itemSize 51 | } 52 | 53 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { 54 | return UIEdgeInsets(top: margin, left: margin, bottom: margin, right: margin) 55 | } 56 | 57 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { 58 | return margin 59 | } 60 | 61 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { 62 | return margin 63 | } 64 | 65 | func pageViewController(_ pageViewController: PageViewController, didAppearAt index: PageIndex) { 66 | print("called didAppear at \(index) category is \(category)") 67 | } 68 | 69 | func pageViewController(_ pageViewController: Chausie.PageViewController, didDisappearAt index: PageIndex) { 70 | print("called didDisappear at \(index) category is \(category)") 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Examples/ChausieExample/ChausieExample/ViewControllers/HasCategory.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | protocol HasCategory: class { 4 | var category: Category { get set } 5 | static func make(category: Category) -> Self 6 | } 7 | 8 | extension HasCategory where Self: UIViewController { 9 | static func make(category: Category) -> Self { 10 | let controller = Self() 11 | controller.category = category 12 | return controller 13 | } 14 | } 15 | 16 | extension HasCategory where Self: UICollectionViewController { 17 | static func make(collectionViewLayout: UICollectionViewLayout, category: Category) -> Self { 18 | let controller = Self(collectionViewLayout: collectionViewLayout) 19 | controller.category = category 20 | return controller 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Examples/ChausieExample/ChausieExample/ViewControllers/RootViewController.swift: -------------------------------------------------------------------------------- 1 | import Chausie 2 | import UIKit 3 | 4 | final class RootViewController: UIViewController { 5 | 6 | override func viewDidLoad() { 7 | super.viewDidLoad() 8 | 9 | let controller = TabPageViewController( 10 | components: [ 11 | TabPageComponent( 12 | child: CollectionViewController.make( 13 | collectionViewLayout: UICollectionViewFlowLayout(), 14 | category: .cats 15 | ), 16 | cellModel: .cats 17 | ), 18 | TabPageComponent( 19 | child: TableViewController.make(category: .technics), 20 | cellModel: .technics 21 | ), 22 | TabPageComponent( 23 | child: CollectionViewController.make( 24 | collectionViewLayout: UICollectionViewFlowLayout(), 25 | category: .fashion 26 | ), 27 | cellModel: .fashion 28 | ) 29 | ] 30 | ) 31 | 32 | addChild(controller) 33 | view.addSubview(controller.view) 34 | controller.didMove(toParent: self) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Examples/ChausieExample/ChausieExample/ViewControllers/TableViewController.swift: -------------------------------------------------------------------------------- 1 | import Chausie 2 | import UIKit 3 | 4 | final class TableViewController: UITableViewController, Pageable, HasCategory { 5 | 6 | private let cellIdentifier = String(describing: ImageTableViewCell.self) 7 | 8 | var category = Category.cats 9 | 10 | override func viewDidLoad() { 11 | super.viewDidLoad() 12 | 13 | tableView.register( 14 | UINib(nibName: cellIdentifier, bundle: nil), 15 | forCellReuseIdentifier: cellIdentifier 16 | ) 17 | 18 | tableView.separatorStyle = .none 19 | } 20 | 21 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 22 | return 10 23 | } 24 | 25 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 26 | let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! ImageTableViewCell 27 | let url = URLBuilder.build(category: category, width: tableView.bounds.width, height: 200) 28 | cell.render(with: url) 29 | return cell 30 | } 31 | 32 | override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 33 | return 208 34 | } 35 | 36 | func pageViewController(_ pageViewController: PageViewController, didAppearAt index: PageIndex) { 37 | print("called didAppear at \(index) category is \(category)") 38 | } 39 | 40 | func pageViewController(_ pageViewController: Chausie.PageViewController, didDisappearAt index: PageIndex) { 41 | print("called didDisappear at \(index) category is \(category)") 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Examples/ChausieExample/ChausieExample/Views/ImageCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class ImageCollectionViewCell: UICollectionViewCell { 4 | 5 | private let downloader = ImageDownloader() 6 | 7 | @IBOutlet private weak var contentImageView: UIImageView! { 8 | didSet { 9 | contentImageView.layer.cornerRadius = 20 10 | } 11 | } 12 | 13 | override func prepareForReuse() { 14 | super.prepareForReuse() 15 | contentImageView.image = nil 16 | downloader.cancel() 17 | } 18 | 19 | func render(with url: URL) { 20 | downloader.download(url: url) { [weak self] data, error in 21 | guard let me = self else { return } 22 | 23 | DispatchQueue.main.async { 24 | guard let image = data.map(UIImage.init(data:)), error == nil else { 25 | me.contentImageView.image = nil 26 | return 27 | } 28 | 29 | me.contentImageView.image = image 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Examples/ChausieExample/ChausieExample/Views/ImageCollectionViewCell.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 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /Examples/ChausieExample/ChausieExample/Views/ImageTableViewCell.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class ImageTableViewCell: UITableViewCell { 4 | 5 | @IBOutlet private weak var contentImageView: UIImageView! { 6 | didSet { 7 | contentImageView.layer.cornerRadius = 30 8 | } 9 | } 10 | 11 | private let downloader = ImageDownloader() 12 | 13 | override func prepareForReuse() { 14 | super.prepareForReuse() 15 | contentImageView.image = nil 16 | downloader.cancel() 17 | } 18 | 19 | override func awakeFromNib() { 20 | super.awakeFromNib() 21 | selectionStyle = .none 22 | backgroundColor = .black 23 | } 24 | 25 | func render(with url: URL) { 26 | downloader.download(url: url) { [weak self] data, error in 27 | guard let me = self else { return } 28 | 29 | DispatchQueue.main.async { 30 | guard let image = data.map(UIImage.init(data:)), error == nil else { 31 | me.contentImageView.image = nil 32 | return 33 | } 34 | 35 | me.contentImageView.image = image 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Examples/ChausieExample/ChausieExample/Views/ImageTableViewCell.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 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /Examples/ChausieExample/Makefile: -------------------------------------------------------------------------------- 1 | PROJECT_DIR := ../../ 2 | 3 | build: 4 | carthage build --no-skip-current --project-directory $(PROJECT_DIR) 5 | mv $(PROJECT_DIR)/Carthage Carthage 6 | -------------------------------------------------------------------------------- /Examples/ChausieExample/README.md: -------------------------------------------------------------------------------- 1 | # ChausieExample 2 | 3 |

4 | 5 |

6 | 7 | 1. Clone the repo to run the example project 8 | 2. Run `$ make` from the root directory to resolve dependency. 9 | 3. Open example project workspace. 10 | 4. Run example project. 11 | 12 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem 'cocoapods', '1.10.0' 4 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.2) 5 | activesupport (5.2.4.4) 6 | concurrent-ruby (~> 1.0, >= 1.0.2) 7 | i18n (>= 0.7, < 2) 8 | minitest (~> 5.1) 9 | tzinfo (~> 1.1) 10 | addressable (2.7.0) 11 | public_suffix (>= 2.0.2, < 5.0) 12 | algoliasearch (1.27.5) 13 | httpclient (~> 2.8, >= 2.8.3) 14 | json (>= 1.5.1) 15 | atomos (0.1.3) 16 | claide (1.0.3) 17 | cocoapods (1.10.0) 18 | addressable (~> 2.6) 19 | claide (>= 1.0.2, < 2.0) 20 | cocoapods-core (= 1.10.0) 21 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 22 | cocoapods-downloader (>= 1.4.0, < 2.0) 23 | cocoapods-plugins (>= 1.0.0, < 2.0) 24 | cocoapods-search (>= 1.0.0, < 2.0) 25 | cocoapods-trunk (>= 1.4.0, < 2.0) 26 | cocoapods-try (>= 1.1.0, < 2.0) 27 | colored2 (~> 3.1) 28 | escape (~> 0.0.4) 29 | fourflusher (>= 2.3.0, < 3.0) 30 | gh_inspector (~> 1.0) 31 | molinillo (~> 0.6.6) 32 | nap (~> 1.0) 33 | ruby-macho (~> 1.4) 34 | xcodeproj (>= 1.19.0, < 2.0) 35 | cocoapods-core (1.10.0) 36 | activesupport (> 5.0, < 6) 37 | addressable (~> 2.6) 38 | algoliasearch (~> 1.0) 39 | concurrent-ruby (~> 1.1) 40 | fuzzy_match (~> 2.0.4) 41 | nap (~> 1.0) 42 | netrc (~> 0.11) 43 | public_suffix 44 | typhoeus (~> 1.0) 45 | cocoapods-deintegrate (1.0.4) 46 | cocoapods-downloader (1.4.0) 47 | cocoapods-plugins (1.0.0) 48 | nap 49 | cocoapods-search (1.0.0) 50 | cocoapods-trunk (1.5.0) 51 | nap (>= 0.8, < 2.0) 52 | netrc (~> 0.11) 53 | cocoapods-try (1.2.0) 54 | colored2 (3.1.2) 55 | concurrent-ruby (1.1.7) 56 | escape (0.0.4) 57 | ethon (0.12.0) 58 | ffi (>= 1.3.0) 59 | ffi (1.13.1) 60 | fourflusher (2.3.1) 61 | fuzzy_match (2.0.4) 62 | gh_inspector (1.1.3) 63 | httpclient (2.8.3) 64 | i18n (1.8.5) 65 | concurrent-ruby (~> 1.0) 66 | json (2.3.1) 67 | minitest (5.14.2) 68 | molinillo (0.6.6) 69 | nanaimo (0.3.0) 70 | nap (1.1.0) 71 | netrc (0.11.0) 72 | public_suffix (4.0.6) 73 | ruby-macho (1.4.0) 74 | thread_safe (0.3.6) 75 | typhoeus (1.4.0) 76 | ethon (>= 0.9.0) 77 | tzinfo (1.2.8) 78 | thread_safe (~> 0.1) 79 | xcodeproj (1.19.0) 80 | CFPropertyList (>= 2.3.3, < 4.0) 81 | atomos (~> 0.1.3) 82 | claide (>= 1.0.2, < 2.0) 83 | colored2 (~> 3.1) 84 | nanaimo (~> 0.3.0) 85 | 86 | PLATFORMS 87 | ruby 88 | 89 | DEPENDENCIES 90 | cocoapods (= 1.10.0) 91 | 92 | BUNDLED WITH 93 | 1.17.3 94 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 CyberAgent, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | xcodebuild \ 3 | -project ./Chausie.xcodeproj \ 4 | -scheme Chausie \ 5 | -configuration Debug \ 6 | -sdk iphonesimulator \ 7 | -destination 'platform=iOS Simulator,OS=12.2,name=iPhone 8' \ 8 | | xcpretty -c 9 | 10 | install-gems: 11 | bundle install --path vendor/bundle 12 | 13 | lint-lib: 14 | bundle exec pod lib lint 15 | 16 | release-pod: 17 | bundle exec pod trunk push --allow-warnings -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chausie 2 | 3 |

4 | 5 |

6 | 7 | ## Overview 8 | 9 | [![Build Status](https://travis-ci.com/cats-oss/Chausie.svg?branch=master)](https://travis-ci.com/cats-oss/Chausie) 10 | [![codecov](https://codecov.io/gh/cats-oss/Chausie/branch/master/graph/badge.svg)](https://codecov.io/gh/cats-oss/Chausie) 11 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-green.svg)](https://github.com/Carthage/Carthage) 12 | [![Carthage compatible](https://img.shields.io/badge/Cocoapods-compatible-brightgreen.svg)](https://cocoapods.org/pods/Chausie) 13 | [![License](https://img.shields.io/badge/License-MIT-lightgrey.svg 14 | )](http://mit-license.org) 15 | ![Swift](https://img.shields.io/badge/Swift-5.0-orange.svg) 16 | 17 | Chausie provides a customizable container view controller that manages navigation between pages of content. Page of contents can be controlled programmatically by your implementation or directly by the user's gesture. Chausie is designed to be flexible and extensible, provides intuitive and simple interfaces. 18 | 19 | ## Features 20 | 21 | 22 | 23 | ### implementation 24 | 25 | Chausie is designed to be a simple and minimal implementation to make the flexible user interface. Chausie provides APIs for managing page content, and implementers can customize views. See [example code](https://github.com/cats-oss/Chausie/tree/master/Examples/ChausieExample) for details. 26 | 27 | ### maintenability 28 | 29 | Chausie is used and oprated in iOS applications. Aim for continuous maintenance and enhancement by members of [CATS ( CyberAgent Advanced Technology Studio )](https://github.com/cats-oss). 30 | 31 | If you need any help, please visit our [GitHub issues](https://github.com/cats-oss/Chausie/issues) and feel free to file an issue. 32 | 33 | There are multiple ways you can contribute to this project. We welcome contributions ( GitHub issues, pull requets, etc. ) 34 | 35 | ## View Components 36 | 37 | Chausie provides container view to compose pages of content. Components that compose view container is available, so you can design flexible layout. 38 | 39 |

40 | 41 |

42 | 43 | ## Usage 44 | 45 | You can use Chausie API intuitively and simply, like this: 46 | 47 | ```swift 48 | TabPageViewController( 49 | components: [ 50 | Component( 51 | child: FirstViewController(), 52 | cellModel: Category.fashion 53 | ), 54 | Component( 55 | child: SecondViewController(), 56 | cellModel: Category.food 57 | ) 58 | ] 59 | ) 60 | ``` 61 | 62 | Clone the repo to run the example project, and run `make` from the [Example directory](https://github.com/cats-oss/Chausie/tree/master/Examples/ChausieExample) first. 63 | See sample code [here](https://github.com/cats-oss/Chausie/tree/master/Examples/ChausieExample/ChausieExample) for details. 64 | 65 | ## Requirements 66 | 67 | - Swift 5.0 68 | - Xcode 10.2.1 69 | 70 | ## Installation 71 | 72 | ### CocoaPods 73 | 74 | Chausie is available through [CocoaPods](http://cocoapods.org). To install it, simply add the following line to your Podfile: 75 | 76 | ```ruby 77 | pod "Chausie" 78 | ``` 79 | 80 | ### Carthage 81 | 82 | Add the following line to your `Cartfile`: 83 | 84 | ```ruby 85 | github "cats-oss/Chausie" 86 | ``` 87 | 88 | ## Future tasks 89 | 90 | - [x] Basic implementation 91 | - [ ] Other tab view style 92 | - [ ] Instantiates from xib or storyboard 93 | - [ ] Rearchitecture content of page 94 | - [ ] And more... 95 | 96 | ## License 97 | 98 | Chausie is available under the MIT license. See the [LICENSE](https://github.com/cats-oss/Chausie/blob/master/LICENSE) file for more info. 99 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "ChausieTests" 3 | - "Examples" 4 | --------------------------------------------------------------------------------