├── .gitignore ├── Container.png ├── ContainerViewControllers.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── ContainerViewControllers ├── AppDelegate.swift ├── AssetLoaders │ ├── AssetLoaderBuilder.swift │ ├── ImageLoader.swift │ └── ListLoader.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-1024.png │ │ ├── Icon-120.png │ │ ├── Icon-152.png │ │ ├── Icon-167.png │ │ ├── Icon-180.png │ │ ├── Icon-58.png │ │ ├── Icon-76.png │ │ └── Icon-87.png │ ├── Contents.json │ ├── dmitriy-ilkevich-boots.imageset │ │ ├── Contents.json │ │ └── dmitriy-ilkevich-boots.png │ ├── port-hercule.imageset │ │ ├── Contents.json │ │ └── port-hercule.jpg │ ├── red-landscape.imageset │ │ ├── Contents.json │ │ └── red-landscape.jpg │ ├── socrates.imageset │ │ ├── Contents.json │ │ └── socrates.png │ └── taj.imageset │ │ ├── Contents.json │ │ └── taj.jpg ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── ContainerViewControllers │ ├── CardContainerViewController.swift │ ├── ContainerViewController.swift │ ├── ScrollingContentViewController.swift │ ├── StackViewController.swift │ └── VerticalScrollingViewController.swift ├── Coordinators │ ├── AppCoordinator.swift │ ├── CardContainerCoordinator.swift │ ├── ContainerCoordinator.swift │ ├── ListDetailCoordinator.swift │ ├── StackViewCoordinator.swift │ ├── TabBarCoordinator.swift │ └── VerticalScrollingCoordinator.swift ├── DataSources │ └── ListDataSource.swift ├── Extensions │ ├── FontExtenstions.swift │ ├── UIColorExtension.swift │ ├── UIImageExtensions.swift │ ├── UITableViewCellExtension.swift │ ├── UITableViewExtension.swift │ ├── UIViewControllerExtension.swift │ └── UIViewExtensions.swift ├── Info.plist ├── Models │ ├── Album.swift │ ├── MarinaStory.swift │ ├── Profile.swift │ ├── Story.swift │ └── TajMahalStory.swift ├── Networking │ ├── Resource.swift │ └── WebService.swift ├── Protocols │ ├── Coordinator.swift │ ├── Enums.swift │ ├── ItemViewModel.swift │ ├── ListViewControllerDelegate.swift │ ├── Loader.swift │ ├── NibLoadableView.swift │ ├── ProfileViewModel.swift │ ├── ReusableView.swift │ ├── StoryViewModel.swift │ └── TabbedCoordinator.swift ├── Theme │ ├── Fonts.swift │ └── Theme.swift ├── ViewControllers │ ├── CardViewController.swift │ ├── CustomViews │ │ ├── CircularImageView.swift │ │ ├── ContentView.swift │ │ ├── HandleView.swift │ │ ├── ProfileView.swift │ │ └── StoryView.swift │ ├── ImageViewController.swift │ ├── ListDetailViewController.swift │ ├── ListViewController.swift │ ├── LoadingViewController.swift │ ├── StoryViewController.swift │ ├── TabBarController.swift │ └── UserProfileViewController.swift ├── dmitriy-ilkevich-boots.png └── dmitriy-ilkevich.png ├── InteractiveContainer.png ├── README.md ├── ScrollingContainer.png ├── ScrollingStackView.png └── images ├── article ├── CardContainers.png ├── ContainerViewController.png ├── ScrollingContent.png ├── StackView.png └── Stackview-lanscape-540x250.png ├── originals ├── CardContainer.png ├── CardExpanded.png ├── ContainerExample.png ├── Scrolling1.png ├── Scrolling2.png ├── ScrollingContent.png ├── StackView-landscape.png ├── StackView1.png ├── StackView2.png ├── TableDetailScreen.png ├── dmitriy-ilkevich-1169660-unsplash.jpg ├── dmitriy-ilkevich-1169661-unsplash.jpg └── raghu-nayyar-501556-unsplash.jpg └── scaled ├── CardContainer-250x540.png ├── CardExpanded-250x540.png ├── ContainerExample-250x540.png ├── ScrollingContent-250x540.png └── TableDetailScreen-250x540.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xcuserstate 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | *.dSYM.zip 28 | *.dSYM 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | # Swift Package Manager 35 | # 36 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 37 | # Packages/ 38 | .build/ 39 | 40 | # CocoaPods 41 | # 42 | # We recommend against adding the Pods directory to your .gitignore. However 43 | # you should judge for yourself, the pros and cons are mentioned at: 44 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 45 | # 46 | # Pods/ 47 | 48 | # Carthage 49 | # 50 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 51 | # Carthage/Checkouts 52 | 53 | Carthage/Build 54 | 55 | # fastlane 56 | # 57 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 58 | # screenshots whenever they are needed. 59 | # For more information about the recommended setup visit: 60 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 61 | 62 | fastlane/report.xml 63 | fastlane/Preview.html 64 | fastlane/screenshots 65 | fastlane/test_output 66 | -------------------------------------------------------------------------------- /Container.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkw5877/ContainerViewControllers/06cca59cd300b9223595b16763649297d58cccd3/Container.png -------------------------------------------------------------------------------- /ContainerViewControllers.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | CE02EAE9217E1EFD0014E9E6 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE02EAE8217E1EFD0014E9E6 /* AppDelegate.swift */; }; 11 | CE02EAF0217E1EFF0014E9E6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CE02EAEF217E1EFF0014E9E6 /* Assets.xcassets */; }; 12 | CE02EAF3217E1EFF0014E9E6 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CE02EAF1217E1EFF0014E9E6 /* LaunchScreen.storyboard */; }; 13 | CE02EAFB217E1F360014E9E6 /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE02EAFA217E1F360014E9E6 /* AppCoordinator.swift */; }; 14 | CE02EAFD217E1F450014E9E6 /* UIViewControllerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE02EAFC217E1F450014E9E6 /* UIViewControllerExtension.swift */; }; 15 | CE02EB01217E1F610014E9E6 /* LoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE02EB00217E1F610014E9E6 /* LoadingViewController.swift */; }; 16 | CE02EB03217E1FA70014E9E6 /* WebService.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE02EB02217E1FA70014E9E6 /* WebService.swift */; }; 17 | CE02EB05217E1FAD0014E9E6 /* Resource.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE02EB04217E1FAD0014E9E6 /* Resource.swift */; }; 18 | CE02EB0D217E21100014E9E6 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE02EB0C217E21100014E9E6 /* Theme.swift */; }; 19 | CE02EB0F217E213B0014E9E6 /* UIColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE02EB0E217E213B0014E9E6 /* UIColorExtension.swift */; }; 20 | CE02EB11217E21CF0014E9E6 /* ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE02EB10217E21CF0014E9E6 /* ReusableView.swift */; }; 21 | CE02EB14217E21DC0014E9E6 /* UITableViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE02EB12217E21DC0014E9E6 /* UITableViewExtension.swift */; }; 22 | CE02EB15217E21DC0014E9E6 /* UITableViewCellExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE02EB13217E21DC0014E9E6 /* UITableViewCellExtension.swift */; }; 23 | CE02EB18217E221C0014E9E6 /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE02EB17217E221C0014E9E6 /* Coordinator.swift */; }; 24 | CE02EB1A217E23010014E9E6 /* NibLoadableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE02EB19217E23010014E9E6 /* NibLoadableView.swift */; }; 25 | CE02EB1C217E28530014E9E6 /* TabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE02EB1B217E28530014E9E6 /* TabBarController.swift */; }; 26 | CE02EB1F217E34FE0014E9E6 /* TabBarCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE02EB1E217E34FE0014E9E6 /* TabBarCoordinator.swift */; }; 27 | CE02EB25217E47D30014E9E6 /* ImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE02EB24217E47D30014E9E6 /* ImageViewController.swift */; }; 28 | CE02EB27217E483E0014E9E6 /* ImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE02EB26217E483E0014E9E6 /* ImageLoader.swift */; }; 29 | CE02EB29217E4DD40014E9E6 /* ContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE02EB28217E4DD40014E9E6 /* ContainerViewController.swift */; }; 30 | CE0DBE1C224E79EA00A6A453 /* HandleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0DBE1B224E79EA00A6A453 /* HandleView.swift */; }; 31 | CE0DBE20224E84C700A6A453 /* Loader.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0DBE1F224E84C700A6A453 /* Loader.swift */; }; 32 | CE136ABD225D6236009984B4 /* MarinaStory.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE136ABC225D6236009984B4 /* MarinaStory.swift */; }; 33 | CE136ABF225D624D009984B4 /* TajMahalStory.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE136ABE225D624D009984B4 /* TajMahalStory.swift */; }; 34 | CE196FB32230436200117313 /* dmitriy-ilkevich.png in Resources */ = {isa = PBXBuildFile; fileRef = CE196FB22230436200117313 /* dmitriy-ilkevich.png */; }; 35 | CE1FD47E219358E10094BBD1 /* ScrollingContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1FD47D219358E10094BBD1 /* ScrollingContentViewController.swift */; }; 36 | CE1FD480219363A10094BBD1 /* UIViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1FD47F219363A10094BBD1 /* UIViewExtensions.swift */; }; 37 | CE25E846220DE77C00CCC791 /* ListViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE25E845220DE77C00CCC791 /* ListViewControllerDelegate.swift */; }; 38 | CE25E848220E2C4F00CCC791 /* ContainerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE25E847220E2C4F00CCC791 /* ContainerCoordinator.swift */; }; 39 | CE25E84A220E300F00CCC791 /* VerticalScrollingCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE25E849220E300F00CCC791 /* VerticalScrollingCoordinator.swift */; }; 40 | CE25E84C220E310200CCC791 /* StackViewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE25E84B220E310200CCC791 /* StackViewCoordinator.swift */; }; 41 | CE3FCBC0218A316F00061433 /* StackViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3FCBBF218A316F00061433 /* StackViewController.swift */; }; 42 | CE595FE9221E2ECC007FB084 /* CardContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE595FE8221E2ECC007FB084 /* CardContainerViewController.swift */; }; 43 | CE595FED221E2EED007FB084 /* CardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE595FEB221E2EED007FB084 /* CardViewController.swift */; }; 44 | CE595FEF221E2F46007FB084 /* Enums.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE595FEE221E2F45007FB084 /* Enums.swift */; }; 45 | CE595FF1221E3106007FB084 /* CardContainerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE595FF0221E3106007FB084 /* CardContainerCoordinator.swift */; }; 46 | CE5A9B6A2182977E00D3FE1D /* StoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE5A9B692182977E00D3FE1D /* StoryViewController.swift */; }; 47 | CE5A9B6C2182983B00D3FE1D /* StoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE5A9B6B2182983B00D3FE1D /* StoryView.swift */; }; 48 | CE5A9B702183547C00D3FE1D /* UserProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE5A9B6F2183547C00D3FE1D /* UserProfileViewController.swift */; }; 49 | CE5A9B722183549C00D3FE1D /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE5A9B712183549C00D3FE1D /* ProfileView.swift */; }; 50 | CE7DC2E92184F2B300DE8DA2 /* ListDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7DC2E82184F2B300DE8DA2 /* ListDetailViewController.swift */; }; 51 | CE7DC2EB2184F5BF00DE8DA2 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7DC2EA2184F5BF00DE8DA2 /* ContentView.swift */; }; 52 | CE7DC2ED2184FA0A00DE8DA2 /* ListDetailCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7DC2EC2184FA0A00DE8DA2 /* ListDetailCoordinator.swift */; }; 53 | CE7FBCC7218356B80043BC9D /* Story.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7FBCC6218356B80043BC9D /* Story.swift */; }; 54 | CE7FBCC9218358930043BC9D /* Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7FBCC8218358930043BC9D /* Profile.swift */; }; 55 | CE7FBCCB218359230043BC9D /* CircularImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7FBCCA218359230043BC9D /* CircularImageView.swift */; }; 56 | CE7FBCCD218385F00043BC9D /* StoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7FBCCC218385F00043BC9D /* StoryViewModel.swift */; }; 57 | CE7FBCCF218386460043BC9D /* ProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7FBCCE218386460043BC9D /* ProfileViewModel.swift */; }; 58 | CE85CC17222DFD3B00D059AF /* UIImageExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE85CC16222DFD3B00D059AF /* UIImageExtensions.swift */; }; 59 | CE9F0038217E7F9B00C07B9E /* ListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE9F0037217E7F9B00C07B9E /* ListViewController.swift */; }; 60 | CE9F003A217E7FD500C07B9E /* ListLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE9F0039217E7FD500C07B9E /* ListLoader.swift */; }; 61 | CE9F003C217E833F00C07B9E /* ListDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE9F003B217E833F00C07B9E /* ListDataSource.swift */; }; 62 | CE9F003E217E837F00C07B9E /* ItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE9F003D217E837F00C07B9E /* ItemViewModel.swift */; }; 63 | CE9F0042217E88C900C07B9E /* Album.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE9F0041217E88C900C07B9E /* Album.swift */; }; 64 | CEA3635D2193B9F400195B2E /* VerticalScrollingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEA3635C2193B9F400195B2E /* VerticalScrollingViewController.swift */; }; 65 | CEC027B721EFFC09006B357A /* Fonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC027B621EFFC09006B357A /* Fonts.swift */; }; 66 | CEEB1D1A225D07270019099C /* FontExtenstions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEEB1D19225D07270019099C /* FontExtenstions.swift */; }; 67 | /* End PBXBuildFile section */ 68 | 69 | /* Begin PBXFileReference section */ 70 | CE02EAE5217E1EFD0014E9E6 /* ContainerViewControllers.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ContainerViewControllers.app; sourceTree = BUILT_PRODUCTS_DIR; }; 71 | CE02EAE8217E1EFD0014E9E6 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 72 | CE02EAEF217E1EFF0014E9E6 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 73 | CE02EAF2217E1EFF0014E9E6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 74 | CE02EAF4217E1EFF0014E9E6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 75 | CE02EAFA217E1F360014E9E6 /* AppCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = ""; }; 76 | CE02EAFC217E1F450014E9E6 /* UIViewControllerExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewControllerExtension.swift; sourceTree = ""; }; 77 | CE02EB00217E1F610014E9E6 /* LoadingViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadingViewController.swift; sourceTree = ""; }; 78 | CE02EB02217E1FA70014E9E6 /* WebService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebService.swift; sourceTree = ""; }; 79 | CE02EB04217E1FAD0014E9E6 /* Resource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Resource.swift; sourceTree = ""; }; 80 | CE02EB0C217E21100014E9E6 /* Theme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = ""; }; 81 | CE02EB0E217E213B0014E9E6 /* UIColorExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIColorExtension.swift; sourceTree = ""; }; 82 | CE02EB10217E21CF0014E9E6 /* ReusableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReusableView.swift; sourceTree = ""; }; 83 | CE02EB12217E21DC0014E9E6 /* UITableViewExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UITableViewExtension.swift; sourceTree = ""; }; 84 | CE02EB13217E21DC0014E9E6 /* UITableViewCellExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UITableViewCellExtension.swift; sourceTree = ""; }; 85 | CE02EB17217E221C0014E9E6 /* Coordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Coordinator.swift; sourceTree = ""; }; 86 | CE02EB19217E23010014E9E6 /* NibLoadableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NibLoadableView.swift; sourceTree = ""; }; 87 | CE02EB1B217E28530014E9E6 /* TabBarController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabBarController.swift; sourceTree = ""; }; 88 | CE02EB1E217E34FE0014E9E6 /* TabBarCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabBarCoordinator.swift; sourceTree = ""; }; 89 | CE02EB24217E47D30014E9E6 /* ImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewController.swift; sourceTree = ""; }; 90 | CE02EB26217E483E0014E9E6 /* ImageLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageLoader.swift; sourceTree = ""; }; 91 | CE02EB28217E4DD40014E9E6 /* ContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContainerViewController.swift; sourceTree = ""; }; 92 | CE0DBE1B224E79EA00A6A453 /* HandleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandleView.swift; sourceTree = ""; }; 93 | CE0DBE1F224E84C700A6A453 /* Loader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Loader.swift; sourceTree = ""; }; 94 | CE136ABC225D6236009984B4 /* MarinaStory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarinaStory.swift; sourceTree = ""; }; 95 | CE136ABE225D624D009984B4 /* TajMahalStory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TajMahalStory.swift; sourceTree = ""; }; 96 | CE196FB22230436200117313 /* dmitriy-ilkevich.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "dmitriy-ilkevich.png"; sourceTree = ""; }; 97 | CE1FD47D219358E10094BBD1 /* ScrollingContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollingContentViewController.swift; sourceTree = ""; }; 98 | CE1FD47F219363A10094BBD1 /* UIViewExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewExtensions.swift; sourceTree = ""; }; 99 | CE25E845220DE77C00CCC791 /* ListViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListViewControllerDelegate.swift; sourceTree = ""; }; 100 | CE25E847220E2C4F00CCC791 /* ContainerCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContainerCoordinator.swift; sourceTree = ""; }; 101 | CE25E849220E300F00CCC791 /* VerticalScrollingCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalScrollingCoordinator.swift; sourceTree = ""; }; 102 | CE25E84B220E310200CCC791 /* StackViewCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StackViewCoordinator.swift; sourceTree = ""; }; 103 | CE3FCBBF218A316F00061433 /* StackViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StackViewController.swift; sourceTree = ""; }; 104 | CE595FE8221E2ECC007FB084 /* CardContainerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardContainerViewController.swift; sourceTree = ""; }; 105 | CE595FEB221E2EED007FB084 /* CardViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardViewController.swift; sourceTree = ""; }; 106 | CE595FEE221E2F45007FB084 /* Enums.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Enums.swift; sourceTree = ""; }; 107 | CE595FF0221E3106007FB084 /* CardContainerCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardContainerCoordinator.swift; sourceTree = ""; }; 108 | CE5A9B692182977E00D3FE1D /* StoryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryViewController.swift; sourceTree = ""; }; 109 | CE5A9B6B2182983B00D3FE1D /* StoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryView.swift; sourceTree = ""; }; 110 | CE5A9B6F2183547C00D3FE1D /* UserProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileViewController.swift; sourceTree = ""; }; 111 | CE5A9B712183549C00D3FE1D /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = ""; }; 112 | CE7DC2E82184F2B300DE8DA2 /* ListDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListDetailViewController.swift; sourceTree = ""; }; 113 | CE7DC2EA2184F5BF00DE8DA2 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 114 | CE7DC2EC2184FA0A00DE8DA2 /* ListDetailCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListDetailCoordinator.swift; sourceTree = ""; }; 115 | CE7FBCC6218356B80043BC9D /* Story.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Story.swift; sourceTree = ""; }; 116 | CE7FBCC8218358930043BC9D /* Profile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Profile.swift; sourceTree = ""; }; 117 | CE7FBCCA218359230043BC9D /* CircularImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircularImageView.swift; sourceTree = ""; }; 118 | CE7FBCCC218385F00043BC9D /* StoryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryViewModel.swift; sourceTree = ""; }; 119 | CE7FBCCE218386460043BC9D /* ProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewModel.swift; sourceTree = ""; }; 120 | CE85CC16222DFD3B00D059AF /* UIImageExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImageExtensions.swift; sourceTree = ""; }; 121 | CE9F0037217E7F9B00C07B9E /* ListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListViewController.swift; sourceTree = ""; }; 122 | CE9F0039217E7FD500C07B9E /* ListLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListLoader.swift; sourceTree = ""; }; 123 | CE9F003B217E833F00C07B9E /* ListDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListDataSource.swift; sourceTree = ""; }; 124 | CE9F003D217E837F00C07B9E /* ItemViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemViewModel.swift; sourceTree = ""; }; 125 | CE9F0041217E88C900C07B9E /* Album.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Album.swift; sourceTree = ""; }; 126 | CEA3635C2193B9F400195B2E /* VerticalScrollingViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerticalScrollingViewController.swift; sourceTree = ""; }; 127 | CEC027B621EFFC09006B357A /* Fonts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fonts.swift; sourceTree = ""; }; 128 | CEEB1D19225D07270019099C /* FontExtenstions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontExtenstions.swift; sourceTree = ""; }; 129 | /* End PBXFileReference section */ 130 | 131 | /* Begin PBXFrameworksBuildPhase section */ 132 | CE02EAE2217E1EFD0014E9E6 /* Frameworks */ = { 133 | isa = PBXFrameworksBuildPhase; 134 | buildActionMask = 2147483647; 135 | files = ( 136 | ); 137 | runOnlyForDeploymentPostprocessing = 0; 138 | }; 139 | /* End PBXFrameworksBuildPhase section */ 140 | 141 | /* Begin PBXGroup section */ 142 | CE02EADC217E1EFD0014E9E6 = { 143 | isa = PBXGroup; 144 | children = ( 145 | CE02EAE7217E1EFD0014E9E6 /* ContainerViewControllers */, 146 | CE02EAE6217E1EFD0014E9E6 /* Products */, 147 | ); 148 | sourceTree = ""; 149 | }; 150 | CE02EAE6217E1EFD0014E9E6 /* Products */ = { 151 | isa = PBXGroup; 152 | children = ( 153 | CE02EAE5217E1EFD0014E9E6 /* ContainerViewControllers.app */, 154 | ); 155 | name = Products; 156 | sourceTree = ""; 157 | }; 158 | CE02EAE7217E1EFD0014E9E6 /* ContainerViewControllers */ = { 159 | isa = PBXGroup; 160 | children = ( 161 | CE02EAE8217E1EFD0014E9E6 /* AppDelegate.swift */, 162 | CE9F0040217E881900C07B9E /* AssetLoaders */, 163 | CE02EB09217E20330014E9E6 /* ContainerViewControllers */, 164 | CE02EB08217E20240014E9E6 /* Coordinators */, 165 | CE9F003F217E880E00C07B9E /* DataSources */, 166 | CE02EB0B217E204B0014E9E6 /* Extensions */, 167 | CE02EB0A217E20410014E9E6 /* Networking */, 168 | CE02EB16217E21E30014E9E6 /* Protocols */, 169 | CE02EB1D217E33AF0014E9E6 /* ViewControllers */, 170 | CE9F0044217E8D2E00C07B9E /* Theme */, 171 | CE9F0043217E8BFB00C07B9E /* Models */, 172 | CE02EAEF217E1EFF0014E9E6 /* Assets.xcassets */, 173 | CE196FB22230436200117313 /* dmitriy-ilkevich.png */, 174 | CE02EAF1217E1EFF0014E9E6 /* LaunchScreen.storyboard */, 175 | CE02EAF4217E1EFF0014E9E6 /* Info.plist */, 176 | ); 177 | path = ContainerViewControllers; 178 | sourceTree = ""; 179 | }; 180 | CE02EB08217E20240014E9E6 /* Coordinators */ = { 181 | isa = PBXGroup; 182 | children = ( 183 | CE02EAFA217E1F360014E9E6 /* AppCoordinator.swift */, 184 | CE25E847220E2C4F00CCC791 /* ContainerCoordinator.swift */, 185 | CE595FF0221E3106007FB084 /* CardContainerCoordinator.swift */, 186 | CE7DC2EC2184FA0A00DE8DA2 /* ListDetailCoordinator.swift */, 187 | CE02EB1E217E34FE0014E9E6 /* TabBarCoordinator.swift */, 188 | CE25E84B220E310200CCC791 /* StackViewCoordinator.swift */, 189 | CE25E849220E300F00CCC791 /* VerticalScrollingCoordinator.swift */, 190 | ); 191 | path = Coordinators; 192 | sourceTree = ""; 193 | }; 194 | CE02EB09217E20330014E9E6 /* ContainerViewControllers */ = { 195 | isa = PBXGroup; 196 | children = ( 197 | CE02EB28217E4DD40014E9E6 /* ContainerViewController.swift */, 198 | CE3FCBBF218A316F00061433 /* StackViewController.swift */, 199 | CE1FD47D219358E10094BBD1 /* ScrollingContentViewController.swift */, 200 | CEA3635C2193B9F400195B2E /* VerticalScrollingViewController.swift */, 201 | CE595FE8221E2ECC007FB084 /* CardContainerViewController.swift */, 202 | ); 203 | path = ContainerViewControllers; 204 | sourceTree = ""; 205 | }; 206 | CE02EB0A217E20410014E9E6 /* Networking */ = { 207 | isa = PBXGroup; 208 | children = ( 209 | CE02EB04217E1FAD0014E9E6 /* Resource.swift */, 210 | CE02EB02217E1FA70014E9E6 /* WebService.swift */, 211 | ); 212 | path = Networking; 213 | sourceTree = ""; 214 | }; 215 | CE02EB0B217E204B0014E9E6 /* Extensions */ = { 216 | isa = PBXGroup; 217 | children = ( 218 | CE02EB13217E21DC0014E9E6 /* UITableViewCellExtension.swift */, 219 | CE85CC16222DFD3B00D059AF /* UIImageExtensions.swift */, 220 | CE02EB12217E21DC0014E9E6 /* UITableViewExtension.swift */, 221 | CE02EB0E217E213B0014E9E6 /* UIColorExtension.swift */, 222 | CE02EAFC217E1F450014E9E6 /* UIViewControllerExtension.swift */, 223 | CE1FD47F219363A10094BBD1 /* UIViewExtensions.swift */, 224 | CEEB1D19225D07270019099C /* FontExtenstions.swift */, 225 | ); 226 | path = Extensions; 227 | sourceTree = ""; 228 | }; 229 | CE02EB16217E21E30014E9E6 /* Protocols */ = { 230 | isa = PBXGroup; 231 | children = ( 232 | CE595FEE221E2F45007FB084 /* Enums.swift */, 233 | CE9F003D217E837F00C07B9E /* ItemViewModel.swift */, 234 | CE7FBCCC218385F00043BC9D /* StoryViewModel.swift */, 235 | CE7FBCCE218386460043BC9D /* ProfileViewModel.swift */, 236 | CE02EB19217E23010014E9E6 /* NibLoadableView.swift */, 237 | CE02EB10217E21CF0014E9E6 /* ReusableView.swift */, 238 | CE02EB17217E221C0014E9E6 /* Coordinator.swift */, 239 | CE25E845220DE77C00CCC791 /* ListViewControllerDelegate.swift */, 240 | CE0DBE1F224E84C700A6A453 /* Loader.swift */, 241 | ); 242 | path = Protocols; 243 | sourceTree = ""; 244 | }; 245 | CE02EB1D217E33AF0014E9E6 /* ViewControllers */ = { 246 | isa = PBXGroup; 247 | children = ( 248 | CE02EB00217E1F610014E9E6 /* LoadingViewController.swift */, 249 | CE02EB1B217E28530014E9E6 /* TabBarController.swift */, 250 | CE02EB24217E47D30014E9E6 /* ImageViewController.swift */, 251 | CE9F0037217E7F9B00C07B9E /* ListViewController.swift */, 252 | CE7DC2E82184F2B300DE8DA2 /* ListDetailViewController.swift */, 253 | CE5A9B692182977E00D3FE1D /* StoryViewController.swift */, 254 | CE5A9B6F2183547C00D3FE1D /* UserProfileViewController.swift */, 255 | CE595FEB221E2EED007FB084 /* CardViewController.swift */, 256 | CE5A9B73218354C200D3FE1D /* CustomViews */, 257 | ); 258 | path = ViewControllers; 259 | sourceTree = ""; 260 | }; 261 | CE5A9B73218354C200D3FE1D /* CustomViews */ = { 262 | isa = PBXGroup; 263 | children = ( 264 | CE0DBE1B224E79EA00A6A453 /* HandleView.swift */, 265 | CE7FBCCA218359230043BC9D /* CircularImageView.swift */, 266 | CE5A9B6B2182983B00D3FE1D /* StoryView.swift */, 267 | CE5A9B712183549C00D3FE1D /* ProfileView.swift */, 268 | CE7DC2EA2184F5BF00DE8DA2 /* ContentView.swift */, 269 | ); 270 | path = CustomViews; 271 | sourceTree = ""; 272 | }; 273 | CE9F003F217E880E00C07B9E /* DataSources */ = { 274 | isa = PBXGroup; 275 | children = ( 276 | CE9F003B217E833F00C07B9E /* ListDataSource.swift */, 277 | ); 278 | path = DataSources; 279 | sourceTree = ""; 280 | }; 281 | CE9F0040217E881900C07B9E /* AssetLoaders */ = { 282 | isa = PBXGroup; 283 | children = ( 284 | CE02EB26217E483E0014E9E6 /* ImageLoader.swift */, 285 | CE9F0039217E7FD500C07B9E /* ListLoader.swift */, 286 | ); 287 | path = AssetLoaders; 288 | sourceTree = ""; 289 | }; 290 | CE9F0043217E8BFB00C07B9E /* Models */ = { 291 | isa = PBXGroup; 292 | children = ( 293 | CE9F0041217E88C900C07B9E /* Album.swift */, 294 | CE7FBCC6218356B80043BC9D /* Story.swift */, 295 | CE136ABE225D624D009984B4 /* TajMahalStory.swift */, 296 | CE136ABC225D6236009984B4 /* MarinaStory.swift */, 297 | CE7FBCC8218358930043BC9D /* Profile.swift */, 298 | ); 299 | path = Models; 300 | sourceTree = ""; 301 | }; 302 | CE9F0044217E8D2E00C07B9E /* Theme */ = { 303 | isa = PBXGroup; 304 | children = ( 305 | CE02EB0C217E21100014E9E6 /* Theme.swift */, 306 | CEC027B621EFFC09006B357A /* Fonts.swift */, 307 | ); 308 | path = Theme; 309 | sourceTree = ""; 310 | }; 311 | /* End PBXGroup section */ 312 | 313 | /* Begin PBXNativeTarget section */ 314 | CE02EAE4217E1EFD0014E9E6 /* ContainerViewControllers */ = { 315 | isa = PBXNativeTarget; 316 | buildConfigurationList = CE02EAF7217E1EFF0014E9E6 /* Build configuration list for PBXNativeTarget "ContainerViewControllers" */; 317 | buildPhases = ( 318 | CE02EAE1217E1EFD0014E9E6 /* Sources */, 319 | CE02EAE2217E1EFD0014E9E6 /* Frameworks */, 320 | CE02EAE3217E1EFD0014E9E6 /* Resources */, 321 | ); 322 | buildRules = ( 323 | ); 324 | dependencies = ( 325 | ); 326 | name = ContainerViewControllers; 327 | productName = ContainerViewControllers; 328 | productReference = CE02EAE5217E1EFD0014E9E6 /* ContainerViewControllers.app */; 329 | productType = "com.apple.product-type.application"; 330 | }; 331 | /* End PBXNativeTarget section */ 332 | 333 | /* Begin PBXProject section */ 334 | CE02EADD217E1EFD0014E9E6 /* Project object */ = { 335 | isa = PBXProject; 336 | attributes = { 337 | LastSwiftUpdateCheck = 1000; 338 | LastUpgradeCheck = 1000; 339 | ORGANIZATIONNAME = user; 340 | TargetAttributes = { 341 | CE02EAE4217E1EFD0014E9E6 = { 342 | CreatedOnToolsVersion = 10.0; 343 | }; 344 | }; 345 | }; 346 | buildConfigurationList = CE02EAE0217E1EFD0014E9E6 /* Build configuration list for PBXProject "ContainerViewControllers" */; 347 | compatibilityVersion = "Xcode 9.3"; 348 | developmentRegion = en; 349 | hasScannedForEncodings = 0; 350 | knownRegions = ( 351 | en, 352 | Base, 353 | ); 354 | mainGroup = CE02EADC217E1EFD0014E9E6; 355 | productRefGroup = CE02EAE6217E1EFD0014E9E6 /* Products */; 356 | projectDirPath = ""; 357 | projectRoot = ""; 358 | targets = ( 359 | CE02EAE4217E1EFD0014E9E6 /* ContainerViewControllers */, 360 | ); 361 | }; 362 | /* End PBXProject section */ 363 | 364 | /* Begin PBXResourcesBuildPhase section */ 365 | CE02EAE3217E1EFD0014E9E6 /* Resources */ = { 366 | isa = PBXResourcesBuildPhase; 367 | buildActionMask = 2147483647; 368 | files = ( 369 | CE196FB32230436200117313 /* dmitriy-ilkevich.png in Resources */, 370 | CE02EAF3217E1EFF0014E9E6 /* LaunchScreen.storyboard in Resources */, 371 | CE02EAF0217E1EFF0014E9E6 /* Assets.xcassets in Resources */, 372 | ); 373 | runOnlyForDeploymentPostprocessing = 0; 374 | }; 375 | /* End PBXResourcesBuildPhase section */ 376 | 377 | /* Begin PBXSourcesBuildPhase section */ 378 | CE02EAE1217E1EFD0014E9E6 /* Sources */ = { 379 | isa = PBXSourcesBuildPhase; 380 | buildActionMask = 2147483647; 381 | files = ( 382 | CE02EAFD217E1F450014E9E6 /* UIViewControllerExtension.swift in Sources */, 383 | CE9F0042217E88C900C07B9E /* Album.swift in Sources */, 384 | CE25E848220E2C4F00CCC791 /* ContainerCoordinator.swift in Sources */, 385 | CE02EB25217E47D30014E9E6 /* ImageViewController.swift in Sources */, 386 | CE02EB1C217E28530014E9E6 /* TabBarController.swift in Sources */, 387 | CE9F003A217E7FD500C07B9E /* ListLoader.swift in Sources */, 388 | CE5A9B6C2182983B00D3FE1D /* StoryView.swift in Sources */, 389 | CE02EB05217E1FAD0014E9E6 /* Resource.swift in Sources */, 390 | CE02EB18217E221C0014E9E6 /* Coordinator.swift in Sources */, 391 | CE02EB29217E4DD40014E9E6 /* ContainerViewController.swift in Sources */, 392 | CEEB1D1A225D07270019099C /* FontExtenstions.swift in Sources */, 393 | CE02EAFB217E1F360014E9E6 /* AppCoordinator.swift in Sources */, 394 | CE136ABF225D624D009984B4 /* TajMahalStory.swift in Sources */, 395 | CE7DC2EB2184F5BF00DE8DA2 /* ContentView.swift in Sources */, 396 | CE136ABD225D6236009984B4 /* MarinaStory.swift in Sources */, 397 | CE25E846220DE77C00CCC791 /* ListViewControllerDelegate.swift in Sources */, 398 | CE5A9B702183547C00D3FE1D /* UserProfileViewController.swift in Sources */, 399 | CE7DC2ED2184FA0A00DE8DA2 /* ListDetailCoordinator.swift in Sources */, 400 | CE02EB1F217E34FE0014E9E6 /* TabBarCoordinator.swift in Sources */, 401 | CE02EB11217E21CF0014E9E6 /* ReusableView.swift in Sources */, 402 | CE3FCBC0218A316F00061433 /* StackViewController.swift in Sources */, 403 | CE02EB03217E1FA70014E9E6 /* WebService.swift in Sources */, 404 | CE7DC2E92184F2B300DE8DA2 /* ListDetailViewController.swift in Sources */, 405 | CE595FEF221E2F46007FB084 /* Enums.swift in Sources */, 406 | CE02EB1A217E23010014E9E6 /* NibLoadableView.swift in Sources */, 407 | CE0DBE20224E84C700A6A453 /* Loader.swift in Sources */, 408 | CEC027B721EFFC09006B357A /* Fonts.swift in Sources */, 409 | CE85CC17222DFD3B00D059AF /* UIImageExtensions.swift in Sources */, 410 | CE7FBCCB218359230043BC9D /* CircularImageView.swift in Sources */, 411 | CE25E84A220E300F00CCC791 /* VerticalScrollingCoordinator.swift in Sources */, 412 | CE5A9B722183549C00D3FE1D /* ProfileView.swift in Sources */, 413 | CE595FF1221E3106007FB084 /* CardContainerCoordinator.swift in Sources */, 414 | CE02EB14217E21DC0014E9E6 /* UITableViewExtension.swift in Sources */, 415 | CE02EB27217E483E0014E9E6 /* ImageLoader.swift in Sources */, 416 | CE02EAE9217E1EFD0014E9E6 /* AppDelegate.swift in Sources */, 417 | CE1FD47E219358E10094BBD1 /* ScrollingContentViewController.swift in Sources */, 418 | CE1FD480219363A10094BBD1 /* UIViewExtensions.swift in Sources */, 419 | CE595FE9221E2ECC007FB084 /* CardContainerViewController.swift in Sources */, 420 | CE9F0038217E7F9B00C07B9E /* ListViewController.swift in Sources */, 421 | CE7FBCCD218385F00043BC9D /* StoryViewModel.swift in Sources */, 422 | CE02EB0D217E21100014E9E6 /* Theme.swift in Sources */, 423 | CEA3635D2193B9F400195B2E /* VerticalScrollingViewController.swift in Sources */, 424 | CE5A9B6A2182977E00D3FE1D /* StoryViewController.swift in Sources */, 425 | CE7FBCC9218358930043BC9D /* Profile.swift in Sources */, 426 | CE02EB0F217E213B0014E9E6 /* UIColorExtension.swift in Sources */, 427 | CE7FBCC7218356B80043BC9D /* Story.swift in Sources */, 428 | CE02EB15217E21DC0014E9E6 /* UITableViewCellExtension.swift in Sources */, 429 | CE595FED221E2EED007FB084 /* CardViewController.swift in Sources */, 430 | CE9F003C217E833F00C07B9E /* ListDataSource.swift in Sources */, 431 | CE9F003E217E837F00C07B9E /* ItemViewModel.swift in Sources */, 432 | CE0DBE1C224E79EA00A6A453 /* HandleView.swift in Sources */, 433 | CE02EB01217E1F610014E9E6 /* LoadingViewController.swift in Sources */, 434 | CE25E84C220E310200CCC791 /* StackViewCoordinator.swift in Sources */, 435 | CE7FBCCF218386460043BC9D /* ProfileViewModel.swift in Sources */, 436 | ); 437 | runOnlyForDeploymentPostprocessing = 0; 438 | }; 439 | /* End PBXSourcesBuildPhase section */ 440 | 441 | /* Begin PBXVariantGroup section */ 442 | CE02EAF1217E1EFF0014E9E6 /* LaunchScreen.storyboard */ = { 443 | isa = PBXVariantGroup; 444 | children = ( 445 | CE02EAF2217E1EFF0014E9E6 /* Base */, 446 | ); 447 | name = LaunchScreen.storyboard; 448 | sourceTree = ""; 449 | }; 450 | /* End PBXVariantGroup section */ 451 | 452 | /* Begin XCBuildConfiguration section */ 453 | CE02EAF5217E1EFF0014E9E6 /* Debug */ = { 454 | isa = XCBuildConfiguration; 455 | buildSettings = { 456 | ALWAYS_SEARCH_USER_PATHS = NO; 457 | CLANG_ANALYZER_NONNULL = YES; 458 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 459 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 460 | CLANG_CXX_LIBRARY = "libc++"; 461 | CLANG_ENABLE_MODULES = YES; 462 | CLANG_ENABLE_OBJC_ARC = YES; 463 | CLANG_ENABLE_OBJC_WEAK = YES; 464 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 465 | CLANG_WARN_BOOL_CONVERSION = YES; 466 | CLANG_WARN_COMMA = YES; 467 | CLANG_WARN_CONSTANT_CONVERSION = YES; 468 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 469 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 470 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 471 | CLANG_WARN_EMPTY_BODY = YES; 472 | CLANG_WARN_ENUM_CONVERSION = YES; 473 | CLANG_WARN_INFINITE_RECURSION = YES; 474 | CLANG_WARN_INT_CONVERSION = YES; 475 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 476 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 477 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 478 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 479 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 480 | CLANG_WARN_STRICT_PROTOTYPES = YES; 481 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 482 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 483 | CLANG_WARN_UNREACHABLE_CODE = YES; 484 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 485 | CODE_SIGN_IDENTITY = "iPhone Developer"; 486 | COPY_PHASE_STRIP = NO; 487 | DEBUG_INFORMATION_FORMAT = dwarf; 488 | ENABLE_STRICT_OBJC_MSGSEND = YES; 489 | ENABLE_TESTABILITY = YES; 490 | GCC_C_LANGUAGE_STANDARD = gnu11; 491 | GCC_DYNAMIC_NO_PIC = NO; 492 | GCC_NO_COMMON_BLOCKS = YES; 493 | GCC_OPTIMIZATION_LEVEL = 0; 494 | GCC_PREPROCESSOR_DEFINITIONS = ( 495 | "DEBUG=1", 496 | "$(inherited)", 497 | ); 498 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 499 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 500 | GCC_WARN_UNDECLARED_SELECTOR = YES; 501 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 502 | GCC_WARN_UNUSED_FUNCTION = YES; 503 | GCC_WARN_UNUSED_VARIABLE = YES; 504 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 505 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 506 | MTL_FAST_MATH = YES; 507 | ONLY_ACTIVE_ARCH = YES; 508 | SDKROOT = iphoneos; 509 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 510 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 511 | }; 512 | name = Debug; 513 | }; 514 | CE02EAF6217E1EFF0014E9E6 /* Release */ = { 515 | isa = XCBuildConfiguration; 516 | buildSettings = { 517 | ALWAYS_SEARCH_USER_PATHS = NO; 518 | CLANG_ANALYZER_NONNULL = YES; 519 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 520 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 521 | CLANG_CXX_LIBRARY = "libc++"; 522 | CLANG_ENABLE_MODULES = YES; 523 | CLANG_ENABLE_OBJC_ARC = YES; 524 | CLANG_ENABLE_OBJC_WEAK = YES; 525 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 526 | CLANG_WARN_BOOL_CONVERSION = YES; 527 | CLANG_WARN_COMMA = YES; 528 | CLANG_WARN_CONSTANT_CONVERSION = YES; 529 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 530 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 531 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 532 | CLANG_WARN_EMPTY_BODY = YES; 533 | CLANG_WARN_ENUM_CONVERSION = YES; 534 | CLANG_WARN_INFINITE_RECURSION = YES; 535 | CLANG_WARN_INT_CONVERSION = YES; 536 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 537 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 538 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 539 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 540 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 541 | CLANG_WARN_STRICT_PROTOTYPES = YES; 542 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 543 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 544 | CLANG_WARN_UNREACHABLE_CODE = YES; 545 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 546 | CODE_SIGN_IDENTITY = "iPhone Developer"; 547 | COPY_PHASE_STRIP = NO; 548 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 549 | ENABLE_NS_ASSERTIONS = NO; 550 | ENABLE_STRICT_OBJC_MSGSEND = YES; 551 | GCC_C_LANGUAGE_STANDARD = gnu11; 552 | GCC_NO_COMMON_BLOCKS = YES; 553 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 554 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 555 | GCC_WARN_UNDECLARED_SELECTOR = YES; 556 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 557 | GCC_WARN_UNUSED_FUNCTION = YES; 558 | GCC_WARN_UNUSED_VARIABLE = YES; 559 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 560 | MTL_ENABLE_DEBUG_INFO = NO; 561 | MTL_FAST_MATH = YES; 562 | SDKROOT = iphoneos; 563 | SWIFT_COMPILATION_MODE = wholemodule; 564 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 565 | VALIDATE_PRODUCT = YES; 566 | }; 567 | name = Release; 568 | }; 569 | CE02EAF8217E1EFF0014E9E6 /* Debug */ = { 570 | isa = XCBuildConfiguration; 571 | buildSettings = { 572 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 573 | CODE_SIGN_STYLE = Automatic; 574 | DEVELOPMENT_TEAM = S264RF7436; 575 | INFOPLIST_FILE = ContainerViewControllers/Info.plist; 576 | IPHONEOS_DEPLOYMENT_TARGET = 11.4; 577 | LD_RUNPATH_SEARCH_PATHS = ( 578 | "$(inherited)", 579 | "@executable_path/Frameworks", 580 | ); 581 | PRODUCT_BUNDLE_IDENTIFIER = com.example.ContainerViewControllers; 582 | PRODUCT_NAME = "$(TARGET_NAME)"; 583 | SWIFT_VERSION = 5.0; 584 | TARGETED_DEVICE_FAMILY = "1,2"; 585 | }; 586 | name = Debug; 587 | }; 588 | CE02EAF9217E1EFF0014E9E6 /* Release */ = { 589 | isa = XCBuildConfiguration; 590 | buildSettings = { 591 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 592 | CODE_SIGN_STYLE = Automatic; 593 | DEVELOPMENT_TEAM = S264RF7436; 594 | INFOPLIST_FILE = ContainerViewControllers/Info.plist; 595 | IPHONEOS_DEPLOYMENT_TARGET = 11.4; 596 | LD_RUNPATH_SEARCH_PATHS = ( 597 | "$(inherited)", 598 | "@executable_path/Frameworks", 599 | ); 600 | PRODUCT_BUNDLE_IDENTIFIER = com.example.ContainerViewControllers; 601 | PRODUCT_NAME = "$(TARGET_NAME)"; 602 | SWIFT_VERSION = 5.0; 603 | TARGETED_DEVICE_FAMILY = "1,2"; 604 | }; 605 | name = Release; 606 | }; 607 | /* End XCBuildConfiguration section */ 608 | 609 | /* Begin XCConfigurationList section */ 610 | CE02EAE0217E1EFD0014E9E6 /* Build configuration list for PBXProject "ContainerViewControllers" */ = { 611 | isa = XCConfigurationList; 612 | buildConfigurations = ( 613 | CE02EAF5217E1EFF0014E9E6 /* Debug */, 614 | CE02EAF6217E1EFF0014E9E6 /* Release */, 615 | ); 616 | defaultConfigurationIsVisible = 0; 617 | defaultConfigurationName = Release; 618 | }; 619 | CE02EAF7217E1EFF0014E9E6 /* Build configuration list for PBXNativeTarget "ContainerViewControllers" */ = { 620 | isa = XCConfigurationList; 621 | buildConfigurations = ( 622 | CE02EAF8217E1EFF0014E9E6 /* Debug */, 623 | CE02EAF9217E1EFF0014E9E6 /* Release */, 624 | ); 625 | defaultConfigurationIsVisible = 0; 626 | defaultConfigurationName = Release; 627 | }; 628 | /* End XCConfigurationList section */ 629 | }; 630 | rootObject = CE02EADD217E1EFD0014E9E6 /* Project object */; 631 | } 632 | -------------------------------------------------------------------------------- /ContainerViewControllers.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ContainerViewControllers.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ContainerViewControllers/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // ContainerViewControllers 4 | // 5 | // Created by user on 10/22/18. 6 | // Copyright © 2018 user. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | var appCoordinator:Coordinator? 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | window = UIWindow(frame: UIScreen.main.bounds) 19 | window?.rootViewController = TabBarController() 20 | appCoordinator = AppCoordinator(tabBarController: window?.rootViewController as! TabBarController) 21 | appCoordinator?.start() 22 | Theme.apply() 23 | window?.makeKeyAndVisible() 24 | return true 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /ContainerViewControllers/AssetLoaders/AssetLoaderBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | //import Foundation 3 | //import UIKit.UIImage 4 | // 5 | //enum ResourceType { 6 | // case image 7 | // case list 8 | //} 9 | // 10 | //class AssetLoaderBuilder { 11 | // 12 | // private var resourceType:ResourceType = .image 13 | // private var urlString:String = "" 14 | // 15 | // func withResource(resource:Resource) -> AssetLoaderBuilder { 16 | // self.resourceType = type 17 | // return self 18 | // } 19 | // 20 | // func withURLString(urlString:String) -> AssetLoaderBuilder { 21 | // self.urlString = urlString 22 | // return self 23 | // } 24 | // 25 | // func build() -> ImageLoader { 26 | // 27 | // let url = URL(string: urlString)! 28 | // 29 | // switch resourceType { 30 | // case .image: 31 | // let resource = Resource(url: url, parse: { (data) -> UIImage? in 32 | // let image = UIImage(data: data) 33 | // return image 34 | // }) 35 | // return ImageLoader(resource: resource) 36 | // case .list: 37 | // let resource = Resource<[Album]> 38 | // return ListLoader(resource: resource) 39 | // } 40 | // } 41 | //} 42 | -------------------------------------------------------------------------------- /ContainerViewControllers/AssetLoaders/ImageLoader.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit.UIImage 3 | 4 | final class ImageLoader:Loader { 5 | 6 | private let webService = WebService() 7 | private let resource:Resource 8 | 9 | init(resource:Resource) { 10 | self.resource = resource 11 | } 12 | 13 | func loadData(completion: @escaping ((UIImage?) -> ())) { 14 | webService.load(resource: resource) { (image) in 15 | completion(image) 16 | } 17 | } 18 | 19 | public func loadImage(completion:@escaping ((UIImage?) -> ()) ) { 20 | webService.load(resource: resource) { (image) in 21 | completion(image) 22 | } 23 | } 24 | 25 | class func imageLoader(urlString:String) -> ImageLoader? { 26 | guard let url = URL(string: urlString) else { return nil } 27 | let resource = Resource(url: url, parse: { (data) -> UIImage? in 28 | let image = UIImage(data: data) 29 | return image 30 | }) 31 | 32 | return ImageLoader(resource: resource) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ContainerViewControllers/AssetLoaders/ListLoader.swift: -------------------------------------------------------------------------------- 1 | 2 | final class ListLoader:Loader { 3 | 4 | private let webService = WebService() 5 | private let resource:Resource<[Album]> 6 | 7 | init(resource:Resource<[Album]>) { 8 | self.resource = resource 9 | } 10 | 11 | func loadData(completion: @escaping (([ItemViewModel]?) -> ())) { 12 | webService.load(resource: resource) { (items) in 13 | completion(items) 14 | } 15 | } 16 | 17 | public func loadItems(completion:@escaping (([ItemViewModel]?) -> ()) ) { 18 | webService.load(resource: resource) { (items) in 19 | completion(items) 20 | } 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /ContainerViewControllers/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 | "size" : "29x29", 15 | "idiom" : "iphone", 16 | "filename" : "Icon-58.png", 17 | "scale" : "2x" 18 | }, 19 | { 20 | "size" : "29x29", 21 | "idiom" : "iphone", 22 | "filename" : "Icon-87.png", 23 | "scale" : "3x" 24 | }, 25 | { 26 | "idiom" : "iphone", 27 | "size" : "40x40", 28 | "scale" : "2x" 29 | }, 30 | { 31 | "idiom" : "iphone", 32 | "size" : "40x40", 33 | "scale" : "3x" 34 | }, 35 | { 36 | "size" : "60x60", 37 | "idiom" : "iphone", 38 | "filename" : "Icon-120.png", 39 | "scale" : "2x" 40 | }, 41 | { 42 | "size" : "60x60", 43 | "idiom" : "iphone", 44 | "filename" : "Icon-180.png", 45 | "scale" : "3x" 46 | }, 47 | { 48 | "idiom" : "ipad", 49 | "size" : "20x20", 50 | "scale" : "1x" 51 | }, 52 | { 53 | "idiom" : "ipad", 54 | "size" : "20x20", 55 | "scale" : "2x" 56 | }, 57 | { 58 | "idiom" : "ipad", 59 | "size" : "29x29", 60 | "scale" : "1x" 61 | }, 62 | { 63 | "idiom" : "ipad", 64 | "size" : "29x29", 65 | "scale" : "2x" 66 | }, 67 | { 68 | "idiom" : "ipad", 69 | "size" : "40x40", 70 | "scale" : "1x" 71 | }, 72 | { 73 | "idiom" : "ipad", 74 | "size" : "40x40", 75 | "scale" : "2x" 76 | }, 77 | { 78 | "size" : "76x76", 79 | "idiom" : "ipad", 80 | "filename" : "Icon-76.png", 81 | "scale" : "1x" 82 | }, 83 | { 84 | "size" : "76x76", 85 | "idiom" : "ipad", 86 | "filename" : "Icon-152.png", 87 | "scale" : "2x" 88 | }, 89 | { 90 | "size" : "83.5x83.5", 91 | "idiom" : "ipad", 92 | "filename" : "Icon-167.png", 93 | "scale" : "2x" 94 | }, 95 | { 96 | "size" : "1024x1024", 97 | "idiom" : "ios-marketing", 98 | "filename" : "Icon-1024.png", 99 | "scale" : "1x" 100 | } 101 | ], 102 | "info" : { 103 | "version" : 1, 104 | "author" : "xcode" 105 | } 106 | } -------------------------------------------------------------------------------- /ContainerViewControllers/Assets.xcassets/AppIcon.appiconset/Icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkw5877/ContainerViewControllers/06cca59cd300b9223595b16763649297d58cccd3/ContainerViewControllers/Assets.xcassets/AppIcon.appiconset/Icon-1024.png -------------------------------------------------------------------------------- /ContainerViewControllers/Assets.xcassets/AppIcon.appiconset/Icon-120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkw5877/ContainerViewControllers/06cca59cd300b9223595b16763649297d58cccd3/ContainerViewControllers/Assets.xcassets/AppIcon.appiconset/Icon-120.png -------------------------------------------------------------------------------- /ContainerViewControllers/Assets.xcassets/AppIcon.appiconset/Icon-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkw5877/ContainerViewControllers/06cca59cd300b9223595b16763649297d58cccd3/ContainerViewControllers/Assets.xcassets/AppIcon.appiconset/Icon-152.png -------------------------------------------------------------------------------- /ContainerViewControllers/Assets.xcassets/AppIcon.appiconset/Icon-167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkw5877/ContainerViewControllers/06cca59cd300b9223595b16763649297d58cccd3/ContainerViewControllers/Assets.xcassets/AppIcon.appiconset/Icon-167.png -------------------------------------------------------------------------------- /ContainerViewControllers/Assets.xcassets/AppIcon.appiconset/Icon-180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkw5877/ContainerViewControllers/06cca59cd300b9223595b16763649297d58cccd3/ContainerViewControllers/Assets.xcassets/AppIcon.appiconset/Icon-180.png -------------------------------------------------------------------------------- /ContainerViewControllers/Assets.xcassets/AppIcon.appiconset/Icon-58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkw5877/ContainerViewControllers/06cca59cd300b9223595b16763649297d58cccd3/ContainerViewControllers/Assets.xcassets/AppIcon.appiconset/Icon-58.png -------------------------------------------------------------------------------- /ContainerViewControllers/Assets.xcassets/AppIcon.appiconset/Icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkw5877/ContainerViewControllers/06cca59cd300b9223595b16763649297d58cccd3/ContainerViewControllers/Assets.xcassets/AppIcon.appiconset/Icon-76.png -------------------------------------------------------------------------------- /ContainerViewControllers/Assets.xcassets/AppIcon.appiconset/Icon-87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkw5877/ContainerViewControllers/06cca59cd300b9223595b16763649297d58cccd3/ContainerViewControllers/Assets.xcassets/AppIcon.appiconset/Icon-87.png -------------------------------------------------------------------------------- /ContainerViewControllers/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /ContainerViewControllers/Assets.xcassets/dmitriy-ilkevich-boots.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "dmitriy-ilkevich-boots.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /ContainerViewControllers/Assets.xcassets/dmitriy-ilkevich-boots.imageset/dmitriy-ilkevich-boots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkw5877/ContainerViewControllers/06cca59cd300b9223595b16763649297d58cccd3/ContainerViewControllers/Assets.xcassets/dmitriy-ilkevich-boots.imageset/dmitriy-ilkevich-boots.png -------------------------------------------------------------------------------- /ContainerViewControllers/Assets.xcassets/port-hercule.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "port-hercule.jpg", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /ContainerViewControllers/Assets.xcassets/port-hercule.imageset/port-hercule.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkw5877/ContainerViewControllers/06cca59cd300b9223595b16763649297d58cccd3/ContainerViewControllers/Assets.xcassets/port-hercule.imageset/port-hercule.jpg -------------------------------------------------------------------------------- /ContainerViewControllers/Assets.xcassets/red-landscape.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "red-landscape.jpg", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /ContainerViewControllers/Assets.xcassets/red-landscape.imageset/red-landscape.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkw5877/ContainerViewControllers/06cca59cd300b9223595b16763649297d58cccd3/ContainerViewControllers/Assets.xcassets/red-landscape.imageset/red-landscape.jpg -------------------------------------------------------------------------------- /ContainerViewControllers/Assets.xcassets/socrates.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "socrates.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /ContainerViewControllers/Assets.xcassets/socrates.imageset/socrates.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkw5877/ContainerViewControllers/06cca59cd300b9223595b16763649297d58cccd3/ContainerViewControllers/Assets.xcassets/socrates.imageset/socrates.png -------------------------------------------------------------------------------- /ContainerViewControllers/Assets.xcassets/taj.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "taj.jpg", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /ContainerViewControllers/Assets.xcassets/taj.imageset/taj.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkw5877/ContainerViewControllers/06cca59cd300b9223595b16763649297d58cccd3/ContainerViewControllers/Assets.xcassets/taj.imageset/taj.jpg -------------------------------------------------------------------------------- /ContainerViewControllers/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 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /ContainerViewControllers/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /ContainerViewControllers/ContainerViewControllers/CardContainerViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /* protocol to ensure there is a delegate and interactive area */ 4 | protocol CardViewProtocol:AnyObject { 5 | var handle:UIView { get } 6 | var delegate:CardViewControllerGesturesDelegate? { get set } 7 | } 8 | 9 | /* gesture recognizer protocols */ 10 | protocol CardViewControllerGesturesDelegate:AnyObject { 11 | func handleTapGesture(tapGesture: UITapGestureRecognizer) 12 | func handlePanGesture(panGesture: UIPanGestureRecognizer) 13 | } 14 | 15 | final class CardContainerViewController: UIViewController { 16 | 17 | typealias CardViewController = UIViewController & CardViewProtocol 18 | private let mainViewController:UIViewController 19 | private let cardViewController:CardViewController 20 | 21 | /* public API */ 22 | var cardHeight:CGFloat = 600 23 | var duration:TimeInterval = 0.9 24 | 25 | private var cardHandleHeight:CGFloat = 30.0 26 | private var cardMaximumHeight:CGFloat { 27 | return min(cardHeight, view.frame.height - view.layoutMargins.top) 28 | } 29 | 30 | private var visualEffectView:UIVisualEffectView! 31 | private var cardVisible = false 32 | private var nextState:CardState { 33 | return cardVisible ? .collapsed : .expanded 34 | } 35 | 36 | //holds animations 37 | private var runningAnimations = [UIViewPropertyAnimator]() 38 | private var animationProgressWhenInterrupted:CGFloat = 0 39 | 40 | required init(mainViewController:UIViewController, cardViewController:CardViewController) { 41 | self.mainViewController = mainViewController 42 | self.cardViewController = cardViewController 43 | super.init(nibName: nil, bundle: nil) 44 | } 45 | 46 | required init?(coder aDecoder: NSCoder) { 47 | fatalError("init(coder:) has not been implemented") 48 | } 49 | 50 | override func viewDidLoad() { 51 | super.viewDidLoad() 52 | title = "Card Container" 53 | setupMainViewController() 54 | setupVisualEffectView() 55 | setupCard() 56 | } 57 | 58 | private func setupMainViewController() { 59 | addChild(mainViewController) 60 | view.addSubview(mainViewController.view) 61 | mainViewController.view.frame = view.frame 62 | mainViewController.didMove(toParent: self) 63 | } 64 | 65 | private func setupVisualEffectView() { 66 | visualEffectView = UIVisualEffectView() 67 | visualEffectView.frame = view.bounds 68 | view.addSubview(visualEffectView) 69 | } 70 | 71 | /* setup card view controller as child view controller */ 72 | private func setupCard() { 73 | 74 | cardViewController.delegate = self 75 | add(child: cardViewController, in: view) 76 | 77 | /* get the card handle & set frame */ 78 | setCardFrame() 79 | cardViewController.view.clipsToBounds = true 80 | cardViewController.didMove(toParent: self) 81 | } 82 | 83 | /* 84 | * we need to setup the card's frame initially, and then update it after the margins 85 | * are set by the system. We do this in viewDidLayoutSubviews 86 | **/ 87 | private func setCardFrame() { 88 | cardHandleHeight = cardViewController.handle.frame.height 89 | let yOrigin = cardYOriginWhenCollapsed() 90 | cardViewController.view.frame = CGRect(x: 0, y: yOrigin, width: view.frame.width, height: cardMaximumHeight) 91 | visualEffectView.frame = view.bounds 92 | } 93 | 94 | private func cardYOriginWhenCollapsed() -> CGFloat { 95 | return view.frame.height - view.layoutMargins.bottom - cardHandleHeight 96 | } 97 | 98 | override func viewDidLayoutSubviews() { 99 | super.viewDidLayoutSubviews() 100 | setCardFrame() 101 | } 102 | 103 | /* 104 | * we dismiss the card view when the device rotates 105 | */ 106 | override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { 107 | animateCardFrame(duration, .collapsed) 108 | animateBlurView(duration, .collapsed) 109 | } 110 | } 111 | 112 | extension CardContainerViewController : CardViewControllerGesturesDelegate { 113 | 114 | func handleTapGesture(tapGesture gesture:UITapGestureRecognizer){ 115 | switch gesture.state { 116 | case .ended: 117 | animateTransitionIfNeeded(state: nextState, duration: duration) 118 | default:break 119 | } 120 | } 121 | 122 | func handlePanGesture(panGesture gesture:UIPanGestureRecognizer) { 123 | switch gesture.state { 124 | case .began: 125 | startInteractiveTransition(state: nextState, duration:duration) 126 | case .changed: 127 | let translation = gesture.translation(in: cardViewController.handle) 128 | var fractionComplete = translation.y / cardMaximumHeight 129 | fractionComplete = cardVisible ? fractionComplete : -fractionComplete 130 | updateInteractiveTransition(fractionCompleted: fractionComplete) 131 | case .ended: 132 | continueInteractiveTransition() 133 | default:break 134 | } 135 | } 136 | 137 | func animateTransitionIfNeeded(state:CardState, duration:TimeInterval){ 138 | if runningAnimations.isEmpty { 139 | animateCardFrame(duration, state) 140 | animateCornerRadius(duration, state) 141 | animateBlurView(duration, state) 142 | } 143 | } 144 | 145 | fileprivate func animateCardFrame(_ duration: TimeInterval, _ state: CardState) { 146 | let frameAnimator = UIViewPropertyAnimator(duration: duration, dampingRatio: 1.0) { 147 | switch state { 148 | case .expanded: 149 | self.cardViewController.view.frame.origin.y = self.view.frame.height - self.cardMaximumHeight 150 | case .collapsed: 151 | self.cardViewController.view.frame.origin.y = self.cardYOriginWhenCollapsed() 152 | } 153 | } 154 | 155 | frameAnimator.addCompletion{ _ in 156 | self.cardVisible.toggle() 157 | self.runningAnimations.removeAll() 158 | } 159 | 160 | frameAnimator.startAnimation() 161 | runningAnimations.append(frameAnimator) 162 | } 163 | 164 | fileprivate func animateCornerRadius(_ duration: TimeInterval, _ state: CardState) { 165 | let cornerRadiusAnimator = UIViewPropertyAnimator(duration: duration, curve: .linear) { 166 | switch state { 167 | case .expanded: 168 | self.cardViewController.view.layer.cornerRadius = 12 169 | case .collapsed: 170 | self.cardViewController.view.layer.cornerRadius = 0 171 | } 172 | } 173 | 174 | cornerRadiusAnimator.startAnimation() 175 | runningAnimations.append(cornerRadiusAnimator) 176 | } 177 | 178 | fileprivate func animateBlurView(_ duration:TimeInterval, _ state:CardState) { 179 | let blurAnimator = UIViewPropertyAnimator(duration: duration, dampingRatio: 1.0) { 180 | switch state { 181 | case .expanded: 182 | self.visualEffectView.effect = UIBlurEffect(style: .dark) 183 | case .collapsed: 184 | self.visualEffectView.effect = nil 185 | } 186 | } 187 | 188 | blurAnimator.startAnimation() 189 | runningAnimations.append(blurAnimator) 190 | 191 | } 192 | 193 | func startInteractiveTransition(state:CardState, duration:TimeInterval){ 194 | if runningAnimations.isEmpty { 195 | animateTransitionIfNeeded(state: state, duration: duration) 196 | } 197 | 198 | for animator in runningAnimations { 199 | animator.pauseAnimation() 200 | animationProgressWhenInterrupted = animator.fractionComplete 201 | } 202 | } 203 | 204 | func updateInteractiveTransition(fractionCompleted:CGFloat){ 205 | for animator in runningAnimations { 206 | animator.fractionComplete = fractionCompleted + animationProgressWhenInterrupted 207 | } 208 | } 209 | 210 | func continueInteractiveTransition() { 211 | for animator in runningAnimations { 212 | animator.continueAnimation(withTimingParameters: nil, durationFactor: 0) 213 | } 214 | } 215 | 216 | } 217 | 218 | -------------------------------------------------------------------------------- /ContainerViewControllers/ContainerViewControllers/ContainerViewController.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | 4 | final class ContainerViewController: UIViewController { 5 | 6 | private let imageController:UIViewController 7 | private let listController:UIViewController 8 | 9 | init(imageController:UIViewController, listController:UIViewController) { 10 | self.imageController = imageController 11 | self.listController = listController 12 | super.init(nibName: nil, bundle: nil) 13 | } 14 | 15 | required init?(coder aDecoder: NSCoder) { 16 | fatalError("init(coder:) has not been implemented") 17 | } 18 | 19 | override func viewDidLoad() { 20 | super.viewDidLoad() 21 | title = "Container" 22 | view.backgroundColor = Theme.backgroundColor 23 | add(child: imageController, in: view) 24 | add(child: listController, in: view) 25 | layoutViewControllers() 26 | } 27 | 28 | override func viewWillLayoutSubviews() { 29 | super.viewWillLayoutSubviews() 30 | layoutViewControllers() 31 | } 32 | 33 | private func layoutViewControllers() { 34 | let frame = view.frame 35 | imageController.view.frame = .init(x: 0, y: 0, width: frame.width, height: frame.height * 0.40) 36 | let yOrigin = ceil(imageController.view.frame.height/2) 37 | let height = ceil(frame.maxY - imageController.view.frame.maxY) 38 | listController.view.frame = .init(x: 0, y: yOrigin, width: frame.width, height: height) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /ContainerViewControllers/ContainerViewControllers/ScrollingContentViewController.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | 4 | /** 5 | * ScrollingContentViewController 6 | * See Dave DeLongs example at https://github.com/davedelong/MVCTodo 7 | */ 8 | final class ScrollingContentViewController: UIViewController { 9 | 10 | private let scrollView = UIScrollView() 11 | private let scrollViewContentContainer = UIView() 12 | private let content: UIViewController 13 | 14 | init(content: UIViewController) { 15 | self.content = content 16 | super.init(nibName: nil, bundle: nil) 17 | } 18 | 19 | required init?(coder aDecoder: NSCoder) { 20 | fatalError("init(coder:) has not been implemented") 21 | } 22 | 23 | override func loadView() { 24 | view = UIView() 25 | scrollView.preservesSuperviewLayoutMargins = true 26 | scrollView.backgroundColor = Theme.mainColor 27 | scrollView.translatesAutoresizingMaskIntoConstraints = false 28 | view.addSubview(scrollView) 29 | 30 | NSLayoutConstraint.activate([ 31 | view.safeAreaLayoutGuide.topAnchor.constraint(equalTo: scrollView.topAnchor), 32 | view.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), 33 | view.safeAreaLayoutGuide.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), 34 | view.safeAreaLayoutGuide.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor) 35 | ]) 36 | 37 | /* add child controller and setup constraints */ 38 | add(child: content, in: scrollView) 39 | content.view.translatesAutoresizingMaskIntoConstraints = false 40 | 41 | NSLayoutConstraint.activate([ 42 | content.view.topAnchor.constraint(equalTo: scrollView.topAnchor), 43 | content.view.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), 44 | content.view.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), 45 | content.view.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), 46 | content.view.widthAnchor.constraint(equalTo: scrollView.widthAnchor), 47 | ]) 48 | } 49 | 50 | override func viewDidLoad() { 51 | super.viewDidLoad() 52 | title = content.title 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /ContainerViewControllers/ContainerViewControllers/StackViewController.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | 4 | final class StackViewController: UIViewController { 5 | 6 | private let stackView: UIStackView = { 7 | let stackView = UIStackView() 8 | stackView.axis = .vertical 9 | stackView.alignment = .fill 10 | stackView.distribution = .fill 11 | stackView.spacing = 5 12 | return stackView 13 | }() 14 | 15 | private var contents:[UIViewController] 16 | private var portraitConstraints = [NSLayoutConstraint]() 17 | 18 | init(content:[UIViewController]) { 19 | self.contents = content 20 | super.init(nibName: nil, bundle: nil) 21 | } 22 | 23 | required init?(coder aDecoder: NSCoder) { 24 | fatalError("init(coder:) has not been implemented") 25 | } 26 | 27 | override func loadView() { 28 | view = stackView 29 | add(contents: contents) 30 | setConstraints(contents:contents) 31 | } 32 | 33 | private func add(contents:[UIViewController]){ 34 | for content in contents { 35 | stackView.addArrangedSubview(content.view) 36 | } 37 | } 38 | 39 | private func setConstraints(contents:[UIViewController]) { 40 | for content in contents { 41 | addConstraints(content) 42 | } 43 | } 44 | 45 | private func addConstraints(_ content:UIViewController) { 46 | 47 | let contentSize = content.view.frame.size 48 | var height = contentSize.height 49 | var width = contentSize.width 50 | 51 | if content.preferredContentSize != .zero { 52 | let size = content.preferredContentSize 53 | height = size.height 54 | width = size.width 55 | } 56 | 57 | portraitConstraints.append(contentsOf: [ 58 | content.view.heightAnchor.constraint(equalToConstant: height), 59 | content.view.widthAnchor.constraint(equalToConstant: width) 60 | ]) 61 | 62 | _ = portraitConstraints.map{ $0.priority = .defaultHigh } 63 | updateStackViewAxis() 64 | } 65 | 66 | override func viewDidLoad() { 67 | super.viewDidLoad() 68 | title = "Stack View" 69 | } 70 | 71 | private func updateStackViewAxis() { 72 | switch UIDevice.current.orientation { 73 | case .landscapeLeft, .landscapeRight: 74 | stackView.axis = .horizontal 75 | default: 76 | stackView.axis = .vertical 77 | if portraitConstraints.isEmpty { 78 | setConstraints(contents: contents) 79 | } 80 | NSLayoutConstraint.activate(portraitConstraints) 81 | } 82 | } 83 | 84 | /* change the stackviews axis when the device rotates */ 85 | override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { 86 | updateStackViewAxis() 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /ContainerViewControllers/ContainerViewControllers/VerticalScrollingViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class VerticalScrollingViewController: UIViewController { 4 | 5 | private lazy var scrollView:UIScrollView = { 6 | let scrollView = UIScrollView(frame:UIScreen.main.bounds) 7 | scrollView.translatesAutoresizingMaskIntoConstraints = false 8 | scrollView.preservesSuperviewLayoutMargins = true 9 | scrollView.backgroundColor = Theme.backgroundColor 10 | return scrollView 11 | }() 12 | 13 | private var scrollViewContent = [UIViewController]() 14 | 15 | override var shouldAutorotate: Bool { 16 | return false 17 | } 18 | 19 | override var shouldAutomaticallyForwardAppearanceMethods: Bool { 20 | return false 21 | } 22 | 23 | override var supportedInterfaceOrientations: UIInterfaceOrientationMask { 24 | return .portrait 25 | } 26 | 27 | init(contents:[UIViewController]) { 28 | super.init(nibName: nil, bundle: nil) 29 | self.scrollViewContent.append(contentsOf: contents) 30 | } 31 | 32 | required init?(coder aDecoder: NSCoder) { 33 | super.init(coder: aDecoder) 34 | } 35 | 36 | override func loadView() { 37 | view = scrollView 38 | add(contents: scrollViewContent) 39 | } 40 | 41 | override func viewDidLoad() { 42 | super.viewDidLoad() 43 | title = "Scrollable" 44 | } 45 | 46 | func add(contents:[UIViewController]){ 47 | for child in contents { 48 | add(child: child, in:scrollView) 49 | portrait(content: child) 50 | } 51 | } 52 | 53 | private func portrait(content:UIViewController) { 54 | let bounds = view.bounds 55 | var contentSize = content.view.frame.size 56 | 57 | if content.preferredContentSize != .zero { 58 | contentSize = content.preferredContentSize 59 | } 60 | 61 | let y:CGFloat = scrollView.contentSize.height 62 | let size = CGSize(width:bounds.width, height:contentSize.height) 63 | let height = scrollView.contentSize.height + contentSize.height 64 | let origin = CGPoint(x: 0, y: y) 65 | content.view.frame = CGRect(origin: origin, size: size) 66 | 67 | scrollView.contentSize = CGSize(width: bounds.width, height: height) 68 | } 69 | 70 | private func landscape(content:UIViewController){ 71 | let bounds = UIScreen.main.bounds 72 | var contentSize = content.view.frame.size 73 | 74 | if content.preferredContentSize != .zero { 75 | contentSize = content.preferredContentSize 76 | } 77 | 78 | let x:CGFloat = scrollView.contentSize.width 79 | let size = CGSize(width:scrollView.contentSize.width, height:contentSize.height) 80 | let scrollWidth = scrollView.contentSize.width 81 | 82 | let origin = CGPoint(x: x, y: 0) 83 | content.view.frame = CGRect(origin: origin, size: size) 84 | scrollView.contentSize = CGSize(width: scrollWidth, height: bounds.height) 85 | } 86 | 87 | /* Notifies an interested controller that the preferred content size of one of its children changed. */ 88 | override func preferredContentSizeDidChange(forChildContentContainer container: UIContentContainer) { 89 | print(#function) 90 | } 91 | 92 | /* Notifies the container that a child view controller was resized using auto layout. */ 93 | override func systemLayoutFittingSizeDidChange(forChildContentContainer container: UIContentContainer) { 94 | print(#function) 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /ContainerViewControllers/Coordinators/AppCoordinator.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class AppCoordinator: NSObject, Coordinator { 4 | 5 | var childCoordinators = [Coordinator]() 6 | private let tabBarController:UITabBarController 7 | 8 | init(tabBarController:UITabBarController) { 9 | self.tabBarController = tabBarController 10 | } 11 | 12 | deinit { 13 | print("deallocing \(self)") 14 | } 15 | 16 | func start() { 17 | showTabBarView() 18 | } 19 | 20 | private func showTabBarView() { 21 | let tabBarCoordinatorCoordinator = TabBarCoordinator(tabBarController: tabBarController) 22 | tabBarCoordinatorCoordinator.parent = self 23 | childCoordinators.append(tabBarCoordinatorCoordinator) 24 | tabBarCoordinatorCoordinator.start() 25 | } 26 | } 27 | 28 | extension AppCoordinator : UINavigationControllerDelegate { 29 | 30 | func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) { 31 | guard let fromViewController = navigationController.transitionCoordinator?.viewController(forKey: .from), 32 | navigationController.viewControllers.contains(fromViewController) else { return } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ContainerViewControllers/Coordinators/CardContainerCoordinator.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class CardContainerCoordinator: Coordinator { 4 | 5 | var childCoordinators: [Coordinator] 6 | weak var parent:Coordinator? 7 | private let navigationController:UINavigationController 8 | 9 | init(navigationController:UINavigationController) { 10 | childCoordinators = [Coordinator]() 11 | self.navigationController = navigationController 12 | } 13 | 14 | deinit { 15 | print("deallocing \(self)") 16 | } 17 | 18 | func start() { 19 | initialzieCardViewContent() 20 | } 21 | 22 | private func initialzieCardViewContent(){ 23 | let imageViewController = getImageViewController() 24 | let cardViewController = CardViewController() 25 | let cardContainter = CardContainerViewController(mainViewController: imageViewController, cardViewController: cardViewController) 26 | navigationController.pushViewController(cardContainter, animated: false) 27 | } 28 | 29 | private func getImageViewController() -> UIViewController { 30 | let url = Bundle.main.url(forResource: "dmitriy-ilkevich", withExtension: "png")! 31 | let resource = Resource(url: url, parse: { (data) -> UIImage? in 32 | let image = UIImage(data: data) 33 | return image 34 | }) 35 | 36 | let imageLoader = ImageLoader(resource: resource) 37 | let imageViewController = ImageViewController(imageLoader: imageLoader) 38 | return imageViewController 39 | } 40 | } 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /ContainerViewControllers/Coordinators/ContainerCoordinator.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | 4 | final class ContainerCoordinator: NSObject, Coordinator { 5 | 6 | var childCoordinators: [Coordinator] 7 | weak var parent:Coordinator? 8 | private let navigationController:UINavigationController 9 | 10 | init(navigationController:UINavigationController) { 11 | childCoordinators = [Coordinator]() 12 | self.navigationController = navigationController 13 | } 14 | 15 | deinit { 16 | print("deallocing \(self)") 17 | } 18 | 19 | func start() { 20 | initializeContainerContent() 21 | } 22 | 23 | func initializeContainerContent() { 24 | let imageViewController = getImageViewController() 25 | let listViewController = getListViewController() 26 | let profileViewController = ContainerViewController(imageController: imageViewController, listController: listViewController) 27 | navigationController.pushViewController(profileViewController, animated: false) 28 | } 29 | 30 | private func getImageViewController() -> UIViewController { 31 | let urlString = "https://www.stockvault.net/photo/download/180092" 32 | if let imageLoader = ImageLoader.imageLoader(urlString: urlString) { 33 | let imageViewController = ImageViewController(imageLoader: imageLoader) 34 | return imageViewController 35 | } 36 | 37 | return ImageViewController() 38 | } 39 | 40 | private func getListViewController() -> UIViewController { 41 | let resource = Album.all 42 | let listLoader = ListLoader(resource: resource) 43 | let listViewController = ListViewController(listLoader: listLoader) 44 | listViewController.delegate = self 45 | return listViewController 46 | } 47 | } 48 | 49 | /* coordinator handles the delegate request to view content detail */ 50 | extension ContainerCoordinator : ListViewControllerDelegate { 51 | func didSelectItem(item: ItemViewModel) { 52 | navigationController.delegate = self 53 | let coordinator = ListDetailCoordinator(navigationController: navigationController, item:item) 54 | coordinator.parent = self 55 | childCoordinators.append(coordinator) 56 | coordinator.start() 57 | } 58 | } 59 | 60 | /* we conform to the UINavigationControllerDelegate to dealloc the coordinator when the back button is pressed 61 | */ 62 | extension ContainerCoordinator: UINavigationControllerDelegate { 63 | func navigationController(_ navigationController: UINavigationController, 64 | didShow viewController: UIViewController, animated: Bool) { 65 | 66 | // ensure the view controller is popping, here we are pushing another view controller 67 | guard let fromViewController = navigationController.transitionCoordinator?.viewController(forKey: .from), 68 | !navigationController.viewControllers.contains(fromViewController) else { 69 | return 70 | } 71 | 72 | if fromViewController is ListDetailViewController { 73 | print(fromViewController) 74 | childDidFinish(child: ListDetailCoordinator.self) 75 | } 76 | } 77 | 78 | /* remove coordinator from list */ 79 | func childDidFinish(child:T.Type) { 80 | for (index, coordinator) in childCoordinators.enumerated() { 81 | if coordinator.self is T { 82 | childCoordinators.remove(at: index) 83 | break 84 | } 85 | } 86 | } 87 | } 88 | 89 | -------------------------------------------------------------------------------- /ContainerViewControllers/Coordinators/ListDetailCoordinator.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | 4 | final class ListDetailCoordinator: Coordinator { 5 | 6 | var childCoordinators = [Coordinator]() 7 | var navigationController: UINavigationController 8 | weak var parent:Coordinator? 9 | private var item:ItemViewModel 10 | 11 | init(navigationController:UINavigationController, item:ItemViewModel){ 12 | self.navigationController = navigationController 13 | self.item = item 14 | } 15 | 16 | deinit { 17 | print("deallocing \(self)") 18 | } 19 | 20 | func start() { 21 | showDetailView() 22 | } 23 | 24 | private func showDetailView(){ 25 | let imageLoader = ImageLoader.imageLoader(urlString: item.url) 26 | let listDetailViewController = ListDetailViewController(imageLoader:imageLoader, item: item) 27 | navigationController.pushViewController(listDetailViewController, animated: true) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /ContainerViewControllers/Coordinators/StackViewCoordinator.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class StackViewCoordinator: Coordinator { 4 | 5 | var childCoordinators: [Coordinator] 6 | weak var parent:Coordinator? 7 | private let navigationController:UINavigationController 8 | 9 | init(navigationController:UINavigationController) { 10 | childCoordinators = [Coordinator]() 11 | self.navigationController = navigationController 12 | } 13 | 14 | deinit { 15 | print("deallocing \(self)") 16 | } 17 | 18 | func start() { 19 | initialzieStackViewContent() 20 | } 21 | 22 | private func initialzieStackViewContent(){ 23 | let storyViewController1 = StoryViewController(viewModel: MarinaStory()) 24 | storyViewController1.labelColor = Theme.marinaBlue 25 | let storyViewController2 = StoryViewController(viewModel: TajMahalStory()) 26 | storyViewController2.labelColor = Theme.fieldGreen 27 | let stack = StackViewController(content: [storyViewController1, storyViewController2]) 28 | let stackContainer = ScrollingContentViewController(content: stack) 29 | navigationController.pushViewController(stackContainer, animated: false) 30 | } 31 | } 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /ContainerViewControllers/Coordinators/TabBarCoordinator.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | 4 | final class TabBarCoordinator: Coordinator { 5 | 6 | var tabBarController:UITabBarController 7 | var childCoordinators = [Coordinator]() 8 | weak var parent:Coordinator? 9 | 10 | private let containerNavigationController:UINavigationController 11 | private let scrollContentNavigationController:UINavigationController 12 | private let stackViewContentNavigationController:UINavigationController 13 | private let cardContainerNavigationController:UINavigationController 14 | 15 | init(tabBarController:UITabBarController) { 16 | self.tabBarController = tabBarController 17 | self.containerNavigationController = UINavigationController() 18 | self.scrollContentNavigationController = UINavigationController() 19 | self.stackViewContentNavigationController = UINavigationController() 20 | self.cardContainerNavigationController = UINavigationController() 21 | } 22 | 23 | deinit { 24 | print("deallocing \(self)") 25 | } 26 | 27 | func start() { 28 | let containerCoordinator = ContainerCoordinator(navigationController: containerNavigationController) 29 | containerCoordinator.parent = self 30 | childCoordinators.append(containerCoordinator) 31 | containerCoordinator.start() 32 | 33 | let verticalScrollingCoordinator = VerticalScrollingCoordinator(navigationController: scrollContentNavigationController) 34 | verticalScrollingCoordinator.parent = self 35 | childCoordinators.append(verticalScrollingCoordinator) 36 | verticalScrollingCoordinator.start() 37 | 38 | let stackViewCoordinator = StackViewCoordinator(navigationController: stackViewContentNavigationController) 39 | stackViewCoordinator.parent = self 40 | childCoordinators.append(stackViewCoordinator) 41 | stackViewCoordinator.start() 42 | 43 | let cardContainerCoordinator = CardContainerCoordinator(navigationController: cardContainerNavigationController) 44 | cardContainerCoordinator.parent = self 45 | childCoordinators.append(cardContainerCoordinator) 46 | cardContainerCoordinator.start() 47 | 48 | tabBarController.setViewControllers([containerNavigationController, scrollContentNavigationController, stackViewContentNavigationController, cardContainerNavigationController], animated: true) 49 | (tabBarController as? TabBarController)?.setTitles(titles: ["Container", "Scrollable", "StackView", "Card Containter"]) 50 | tabBarController.selectedIndex = 0; 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /ContainerViewControllers/Coordinators/VerticalScrollingCoordinator.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | 4 | final class VerticalScrollingCoordinator: Coordinator { 5 | 6 | var childCoordinators: [Coordinator] 7 | weak var parent:Coordinator? 8 | private let navigationController:UINavigationController 9 | 10 | init(navigationController:UINavigationController) { 11 | childCoordinators = [Coordinator]() 12 | self.navigationController = navigationController 13 | } 14 | 15 | func start() { 16 | initialzieVerticalScrollContent() 17 | } 18 | 19 | deinit { 20 | print("deallocing \(self)") 21 | } 22 | 23 | private func initialzieVerticalScrollContent(){ 24 | let profileViewController = UserProfileViewController(viewModel: Profile()) 25 | let storyViewController = StoryViewController(viewModel: Story()) 26 | storyViewController.labelColor = Theme.redSky 27 | let scrollingViewController = VerticalScrollingViewController(contents: [profileViewController, storyViewController]) 28 | navigationController.pushViewController(scrollingViewController, animated: false) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ContainerViewControllers/DataSources/ListDataSource.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /* enhanced data source protocol to include fetching an item */ 4 | protocol TableViewDataSource:UITableViewDataSource { 5 | func item(at indexPath:IndexPath) -> ItemViewModel? 6 | } 7 | 8 | class ListDataSource: NSObject, TableViewDataSource { 9 | 10 | let viewModels:[ItemViewModel] 11 | 12 | init(viewModels:[ItemViewModel]) { 13 | self.viewModels = viewModels 14 | } 15 | 16 | func numberOfSections(in tableView: UITableView) -> Int { 17 | return 1 18 | } 19 | 20 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 21 | return viewModels.count 22 | } 23 | 24 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 25 | 26 | let cell = tableView.dequeueReusableCell(withIdentifier: "cellId", for: indexPath) 27 | let item = viewModels[indexPath.row] 28 | cell.configure(viewModel: item) 29 | cell.accessoryType = .disclosureIndicator 30 | return cell 31 | } 32 | 33 | func item(at indexPath: IndexPath) -> ItemViewModel? { 34 | if indexPath.row >= viewModels.count { return nil } 35 | return viewModels[indexPath.row] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /ContainerViewControllers/Extensions/FontExtenstions.swift: -------------------------------------------------------------------------------- 1 | 2 | 3 | import UIKit.UIFont 4 | 5 | extension UIFont { 6 | 7 | func sizeOfString(string: String, constrainedToWidth width: CGFloat) -> CGSize { 8 | let size = string.boundingRect(with: CGSize(width: width, height: .greatestFiniteMagnitude),options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [NSAttributedString.Key.font: self], context: nil).size 9 | return CGSize(width: ceil(size.width), height: ceil(size.height)) 10 | } 11 | 12 | func sizeOfStringCTFrame(string: String, constrainedToWidth width: CGFloat) -> CGSize { 13 | let attributes = [NSAttributedString.Key.font:self,] 14 | let attString = NSAttributedString(string: string,attributes: attributes) 15 | let framesetter = CTFramesetterCreateWithAttributedString(attString) 16 | let size = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRange(location: 0,length: 0), nil, CGSize(width: width, height: .greatestFiniteMagnitude), nil) 17 | return CGSize(width: ceil(size.width), height: ceil(size.height)) 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /ContainerViewControllers/Extensions/UIColorExtension.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIColor { 4 | 5 | convenience init(red: Int, green: Int, blue: Int) { 6 | self.init(red: CGFloat(red) / 255.0, green: CGFloat(green) / 255.0, blue: CGFloat(blue) / 255.0, alpha: 1.0) 7 | } 8 | 9 | /* prefix color with 0x, e.g. #5db300 would be 0x5db300 */ 10 | convenience init(netHex:Int) { 11 | self.init(red:(netHex >> 16) & 0xff, green:(netHex >> 8) & 0xff, blue:netHex & 0xff) 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /ContainerViewControllers/Extensions/UIImageExtensions.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit.UIImage 3 | 4 | extension UIImage { 5 | 6 | convenience init(bundleName: String) { 7 | self.init(named: bundleName)! 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /ContainerViewControllers/Extensions/UITableViewCellExtension.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UITableViewCell : ReusableView { } 4 | 5 | extension UITableViewCell { 6 | func configure(viewModel:ItemViewModel){ 7 | selectionStyle = .none 8 | textLabel?.text = viewModel.title 9 | detailTextLabel?.text = viewModel.albumId 10 | loadImage(url: viewModel.thumbnailUrl) 11 | } 12 | 13 | func loadImage(url:String) { 14 | let url = URL(string: url)! 15 | let resource = Resource(url: url, parse: { (data) -> UIImage? in 16 | let image = UIImage(data: data) 17 | return image 18 | }) 19 | 20 | let imageLoader = ImageLoader.init(resource: resource) 21 | imageLoader.loadImage { (image) in 22 | DispatchQueue.main.async { 23 | self.imageView?.image = image 24 | self.setNeedsLayout() 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ContainerViewControllers/Extensions/UITableViewExtension.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UITableView { 4 | 5 | /* create extension to load class and assign reuse identifier */ 6 | func register(_:T.Type) { 7 | register(T.self, forCellReuseIdentifier: T.reuseIdentifier) 8 | } 9 | 10 | /* create extension to load nib and assign reuse identifier */ 11 | // func register(_:T.Type) where T:NibLoadableView { 12 | // let nib = UINib(nibName: T.nibName, bundle: nil) 13 | // register(nib, forCellReuseIdentifier: T.reuseIdentifier) 14 | // } 15 | 16 | /* create extension to dequque and load reusable cell */ 17 | func dequeueReusableCell(forIndexPath indexPath:IndexPath) -> T { 18 | guard let cell = dequeueReusableCell(withIdentifier: T.reuseIdentifier, for: indexPath) as? T else { 19 | fatalError("Could not deque cell with identifier:\(T.reuseIdentifier)") 20 | } 21 | return cell 22 | } 23 | 24 | } 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /ContainerViewControllers/Extensions/UIViewControllerExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewControllerExtension.swift 3 | // SwiftCommonComponents 4 | // 5 | // 6 | import UIKit 7 | 8 | extension UIViewController { 9 | 10 | /* add child controller */ 11 | func add(child childViewController: UIViewController) { 12 | beginAddChild(child: childViewController) 13 | view.addSubview(childViewController.view) 14 | endAddChild(child: childViewController) 15 | } 16 | 17 | /* add child controller in a specific view */ 18 | func add(child childViewController: UIViewController, in view: UIView) { 19 | beginAddChild(child: childViewController) 20 | view.addSubview(childViewController.view) 21 | endAddChild(child: childViewController) 22 | } 23 | 24 | /* add child controller in a specific view with a set frame */ 25 | func add(child childViewController: UIViewController, in view: UIView, with frame:CGRect) { 26 | beginAddChild(child: childViewController) 27 | childViewController.view.frame = frame 28 | view.addSubview(childViewController.view) 29 | endAddChild(child: childViewController) 30 | } 31 | 32 | /* remove child controller */ 33 | func remove(child childViewController:UIViewController){ 34 | guard parent != nil else { return } 35 | childViewController.beginAppearanceTransition(false, animated: false) 36 | childViewController.willMove(toParent: nil) 37 | childViewController.view.removeFromSuperview() 38 | childViewController.removeFromParent() 39 | childViewController.endAppearanceTransition() 40 | } 41 | 42 | /* extract these common methods out to avoid code duplication */ 43 | private func beginAddChild(child childViewController:UIViewController){ 44 | childViewController.beginAppearanceTransition(true, animated: false) 45 | self.addChild(childViewController) 46 | } 47 | 48 | private func endAddChild(child childViewController:UIViewController){ 49 | childViewController.didMove(toParent: self) 50 | childViewController.endAppearanceTransition() 51 | } 52 | 53 | } 54 | 55 | extension UIViewController { 56 | /* standard fade transition between view controllers */ 57 | func transition(to child:UIViewController, completion:((Bool)-> Void)? = nil){ 58 | let duration = 0.25 59 | let current = children.last 60 | addChild(child) 61 | 62 | let newView = child.view! 63 | newView.translatesAutoresizingMaskIntoConstraints = true 64 | newView.autoresizingMask = [.flexibleWidth,.flexibleHeight] 65 | newView.frame = view.bounds 66 | 67 | if let existing = current { 68 | existing.willMove(toParent: nil) 69 | transition(from: existing, to: child, duration: duration, options: [.transitionCrossDissolve], animations:{}) { (done) in 70 | existing.removeFromParent() 71 | child.didMove(toParent: self) 72 | completion?(done) 73 | } 74 | } else { 75 | view.addSubview(newView) 76 | UIView.animate(withDuration: duration, delay: 0, options: [.transitionCrossDissolve], animations: {}) { (done) in 77 | child.didMove(toParent: self) 78 | completion?(done) 79 | } 80 | } 81 | } 82 | } 83 | 84 | 85 | /* let the top view controller decide if it should rotate or not */ 86 | extension UINavigationController { 87 | 88 | open override var shouldAutorotate: Bool { 89 | if let shouldAutorotate = visibleViewController?.shouldAutorotate { 90 | return shouldAutorotate 91 | } else { 92 | return true 93 | } 94 | } 95 | } 96 | 97 | /* let tabbar selected view controller decide if it should rotate or not */ 98 | extension UITabBarController { 99 | 100 | open override var shouldAutorotate:Bool { 101 | if let selected = selectedViewController { 102 | return selected.shouldAutorotate 103 | } 104 | return true 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /ContainerViewControllers/Extensions/UIViewExtensions.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIView { 4 | 5 | /** 6 | * Extensions for ScrollingContentViewController 7 | * See Dave DeLongs example at https://github.com/davedelong/MVCTodo 8 | */ 9 | func embedSubview(_ subview: UIView) { 10 | // do nothing if this view is already in the right place 11 | if subview.superview == self { return } 12 | 13 | if subview.superview != nil { 14 | subview.removeFromSuperview() 15 | } 16 | 17 | subview.translatesAutoresizingMaskIntoConstraints = false 18 | 19 | subview.frame = bounds 20 | addSubview(subview) 21 | 22 | NSLayoutConstraint.activate([ 23 | subview.leadingAnchor.constraint(equalTo: leadingAnchor), 24 | trailingAnchor.constraint(equalTo: subview.trailingAnchor), 25 | 26 | subview.topAnchor.constraint(equalTo: topAnchor), 27 | bottomAnchor.constraint(equalTo: subview.bottomAnchor) 28 | ]) 29 | } 30 | 31 | /** 32 | * Extensions for ScrollingContentViewController 33 | * See Dave DeLongs example at https://github.com/davedelong/MVCTodo 34 | */ 35 | func isContainedWithin(_ other: UIView) -> Bool { 36 | var current: UIView? = self 37 | while let proposedView = current { 38 | if proposedView == other { return true } 39 | current = proposedView.superview 40 | } 41 | return false 42 | } 43 | 44 | func calculateTextHeight(width:CGFloat, text:String, attributes:[NSAttributedString.Key:UIFont]) -> CGFloat { 45 | let size = CGSize(width: width, height: CGFloat.greatestFiniteMagnitude) 46 | let labelRect = text.boundingRect(with: size, options:[.usesLineFragmentOrigin, .usesFontLeading], attributes:attributes, context: nil) 47 | let height = ceil(labelRect.height) 48 | return height 49 | } 50 | 51 | } 52 | 53 | -------------------------------------------------------------------------------- /ContainerViewControllers/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 | NSAppTransportSecurity 24 | 25 | NSAllowsArbitraryLoads 26 | 27 | 28 | UILaunchStoryboardName 29 | LaunchScreen 30 | UIRequiredDeviceCapabilities 31 | 32 | armv7 33 | 34 | UIStatusBarStyle 35 | UIStatusBarStyleLightContent 36 | UISupportedInterfaceOrientations 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | UIInterfaceOrientationPortraitUpsideDown 42 | 43 | UISupportedInterfaceOrientations~ipad 44 | 45 | UIInterfaceOrientationPortrait 46 | UIInterfaceOrientationPortraitUpsideDown 47 | UIInterfaceOrientationLandscapeLeft 48 | UIInterfaceOrientationLandscapeRight 49 | 50 | UIViewControllerBasedStatusBarAppearance 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /ContainerViewControllers/Models/Album.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | 4 | typealias JSONDictionary = [String:AnyObject] 5 | 6 | class Album: ItemViewModel { 7 | var albumId: String = "" 8 | var title: String = "" 9 | var id: Int = 0 10 | var url:String = "" 11 | var thumbnailUrl: String = "" 12 | } 13 | 14 | extension Album { 15 | 16 | //failable initializer 17 | convenience init?(json:JSONDictionary) { 18 | self.init() 19 | //check for any required properties in json as needed 20 | guard let id = json["id"] as? Int else { return nil } 21 | self.id = id 22 | 23 | //check for and assign additional properties 24 | self.albumId = json["albumId"] as? String ?? "" 25 | self.title = json["title"] as? String ?? "" 26 | self.url = json["url"] as? String ?? "" 27 | self.thumbnailUrl = json["thumbnailUrl"] as? String ?? "" 28 | } 29 | } 30 | 31 | extension Album { 32 | static let all = Resource<[Album]>(url: URL(string: "https://jsonplaceholder.typicode.com/albums/1/photos")!) { (jsonData) -> [Album]? in 33 | //check we received json dictionaries 34 | guard let albums = jsonData as? [JSONDictionary] else { return nil } 35 | return albums.compactMap(Album.init) 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /ContainerViewControllers/Models/MarinaStory.swift: -------------------------------------------------------------------------------- 1 | 2 | struct MarinaStory:StoryViewModel { 3 | let imageName = "port-hercule" 4 | let text = "Port Hercule at Night" 5 | let content = """ 6 | Port Hercule is amongst the most recognizable marinas in the world. In high demand during the summer season, the port also hosts a number of large yachts during winter. Amongst its most frequent guests are the Lady Moura, Atlantis II and Wedge Too. The marina can also accommodate yachts over 120 meters but these must compete for one berthing space with cruise ships during the summer. 7 | 8 | In 2010 the Finnish manufacturer of marinas and pontoons Marinetek was hired to deliver three new pontoons to Port Hercule. Monaco's old fixed piers were replaced by Marinetek's floating concrete pontoons. The renovation was completed in 2011. 9 | """ 10 | } 11 | -------------------------------------------------------------------------------- /ContainerViewControllers/Models/Profile.swift: -------------------------------------------------------------------------------- 1 | 2 | struct Profile:ProfileViewModel { 3 | let imageName:String = "socrates" 4 | let name:String = "Socrates" 5 | let quote:String = "One thing I know, that I know nothing. This is the source of my wisdom." 6 | } 7 | -------------------------------------------------------------------------------- /ContainerViewControllers/Models/Story.swift: -------------------------------------------------------------------------------- 1 | 2 | struct Story:StoryViewModel { 3 | let imageName = "red-landscape" 4 | let text = "Hades Landscape" 5 | let content = "Hidden deep within the bowels of the earth and ruled by the god Hades and his wife Persephone, the Underworld was the kingdom of the dead in Greek mythology, the sunless place where the souls of those who died went after death. Watered by the streams of five rivers (Styx, Acheron, Cocytus, Phlegethon, and Lethe), the Underworld was divided into at least four regions: Tartarus (reserved for the worst transgressors), the Elysian Fields (where only the most excellent of men dwelled), the Fields of Mourning (for those who were hurt by love), and the Asphodel Meadows (for the souls of the majority of ordinary people)." 6 | 7 | } 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /ContainerViewControllers/Models/TajMahalStory.swift: -------------------------------------------------------------------------------- 1 | 2 | struct TajMahalStory:StoryViewModel { 3 | let imageName = "taj" 4 | let text = "Taj Mahal" 5 | let content = "The Taj Mahal is an enormous mausoleum complex commissioned in 1632 by the Mughal emperor Shah Jahan to house the remains of his beloved wife. Constructed over a 20-year period on the southern bank of the Yamuna River in Agra, India, the famed complex is one of the most outstanding examples of Mughal architecture, which combined Indian, Persian and Islamic influences. At its center is the Taj Mahal itself, built of shimmering white marble that seems to change color depending on the daylight. Designated a UNESCO World Heritage site in 1983, it remains one of the world’s most celebrated structures and a stunning symbol of India’s rich history." 6 | } 7 | -------------------------------------------------------------------------------- /ContainerViewControllers/Networking/Resource.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | //generic resouce which takes a url and a parsing function 4 | struct Resource { 5 | let url:URL 6 | let parse:((Data) -> A?) //parsing clousure 7 | } 8 | 9 | 10 | /* we create an extension on resource to parse the json based on the provided closure */ 11 | extension Resource { 12 | 13 | init(url:URL, parseJSON:@escaping (Any) -> A? ) { 14 | self.url = url 15 | self.parse = { data in 16 | 17 | //parse json, if fails will return nil 18 | let json = try? JSONSerialization.jsonObject(with:data, options:[]) 19 | return json.flatMap(parseJSON) 20 | } 21 | } 22 | } 23 | 24 | 25 | -------------------------------------------------------------------------------- /ContainerViewControllers/Networking/WebService.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class WebService { 4 | 5 | private var session:URLSessionDataTask? 6 | 7 | //generic load method takes a Resource and a completion clousure 8 | func load(resource:Resource, completion: @escaping (A?) -> ()) { 9 | 10 | session = URLSession.shared.dataTask(with: resource.url) { (data, response, error) in 11 | // this option makes the code a litte more clear in terms of what is happening 12 | // guard let data = data else { completion(nil); return } 13 | // completion(resource.parse(data)) //call completion with parsed resource 14 | 15 | //use flatMap to produce either a nil result or a parsed result 16 | let result = data.flatMap(resource.parse) 17 | completion(result) 18 | } 19 | 20 | self.session?.resume() 21 | 22 | } 23 | 24 | func cancel() { 25 | session?.cancel() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /ContainerViewControllers/Protocols/Coordinator.swift: -------------------------------------------------------------------------------- 1 | 2 | protocol Coordinator:class { 3 | var childCoordinators: [Coordinator] { get set } 4 | func start() 5 | } 6 | -------------------------------------------------------------------------------- /ContainerViewControllers/Protocols/Enums.swift: -------------------------------------------------------------------------------- 1 | 2 | enum TransitionMode { 3 | case presenting 4 | case dismissing 5 | case pop //for navigation controller 6 | } 7 | 8 | enum CardState { 9 | case expanded 10 | case collapsed 11 | } 12 | -------------------------------------------------------------------------------- /ContainerViewControllers/Protocols/ItemViewModel.swift: -------------------------------------------------------------------------------- 1 | 2 | protocol ItemViewModel { 3 | var albumId:String { get } 4 | var title:String { get } 5 | var id:Int { get } 6 | var url:String { get } 7 | var thumbnailUrl:String { get } 8 | } 9 | -------------------------------------------------------------------------------- /ContainerViewControllers/Protocols/ListViewControllerDelegate.swift: -------------------------------------------------------------------------------- 1 | 2 | protocol ListViewControllerDelegate:class { 3 | func didSelectItem(item:ItemViewModel) 4 | } 5 | -------------------------------------------------------------------------------- /ContainerViewControllers/Protocols/Loader.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | 4 | protocol Loader { 5 | 6 | associatedtype Kind 7 | associatedtype Model 8 | 9 | init(resource:Kind) 10 | func loadData(completion:@escaping ((Model?) -> ()) ) 11 | } 12 | -------------------------------------------------------------------------------- /ContainerViewControllers/Protocols/NibLoadableView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | protocol NibLoadableView: class {} 4 | 5 | /* use the name of the nib class as the nib name */ 6 | extension NibLoadableView where Self: UIView { 7 | 8 | static var nibName:String { 9 | return String(describing: self) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ContainerViewControllers/Protocols/ProfileViewModel.swift: -------------------------------------------------------------------------------- 1 | 2 | protocol ProfileViewModel { 3 | var imageName:String { get } 4 | var name:String { get } 5 | var quote:String { get } 6 | } 7 | 8 | 9 | -------------------------------------------------------------------------------- /ContainerViewControllers/Protocols/ReusableView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | protocol ReusableView: class {} 4 | 5 | /* use the name of the cell class as the reuse identifier */ 6 | extension ReusableView where Self: UIView { 7 | static var reuseIdentifier:String { return String(describing: self) } 8 | } 9 | -------------------------------------------------------------------------------- /ContainerViewControllers/Protocols/StoryViewModel.swift: -------------------------------------------------------------------------------- 1 | 2 | protocol StoryViewModel { 3 | var imageName:String { get } 4 | var text:String { get } 5 | var content:String { get } 6 | } 7 | -------------------------------------------------------------------------------- /ContainerViewControllers/Protocols/TabbedCoordinator.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | protocol TabbedCoordinator { 4 | var childCoordinators: [Coordinator] { get set } 5 | var tabBarController: UITabBarController { get set } 6 | func start() 7 | } 8 | -------------------------------------------------------------------------------- /ContainerViewControllers/Theme/Fonts.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | 4 | class Fonts { 5 | 6 | static var caption:UIFont { 7 | return UIFont(name: "Avenir", size: 24.0)! 8 | } 9 | 10 | static var body:UIFont { 11 | return UIFont(name: "Avenir", size: 20.0)! 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ContainerViewControllers/Theme/Theme.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class Theme { 4 | 5 | static var mainColor: UIColor { 6 | return UIColor(netHex: 0x18afbb) 7 | } 8 | 9 | static var mutedMainColor:UIColor { 10 | return UIColor(netHex: 0x138c95) 11 | } 12 | 13 | static var backgroundColor:UIColor { 14 | return UIColor(netHex: 0xF0F0F0) 15 | } 16 | 17 | static var lightBackgroundColor:UIColor { 18 | return UIColor(netHex: 0xffffff) 19 | } 20 | 21 | static var textColor:UIColor { 22 | return UIColor(netHex: 0xffffff).withAlphaComponent(0.80) 23 | } 24 | 25 | /* dark grey */ 26 | static var darkTextColor:UIColor { 27 | return UIColor(netHex: 0xf38434f).withAlphaComponent(0.80) 28 | } 29 | 30 | static var whiteColor:UIColor { 31 | return .white 32 | } 33 | 34 | /* orange color from landscape image */ 35 | static var redSky:UIColor { 36 | return UIColor(netHex: 0xDF5E26) 37 | } 38 | 39 | static var marinaBlue:UIColor { 40 | return UIColor(red: 55, green: 99, blue: 205) 41 | } 42 | 43 | static var fieldGreen:UIColor { 44 | return UIColor(red: 46, green: 73, blue: 13) 45 | } 46 | 47 | static var barStyle: UIBarStyle { 48 | return .default 49 | } 50 | 51 | class func apply() { 52 | 53 | // MARK: UINavigationBar 54 | UINavigationBar.appearance().barStyle = barStyle 55 | UINavigationBar.appearance().tintColor = whiteColor 56 | UINavigationBar.appearance().barTintColor = mainColor 57 | UINavigationBar.appearance().isTranslucent = false 58 | 59 | // MARK: UIBarButtonItem 60 | let attributes = [NSAttributedString.Key.foregroundColor: UIColor.white] 61 | let disabledAttributes = [NSAttributedString.Key.foregroundColor: UIColor.gray] 62 | UIBarButtonItem.appearance(whenContainedInInstancesOf: [UINavigationController.self]).setTitleTextAttributes(attributes, for: .normal) 63 | UIBarButtonItem.appearance(whenContainedInInstancesOf: [UINavigationController.self]).setTitleTextAttributes(disabledAttributes, for: .disabled) 64 | UINavigationBar.appearance().titleTextAttributes = attributes 65 | 66 | // MARK: UITabBar 67 | UITabBar.appearance().backgroundColor = .white 68 | UITabBar.appearance().tintColor = Theme.darkTextColor 69 | 70 | // MARK: UISwitch 71 | UISwitch.appearance().onTintColor = mainColor 72 | 73 | // MARK: UISlider 74 | UISlider.appearance().tintColor = mainColor 75 | 76 | // MARK: UIButton 77 | UIButton.appearance(whenContainedInInstancesOf: [UITableViewCell.self]).tintColor = mainColor 78 | UIButton.appearance(whenContainedInInstancesOf: [UIScrollView.self]).tintColor = mainColor 79 | 80 | // MARK: UITextField 81 | UITextField.appearance(whenContainedInInstancesOf: [UITableViewCell.self]).tintColor = whiteColor 82 | UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self]).tintColor = mainColor 83 | UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self]).backgroundColor = whiteColor 84 | UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self]).defaultTextAttributes = [NSAttributedString.Key.foregroundColor: textColor] 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /ContainerViewControllers/ViewControllers/CardViewController.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | 4 | final class CardViewController: UIViewController, CardViewProtocol { 5 | 6 | var handleArea: UIView! 7 | 8 | private var imageView:UIImageView = { 9 | let imageView = UIImageView() 10 | imageView.image = UIImage(named: "dmitriy-ilkevich-boots") 11 | imageView.contentMode = .scaleAspectFit 12 | imageView.clipsToBounds = true 13 | return imageView 14 | }() 15 | 16 | weak var delegate: CardViewControllerGesturesDelegate? 17 | var handle: UIView { return handleArea } 18 | 19 | deinit { 20 | print("deallocing \(self)") 21 | } 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | view.backgroundColor = .clear 26 | setupViews() 27 | setupGestures() 28 | } 29 | 30 | override func viewDidLayoutSubviews() { 31 | super.viewDidLayoutSubviews() 32 | setupViewFrames() 33 | } 34 | 35 | private func setupViews() { 36 | handleArea = HandleView(frame: .zero) 37 | handleArea.backgroundColor = Theme.mainColor 38 | view.addSubview(imageView) 39 | view.addSubview(handleArea) 40 | setupViewFrames() 41 | } 42 | 43 | private func setupViewFrames() { 44 | handleArea.frame = CGRect(x: 0, y: 0, width: view.frame.width, height: 30.0) 45 | imageView.frame = CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height) 46 | } 47 | 48 | private func setupGestures() { 49 | let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleCardTap(gesture:))) 50 | let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handleCardPan(gesture:))) 51 | handleArea.addGestureRecognizer(tapGesture) 52 | handleArea.addGestureRecognizer(panGesture) 53 | } 54 | 55 | @objc func handleCardTap(gesture:UITapGestureRecognizer){ 56 | delegate?.handleTapGesture(tapGesture: gesture) 57 | } 58 | 59 | @objc func handleCardPan(gesture:UIPanGestureRecognizer) { 60 | delegate?.handlePanGesture(panGesture: gesture) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /ContainerViewControllers/ViewControllers/CustomViews/CircularImageView.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | import UIKit.UIView 4 | 5 | class CircularImageView : UIImageView { 6 | 7 | let maskLayer = CAShapeLayer() 8 | 9 | override init(frame: CGRect) { 10 | super.init(frame: frame) 11 | configure() 12 | } 13 | 14 | required init?(coder aDecoder: NSCoder) { 15 | super.init(coder: aDecoder) 16 | configure() 17 | } 18 | 19 | override func layoutSubviews() { 20 | super.layoutSubviews() 21 | configure() 22 | } 23 | 24 | //create circular mask to round image view 25 | private func configure () { 26 | maskLayer.bounds = bounds 27 | maskLayer.frame = bounds 28 | let path = UIBezierPath(ovalIn: bounds) 29 | maskLayer.path = path.cgPath 30 | layer.mask = maskLayer 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ContainerViewControllers/ViewControllers/CustomViews/ContentView.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | 4 | class ContentView: UIView { 5 | 6 | //public api for class 7 | public var image:UIImage? { 8 | didSet{ 9 | imageView.image = image 10 | } 11 | } 12 | 13 | public var content:String? { 14 | didSet{ 15 | contentLabel.text = content 16 | contentLabel.sizeToFit() 17 | } 18 | } 19 | 20 | private let imageView:UIImageView = { 21 | let imageView = UIImageView() 22 | imageView.layer.masksToBounds = true 23 | imageView.contentMode = .scaleAspectFill 24 | imageView.translatesAutoresizingMaskIntoConstraints = false 25 | return imageView 26 | }() 27 | 28 | private var contentLabel:UILabel = { 29 | let label = UILabel() 30 | label.translatesAutoresizingMaskIntoConstraints = false 31 | label.backgroundColor = Theme.lightBackgroundColor 32 | label.textColor = Theme.darkTextColor 33 | label.numberOfLines = 0 34 | label.font = Fonts.caption 35 | return label 36 | }() 37 | 38 | private var contentHolder:UIView = { 39 | let view = UIView() 40 | return view 41 | }() 42 | 43 | override init(frame: CGRect) { 44 | super.init(frame: frame) 45 | createSubviews() 46 | } 47 | 48 | required init?(coder aDecoder: NSCoder) { 49 | super.init(coder: aDecoder) 50 | createSubviews() 51 | } 52 | 53 | private func createSubviews() { 54 | backgroundColor = Theme.lightBackgroundColor 55 | addSubview(imageView) 56 | addSubview(contentHolder) 57 | contentHolder.addSubview(contentLabel) 58 | } 59 | 60 | override func layoutSubviews() { 61 | super.layoutSubviews() 62 | configureImageView() 63 | configureLabel() 64 | } 65 | 66 | private func configureImageView(){ 67 | imageView.frame = .init(x: 0, y: 0, width:frame.width , height: ceil(frame.height * 0.50)) 68 | } 69 | 70 | private func configureLabel(){ 71 | var origin = imageView.frame.origin 72 | origin.y += imageView.frame.height 73 | let height = ceil(frame.height - imageView.frame.height - safeAreaInsets.bottom) 74 | let size = CGSize(width: imageView.frame.width, height: height) 75 | contentHolder.frame = .init(origin: origin, size: size) 76 | contentLabel.frame = contentHolder.bounds.insetBy(dx: 20, dy: 20) 77 | } 78 | 79 | override var intrinsicContentSize: CGSize { 80 | return CGSize(width: frame.width, height: frame.maxY) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /ContainerViewControllers/ViewControllers/CustomViews/HandleView.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit.UIView 3 | 4 | class HandleView : UIView { 5 | 6 | private let lineView = UIView() 7 | 8 | override init(frame: CGRect) { 9 | super.init(frame: frame) 10 | addSubviews() 11 | } 12 | 13 | required init?(coder aDecoder: NSCoder) { 14 | fatalError("init(coder:) has not been implemented") 15 | } 16 | 17 | override func layoutSubviews() { 18 | super.layoutSubviews() 19 | configureSubviews() 20 | } 21 | 22 | private func addSubviews(){ 23 | lineView.backgroundColor = Theme.whiteColor 24 | addSubview(lineView) 25 | configureSubviews() 26 | } 27 | 28 | private func configureSubviews() { 29 | let lineFrame = CGRect(x: 0, y: 0, width: frame.width * 0.25, height: frame.height * 0.25) 30 | lineView.layer.cornerRadius = frame.height * 0.20 31 | lineView.frame = lineFrame 32 | lineView.center = self.center 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ContainerViewControllers/ViewControllers/CustomViews/ProfileView.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | 4 | class UserProfileView: UIView { 5 | 6 | //public api for view 7 | public var image:UIImage? { 8 | didSet{ 9 | imageView.image = image 10 | } 11 | } 12 | 13 | public var name:String? { 14 | didSet{ 15 | nameLabel.text = name 16 | nameLabel.sizeToFit() 17 | } 18 | } 19 | 20 | public var quote:String? { 21 | didSet{ 22 | quoteLabel.text = quote 23 | quoteLabel.sizeToFit() 24 | } 25 | } 26 | 27 | private var imageView:CircularImageView = { 28 | let imageView = CircularImageView(frame: .zero) 29 | imageView.translatesAutoresizingMaskIntoConstraints = false 30 | return imageView 31 | }() 32 | 33 | private var nameLabel:UILabel = { 34 | let label = UILabel() 35 | label.translatesAutoresizingMaskIntoConstraints = false 36 | label.textColor = Theme.textColor 37 | label.textAlignment = .center 38 | label.numberOfLines = 0 39 | label.font = Fonts.caption 40 | return label 41 | }() 42 | 43 | private var quoteLabel:UILabel = { 44 | let label = UILabel() 45 | label.translatesAutoresizingMaskIntoConstraints = false 46 | label.textColor = Theme.textColor 47 | label.numberOfLines = 0 48 | label.font = Fonts.body 49 | return label 50 | }() 51 | 52 | private let spacing:CGFloat = 8 53 | private let imageSize:CGSize = CGSize(width: 200, height: 200) 54 | 55 | override init(frame: CGRect) { 56 | super.init(frame: frame) 57 | createSubviews() 58 | } 59 | 60 | required init?(coder aDecoder: NSCoder) { 61 | super.init(coder: aDecoder) 62 | createSubviews() 63 | } 64 | 65 | private func createSubviews() { 66 | backgroundColor = Theme.mutedMainColor 67 | addSubview(imageView) 68 | addSubview(nameLabel) 69 | addSubview(quoteLabel) 70 | } 71 | 72 | override func layoutSubviews() { 73 | super.layoutSubviews() 74 | configureImageView() 75 | configureNameLabel() 76 | configureQuoteLabel() 77 | } 78 | 79 | private func configureImageView(){ 80 | let xOrigin = ceil(frame.midX - imageSize.width/2) 81 | imageView.frame = CGRect(x: xOrigin, y: spacing, width: imageSize.width, height: imageSize.height) 82 | } 83 | 84 | private func configureNameLabel(){ 85 | let yOrigin = ceil(imageView.frame.maxY + spacing) 86 | let width = floor(frame.width - layoutMargins.left - layoutMargins.right) 87 | var height:CGFloat = 0 88 | if let labelText = nameLabel.text { 89 | let attributes = [NSAttributedString.Key.font: nameLabel.font!] 90 | height = calculateTextHeight(width: width, text: labelText, attributes: attributes) 91 | } 92 | nameLabel.frame = CGRect(x: layoutMargins.left, y: yOrigin , width: width, height: height) 93 | } 94 | 95 | private func configureQuoteLabel(){ 96 | let yOrigin = ceil(nameLabel.frame.maxY + spacing) 97 | let width = floor(frame.width - layoutMargins.left - layoutMargins.right) 98 | var height:CGFloat = 0 99 | if let labelText = quoteLabel.text { 100 | let attributes = [NSAttributedString.Key.font: quoteLabel.font!] 101 | height = calculateTextHeight(width: width, text: labelText, attributes: attributes) 102 | } 103 | quoteLabel.frame = CGRect(x: layoutMargins.left, y: yOrigin , width: width, height: height) 104 | } 105 | 106 | override var intrinsicContentSize: CGSize { 107 | return CGSize(width: frame.width, height: frame.maxY) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /ContainerViewControllers/ViewControllers/CustomViews/StoryView.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | 4 | class StoryView: UIView { 5 | 6 | //public api for view 7 | public var image:UIImage? { 8 | didSet{ 9 | imageView.image = image 10 | } 11 | } 12 | 13 | public var text:String? { 14 | didSet{ 15 | textLabel.text = text 16 | textLabel.sizeToFit() 17 | } 18 | } 19 | 20 | public var content: String? { 21 | didSet { 22 | textView.text = content 23 | textLabel.sizeToFit() 24 | } 25 | } 26 | 27 | public var labelColor:UIColor? { 28 | didSet { 29 | textLabel.backgroundColor = labelColor 30 | } 31 | } 32 | 33 | private var imageView:UIImageView = { 34 | let imageView = UIImageView() 35 | imageView.translatesAutoresizingMaskIntoConstraints = false 36 | return imageView 37 | }() 38 | 39 | private var textLabel:UILabel = { 40 | let label = UILabel() 41 | label.translatesAutoresizingMaskIntoConstraints = false 42 | label.textColor = Theme.textColor 43 | label.textAlignment = .center 44 | label.font = Fonts.caption 45 | return label 46 | }() 47 | 48 | private var textView:UITextView = { 49 | let textView = UITextView() 50 | textView.translatesAutoresizingMaskIntoConstraints = false 51 | textView.textColor = Theme.darkTextColor 52 | textView.font = Fonts.body 53 | textView.isEditable = false 54 | return textView 55 | }() 56 | 57 | override init(frame: CGRect) { 58 | super.init(frame: frame) 59 | createSubviews() 60 | } 61 | 62 | required init?(coder aDecoder: NSCoder) { 63 | super.init(coder: aDecoder) 64 | createSubviews() 65 | } 66 | 67 | private func createSubviews() { 68 | addSubview(imageView) 69 | addSubview(textLabel) 70 | addSubview(textView) 71 | } 72 | 73 | override func layoutSubviews() { 74 | super.layoutSubviews() 75 | layoutImageView() 76 | layoutTextLabel() 77 | layoutTextView() 78 | } 79 | 80 | func layoutImageView() { 81 | let size = frame.size 82 | imageView.frame = CGRect(x: 0, y:0, width: size.width, height: ceil(size.height * 0.40)) 83 | } 84 | 85 | func layoutTextLabel() { 86 | let imageFrame = imageView.frame 87 | let width = imageView.frame.size.width 88 | var height:CGFloat = 0 89 | 90 | if let labelText = textLabel.text { 91 | let attributes = [NSAttributedString.Key.font: textLabel.font!] 92 | height = calculateTextHeight(width: width, text: labelText, attributes: attributes) 93 | } 94 | 95 | textLabel.frame = CGRect(x: imageFrame.origin.x, y: imageFrame.maxY, width: width, height: height) 96 | } 97 | 98 | func layoutTextView() { 99 | let yOrigin = textLabel.frame.maxY 100 | let width = textLabel.frame.size.width 101 | let height:CGFloat = ceil(frame.height - textLabel.frame.maxY) 102 | textView.frame = CGRect(x: imageView.frame.origin.x, y: yOrigin, width: width, height:height) 103 | } 104 | 105 | override var intrinsicContentSize: CGSize { 106 | return CGSize(width: frame.width, height: frame.maxY) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /ContainerViewControllers/ViewControllers/ImageViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class ImageViewController: UIViewController { 4 | 5 | private var imageLoader:ImageLoader? = nil 6 | private let imageView:UIImageView = { 7 | let imageView = UIImageView() 8 | imageView.layer.masksToBounds = true 9 | imageView.contentMode = .scaleAspectFill 10 | return imageView 11 | }() 12 | 13 | convenience init(imageLoader:ImageLoader) { 14 | self.init() 15 | self.imageLoader = imageLoader 16 | } 17 | 18 | deinit { 19 | print("deallocing \(self)") 20 | } 21 | 22 | override func viewDidLoad() { 23 | super.viewDidLoad() 24 | view.addSubview(imageView) 25 | layoutImageView() 26 | let loadingController = LoadingViewController() 27 | add(child: loadingController) 28 | loadImage() 29 | } 30 | 31 | private func layoutImageView(){ 32 | imageView.frame = view.frame 33 | } 34 | 35 | override func viewWillLayoutSubviews() { 36 | super.viewWillLayoutSubviews() 37 | layoutImageView() 38 | } 39 | 40 | private func loadImage() { 41 | imageLoader?.loadImage { [weak self] (image) in 42 | DispatchQueue.main.async { 43 | self?.imageView.image = image 44 | let _ = self?.children 45 | .filter { $0 is LoadingViewController } 46 | .map{ self?.remove(child: $0) } 47 | } 48 | } 49 | } 50 | } 51 | 52 | -------------------------------------------------------------------------------- /ContainerViewControllers/ViewControllers/ListDetailViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class ListDetailViewController: UIViewController { 4 | 5 | let loadingController = LoadingViewController() 6 | private var contentView = ContentView() 7 | private let item:ItemViewModel 8 | private let imageLoader:ImageLoader? 9 | 10 | init(imageLoader:ImageLoader?, item:ItemViewModel) { 11 | self.item = item 12 | self.imageLoader = imageLoader 13 | super.init(nibName: nil, bundle: nil) 14 | } 15 | 16 | required init?(coder aDecoder: NSCoder) { 17 | fatalError("init(coder:) has not been implemented") 18 | } 19 | 20 | deinit { 21 | print("deallocing \(self)") 22 | } 23 | 24 | override func loadView() { 25 | contentView.bounds = UIScreen.main.bounds 26 | view = contentView 27 | } 28 | 29 | override func viewDidLoad() { 30 | super.viewDidLoad() 31 | loadingController.color = .white 32 | add(child: loadingController) 33 | contentView.content = item.title 34 | imageLoader?.loadImage { [unowned self] (image) in 35 | DispatchQueue.main.async { 36 | self.contentView.image = image 37 | self.remove(child: self.loadingController) 38 | } 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /ContainerViewControllers/ViewControllers/ListViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class ListViewController: UIViewController { 4 | 5 | weak var delegate:ListViewControllerDelegate? 6 | private let loadingController = LoadingViewController() 7 | private let tableView = UITableView() 8 | private let listLoader:ListLoader 9 | private var dataSource:ListDataSource? 10 | private var previewingItem:ItemViewModel? 11 | 12 | init(listLoader:ListLoader){ 13 | self.listLoader = listLoader 14 | super.init(nibName: nil, bundle: nil) 15 | } 16 | 17 | required init?(coder aDecoder: NSCoder) { 18 | fatalError("init(coder:) has not been implemented") 19 | } 20 | 21 | deinit { 22 | print("deallocing \(self)") 23 | } 24 | 25 | override func viewDidLoad() { 26 | super.viewDidLoad() 27 | configureTableView() 28 | add(child: loadingController) 29 | loadData() 30 | } 31 | 32 | private func configureTableView() { 33 | //add peek & pop functionality 34 | registerForPreviewing(with: self, sourceView: tableView) 35 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cellId") 36 | tableView.estimatedRowHeight = 50 37 | tableView.rowHeight = UITableView.automaticDimension 38 | tableView.delegate = self 39 | view.addSubview(tableView) 40 | tableView.tableFooterView = UIView() 41 | layoutTableView() 42 | } 43 | 44 | override func viewWillLayoutSubviews() { 45 | super.viewWillLayoutSubviews() 46 | layoutTableView() 47 | } 48 | 49 | private func layoutTableView(){ 50 | var contentSize = tableView.contentSize 51 | contentSize.height += view.frame.origin.y 52 | tableView.contentSize = contentSize 53 | tableView.frame = view.frame 54 | } 55 | 56 | private func loadData() { 57 | listLoader.loadItems { (items) in 58 | if let items = items { 59 | self.dataSource = ListDataSource(viewModels: items) 60 | DispatchQueue.main.async { 61 | self.tableView.dataSource = self.dataSource 62 | self.tableView.reloadData() 63 | self.remove(child: self.loadingController) 64 | } 65 | } 66 | } 67 | } 68 | } 69 | 70 | extension ListViewController : UITableViewDelegate { 71 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 72 | guard let item = dataSource?.item(at: indexPath) else { return } 73 | delegate?.didSelectItem(item: item) 74 | } 75 | } 76 | 77 | /** 78 | * peek and pop behavior 79 | */ 80 | extension ListViewController : UIViewControllerPreviewingDelegate { 81 | 82 | /* 83 | * Called to let you prepare the presentation of a commit (pop) view from your commit view 84 | * controller. 85 | * @param The context object for the previewing view controller. 86 | * @param The location of the touch in the source view’s coordinate system. 87 | * @note we need to create an instance of the detail view controller 88 | */ 89 | func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? { 90 | 91 | if let indexPath = tableView.indexPathForRow(at: location) { 92 | previewingItem = dataSource?.item(at: indexPath) ?? nil 93 | previewingContext.sourceRect = tableView.rectForRow(at: indexPath) 94 | return getDetailViewController(for: indexPath) 95 | } 96 | 97 | return nil 98 | } 99 | 100 | /* 101 | * Called when the user has pressed a source view in a previewing view controller, 102 | * thereby obtaining a surrounding blur to indicate that a preview (peek) is available. 103 | * @param The context object for the previewing view controller. 104 | * @param The view controller whose view your implementation of this method is moving 105 | * into place as a commit (pop) view. 106 | * @note Here we just send the normal didSelectItem delegat call to the coordinator 107 | */ 108 | func previewingContext(_ previewingContext: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController) { 109 | if let previewingItem = previewingItem { 110 | delegate?.didSelectItem(item: previewingItem) 111 | } 112 | } 113 | 114 | /** 115 | * 116 | */ 117 | private func getDetailViewController(for indexPath:IndexPath) -> UIViewController? { 118 | if let item = dataSource?.item(at: indexPath) { 119 | let imageLoader = ImageLoader.imageLoader(urlString: item.url) 120 | let viewController = ListDetailViewController(imageLoader: imageLoader, item: item) 121 | return viewController 122 | } 123 | return nil 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /ContainerViewControllers/ViewControllers/LoadingViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class LoadingViewController: UIViewController { 4 | 5 | public var color:UIColor? { 6 | didSet { 7 | activityIndicator.color = color 8 | } 9 | } 10 | 11 | private var activityIndicator:UIActivityIndicatorView = { 12 | let indicator = UIActivityIndicatorView(style: .whiteLarge) 13 | indicator.translatesAutoresizingMaskIntoConstraints = false 14 | indicator.color = Theme.mainColor 15 | return indicator 16 | }() 17 | 18 | deinit { 19 | print("deallocing \(self)") 20 | } 21 | 22 | override func viewDidLoad() { 23 | super.viewDidLoad() 24 | view.addSubview(activityIndicator) 25 | } 26 | 27 | override func viewDidLayoutSubviews() { 28 | super.viewDidLayoutSubviews() 29 | activityIndicator.center = view.center 30 | } 31 | 32 | override func viewWillAppear(_ animated: Bool) { 33 | super.viewWillAppear(animated) 34 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak self] in 35 | self?.activityIndicator.startAnimating() 36 | } 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /ContainerViewControllers/ViewControllers/StoryViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class StoryViewController: UIViewController { 4 | 5 | var labelColor:UIColor = Theme.redSky { 6 | didSet { 7 | storyView.labelColor = labelColor 8 | } 9 | } 10 | 11 | private var storyView = StoryView() 12 | private let viewModel:StoryViewModel 13 | 14 | init(viewModel:StoryViewModel) { 15 | self.viewModel = viewModel 16 | super.init(nibName: nil, bundle: nil) 17 | } 18 | 19 | required init?(coder aDecoder: NSCoder) { 20 | fatalError("init(coder:) has not been implemented") 21 | } 22 | 23 | override func loadView() { 24 | storyView.bounds = UIScreen.main.bounds 25 | view = storyView 26 | } 27 | 28 | override func viewDidLoad() { 29 | super.viewDidLoad() 30 | storyView.image = UIImage(named: viewModel.imageName) 31 | storyView.text = viewModel.text 32 | storyView.content = viewModel.content 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ContainerViewControllers/ViewControllers/TabBarController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class TabBarController: UITabBarController { 4 | 5 | public func setIconImages(titles:[String]) { 6 | guard let items = self.tabBar.items else { return } 7 | 8 | for (index, element) in items.enumerated() { 9 | let image = UIImage(named:titles[index]) 10 | element.image = image 11 | } 12 | } 13 | 14 | public func setTitles(titles:[String]) { 15 | guard let items = self.tabBar.items else { return } 16 | 17 | for (index, element) in items.enumerated() { 18 | element.title = titles[index] 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ContainerViewControllers/ViewControllers/UserProfileViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class UserProfileViewController: UIViewController { 4 | 5 | private var profileView = UserProfileView() 6 | private let viewModel:ProfileViewModel 7 | 8 | init(viewModel:ProfileViewModel) { 9 | self.viewModel = viewModel 10 | super.init(nibName: nil, bundle: nil) 11 | } 12 | 13 | required init?(coder aDecoder: NSCoder) { 14 | fatalError("init(coder:) has not been implemented") 15 | } 16 | 17 | override func loadView() { 18 | profileView.bounds = UIScreen.main.bounds 19 | view = profileView 20 | preferredContentSize = view.intrinsicContentSize 21 | } 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | profileView.image = UIImage(named: viewModel.imageName) 26 | profileView.name = viewModel.name 27 | profileView.quote = viewModel.quote 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /ContainerViewControllers/dmitriy-ilkevich-boots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkw5877/ContainerViewControllers/06cca59cd300b9223595b16763649297d58cccd3/ContainerViewControllers/dmitriy-ilkevich-boots.png -------------------------------------------------------------------------------- /ContainerViewControllers/dmitriy-ilkevich.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkw5877/ContainerViewControllers/06cca59cd300b9223595b16763649297d58cccd3/ContainerViewControllers/dmitriy-ilkevich.png -------------------------------------------------------------------------------- /InteractiveContainer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkw5877/ContainerViewControllers/06cca59cd300b9223595b16763649297d58cccd3/InteractiveContainer.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Example of using Coordinators and Container View Controllers in iOS 2 | 3 | You can find the related blog post on Medium at [Container View Controllers Redux](https://medium.com/@dkw5877/container-view-controllers-revisited-e076ef38853f) 4 | 5 | ### Points of Interest 6 | In general the sample code covers the use of container view controller in iOS development to illustrate the concepts of separation of concerns, composition, and the application controller pattern. 7 | 8 | #### Coordinators 9 | The app uses the concept of Coordinators to handle the navigation flow of the app. 10 | 11 | #### Basic View Controller Containment 12 | The sample code uses view controller containment to combine an image loading view controller and a table view controller to form a screen of content. 13 | 14 | #### Scrolling Container View Controller 15 | The sample code shows an example of a scrolling container view controller. The container view controller overrides load view to use a UIScrollView where it displays the contents of injected view controllers 16 | 17 | #### StackView Controller 18 | The sample code shows an example an example of a stack view container view controller. Similar to the scrolling view controller, this view controller overrides loadView to implement a UIStackView that holds the contents of two view controllers. The stack view controller is then added to a scrolling view controller to allow content scrolling. 19 | 20 | #### Card Container View Controller 21 | The sample code shows an example of using a container view controller to hold a screen of content and interactively display a separate card view controller. This example illustrates an alternative to coordinators by encapsulating the functionality of two view controllers into a single view controller. This concept could be extend to encapsulte application flows in a container view controller. 22 | 23 | ![Container](Container.png) 24 | ![Scrolling Container](ScrollingContainer.png) 25 | ![Scrolling StackView](ScrollingStackView.png) 26 | ![Interactive Container](InteractiveContainer.png) 27 | -------------------------------------------------------------------------------- /ScrollingContainer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkw5877/ContainerViewControllers/06cca59cd300b9223595b16763649297d58cccd3/ScrollingContainer.png -------------------------------------------------------------------------------- /ScrollingStackView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkw5877/ContainerViewControllers/06cca59cd300b9223595b16763649297d58cccd3/ScrollingStackView.png -------------------------------------------------------------------------------- /images/article/CardContainers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkw5877/ContainerViewControllers/06cca59cd300b9223595b16763649297d58cccd3/images/article/CardContainers.png -------------------------------------------------------------------------------- /images/article/ContainerViewController.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkw5877/ContainerViewControllers/06cca59cd300b9223595b16763649297d58cccd3/images/article/ContainerViewController.png -------------------------------------------------------------------------------- /images/article/ScrollingContent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkw5877/ContainerViewControllers/06cca59cd300b9223595b16763649297d58cccd3/images/article/ScrollingContent.png -------------------------------------------------------------------------------- /images/article/StackView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkw5877/ContainerViewControllers/06cca59cd300b9223595b16763649297d58cccd3/images/article/StackView.png -------------------------------------------------------------------------------- /images/article/Stackview-lanscape-540x250.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkw5877/ContainerViewControllers/06cca59cd300b9223595b16763649297d58cccd3/images/article/Stackview-lanscape-540x250.png -------------------------------------------------------------------------------- /images/originals/CardContainer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkw5877/ContainerViewControllers/06cca59cd300b9223595b16763649297d58cccd3/images/originals/CardContainer.png -------------------------------------------------------------------------------- /images/originals/CardExpanded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkw5877/ContainerViewControllers/06cca59cd300b9223595b16763649297d58cccd3/images/originals/CardExpanded.png -------------------------------------------------------------------------------- /images/originals/ContainerExample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkw5877/ContainerViewControllers/06cca59cd300b9223595b16763649297d58cccd3/images/originals/ContainerExample.png -------------------------------------------------------------------------------- /images/originals/Scrolling1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkw5877/ContainerViewControllers/06cca59cd300b9223595b16763649297d58cccd3/images/originals/Scrolling1.png -------------------------------------------------------------------------------- /images/originals/Scrolling2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkw5877/ContainerViewControllers/06cca59cd300b9223595b16763649297d58cccd3/images/originals/Scrolling2.png -------------------------------------------------------------------------------- /images/originals/ScrollingContent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkw5877/ContainerViewControllers/06cca59cd300b9223595b16763649297d58cccd3/images/originals/ScrollingContent.png -------------------------------------------------------------------------------- /images/originals/StackView-landscape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkw5877/ContainerViewControllers/06cca59cd300b9223595b16763649297d58cccd3/images/originals/StackView-landscape.png -------------------------------------------------------------------------------- /images/originals/StackView1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkw5877/ContainerViewControllers/06cca59cd300b9223595b16763649297d58cccd3/images/originals/StackView1.png -------------------------------------------------------------------------------- /images/originals/StackView2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkw5877/ContainerViewControllers/06cca59cd300b9223595b16763649297d58cccd3/images/originals/StackView2.png -------------------------------------------------------------------------------- /images/originals/TableDetailScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkw5877/ContainerViewControllers/06cca59cd300b9223595b16763649297d58cccd3/images/originals/TableDetailScreen.png -------------------------------------------------------------------------------- /images/originals/dmitriy-ilkevich-1169660-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkw5877/ContainerViewControllers/06cca59cd300b9223595b16763649297d58cccd3/images/originals/dmitriy-ilkevich-1169660-unsplash.jpg -------------------------------------------------------------------------------- /images/originals/dmitriy-ilkevich-1169661-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkw5877/ContainerViewControllers/06cca59cd300b9223595b16763649297d58cccd3/images/originals/dmitriy-ilkevich-1169661-unsplash.jpg -------------------------------------------------------------------------------- /images/originals/raghu-nayyar-501556-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkw5877/ContainerViewControllers/06cca59cd300b9223595b16763649297d58cccd3/images/originals/raghu-nayyar-501556-unsplash.jpg -------------------------------------------------------------------------------- /images/scaled/CardContainer-250x540.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkw5877/ContainerViewControllers/06cca59cd300b9223595b16763649297d58cccd3/images/scaled/CardContainer-250x540.png -------------------------------------------------------------------------------- /images/scaled/CardExpanded-250x540.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkw5877/ContainerViewControllers/06cca59cd300b9223595b16763649297d58cccd3/images/scaled/CardExpanded-250x540.png -------------------------------------------------------------------------------- /images/scaled/ContainerExample-250x540.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkw5877/ContainerViewControllers/06cca59cd300b9223595b16763649297d58cccd3/images/scaled/ContainerExample-250x540.png -------------------------------------------------------------------------------- /images/scaled/ScrollingContent-250x540.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkw5877/ContainerViewControllers/06cca59cd300b9223595b16763649297d58cccd3/images/scaled/ScrollingContent-250x540.png -------------------------------------------------------------------------------- /images/scaled/TableDetailScreen-250x540.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkw5877/ContainerViewControllers/06cca59cd300b9223595b16763649297d58cccd3/images/scaled/TableDetailScreen-250x540.png --------------------------------------------------------------------------------