├── Images ├── AppScreenShots.png ├── Plugin_loading_topStoriesUI.png ├── Test_coverage.png └── TopStoriesUI_class_diagram.png ├── NewsApp ├── Cartfile ├── Cartfile.resolved ├── Carthage │ ├── Build │ │ ├── .VSCollectionKit.version │ │ └── iOS │ │ │ ├── 58163685-B3EC-3A67-85DD-1D3550CBA96C.bcsymbolmap │ │ │ ├── 95E23107-6DB3-3DC3-8E01-205BABC4BFF0.bcsymbolmap │ │ │ ├── BAAB7A1B-0612-3B8E-B8EF-5A68AE91C7E0.bcsymbolmap │ │ │ ├── VSCollectionKit.framework.dSYM │ │ │ └── Contents │ │ │ │ ├── Info.plist │ │ │ │ └── Resources │ │ │ │ └── DWARF │ │ │ │ └── VSCollectionKit │ │ │ └── VSCollectionKit.framework │ │ │ ├── Headers │ │ │ ├── VSCollectionKit-Swift.h │ │ │ └── VSCollectionKit.h │ │ │ ├── Info.plist │ │ │ ├── Modules │ │ │ ├── VSCollectionKit.swiftmodule │ │ │ │ ├── arm64-apple-ios.swiftdoc │ │ │ │ ├── arm64-apple-ios.swiftmodule │ │ │ │ ├── arm64.swiftdoc │ │ │ │ ├── arm64.swiftmodule │ │ │ │ ├── x86_64-apple-ios-simulator.swiftdoc │ │ │ │ ├── x86_64-apple-ios-simulator.swiftmodule │ │ │ │ ├── x86_64.swiftdoc │ │ │ │ └── x86_64.swiftmodule │ │ │ └── module.modulemap │ │ │ └── VSCollectionKit │ └── Checkouts │ │ └── VSCollectionKit │ │ └── VSCollectionKit │ │ ├── Carthage │ │ └── Build │ │ │ └── iOS │ │ │ ├── 3E3E151D-62CA-3BED-B305-B194909B94A4.bcsymbolmap │ │ │ ├── VSCollectionKit.framework.dSYM │ │ │ └── Contents │ │ │ │ ├── Info.plist │ │ │ │ └── Resources │ │ │ │ └── DWARF │ │ │ │ └── VSCollectionKit │ │ │ └── VSCollectionKit.framework │ │ │ ├── Headers │ │ │ ├── VSCollectionKit-Swift.h │ │ │ └── VSCollectionKit.h │ │ │ ├── Info.plist │ │ │ ├── Modules │ │ │ ├── VSCollectionKit.swiftmodule │ │ │ │ ├── arm64-apple-ios.swiftdoc │ │ │ │ ├── arm64-apple-ios.swiftmodule │ │ │ │ ├── arm64.swiftdoc │ │ │ │ ├── arm64.swiftmodule │ │ │ │ ├── x86_64-apple-ios-simulator.swiftdoc │ │ │ │ ├── x86_64-apple-ios-simulator.swiftmodule │ │ │ │ ├── x86_64.swiftdoc │ │ │ │ └── x86_64.swiftmodule │ │ │ └── module.modulemap │ │ │ └── VSCollectionKit │ │ ├── CollectionKitTestApp │ │ ├── AlbumsCollectionController.swift │ │ ├── AlbumsCollectionViewModel.swift │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── first.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── first.pdf │ │ │ └── second.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── second.pdf │ │ ├── Base.lproj │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ ├── PhotoData │ │ │ ├── DSCF6496.jpg │ │ │ ├── DSCF6513.jpg │ │ │ ├── DSCF6518.jpg │ │ │ ├── DSCF6520.jpg │ │ │ ├── DSCF6528.jpg │ │ │ ├── DSCF6531.jpg │ │ │ ├── DSCF6533.jpg │ │ │ ├── DSCF6544.jpg │ │ │ ├── DSCF6558.jpg │ │ │ ├── DSCF6562.jpg │ │ │ ├── DSCF6588.jpg │ │ │ ├── DSCF6590.jpg │ │ │ ├── DSCF6593.jpg │ │ │ ├── DSCF6597.jpg │ │ │ ├── DSCF6612.jpg │ │ │ ├── DSCF6614.jpg │ │ │ ├── DSCF6615.jpg │ │ │ ├── DSCF6629.jpg │ │ │ ├── DSCF6631.jpg │ │ │ └── DSCF6632.jpg │ │ ├── PhotoTumbnailCell.swift │ │ ├── PhotosSectionHandler.swift │ │ └── SceneDelegate.swift │ │ ├── VSCollectionKit.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ ├── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ │ └── xcuserdata │ │ │ │ └── vkg0009.xcuserdatad │ │ │ │ └── UserInterfaceState.xcuserstate │ │ ├── xcshareddata │ │ │ └── xcschemes │ │ │ │ └── VSCollectionKit.xcscheme │ │ └── xcuserdata │ │ │ └── vkg0009.xcuserdatad │ │ │ └── xcdebugger │ │ │ └── Breakpoints_v2.xcbkptlist │ │ ├── VSCollectionKit │ │ ├── Info.plist │ │ ├── VSCollectionKit.h │ │ └── VSCollectionViewController │ │ │ ├── VSCollectionViewController.swift │ │ │ ├── VSCollectionViewData.swift │ │ │ ├── VSCollectionViewDataSource.swift │ │ │ ├── VSCollectionViewDelegate.swift │ │ │ ├── VSCollectionViewLayoutProvider.swift │ │ │ ├── VSCollectionViewSectionHandler.swift │ │ │ ├── VSCollectionViewUpdate.swift │ │ │ └── VSSectionHandlerProtocol.swift │ │ └── VSCollectionKitTests │ │ ├── Info.plist │ │ ├── MockCellModel.swift │ │ ├── MockSectionHandler.swift │ │ ├── MockSectionModel.swift │ │ ├── VSCollectionKitTests.swift │ │ ├── VSCollectionViewDataSourceTests.swift │ │ ├── VSCollectionViewDataTests.swift │ │ ├── VSCollectionViewDelegateTests.swift │ │ ├── VSCollectionViewLayoutProviderTests.swift │ │ └── VSCollectionViewSectionHandlerTests.swift ├── NewsApp.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ ├── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcuserdata │ │ │ └── vkg0009.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ ├── xcshareddata │ │ └── xcschemes │ │ │ ├── NewsApp.xcscheme │ │ │ ├── NewsDetailTests.xcscheme │ │ │ └── NewsDetailUI.xcscheme │ └── xcuserdata │ │ └── vkg0009.xcuserdatad │ │ └── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist ├── NewsApp │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── Core │ │ ├── NewsShared │ │ │ ├── Assets.xcassets │ │ │ │ ├── Contents.json │ │ │ │ └── placeholder.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── placeholder-1.png │ │ │ │ │ ├── placeholder-2.png │ │ │ │ │ └── placeholder.png │ │ │ ├── Extentions │ │ │ │ ├── Date+ElapsedTime.swift │ │ │ │ ├── ImageDownloadManager.swift │ │ │ │ ├── UIImageView+AsyncLoad.swift │ │ │ │ ├── UIView+AutoLayout.swift │ │ │ │ └── UIView+Shadow.swift │ │ │ ├── Info.plist │ │ │ ├── NewsImageSize.swift │ │ │ ├── NewsShared.h │ │ │ ├── NewsTestUtil.swift │ │ │ └── UIConfig │ │ │ │ ├── AppColor.swift │ │ │ │ └── AppSpacing.swift │ │ └── Plugin │ │ │ ├── Info.plist │ │ │ ├── NewsDetailUIAPI.swift │ │ │ ├── Plugin.h │ │ │ ├── PluginManager.swift │ │ │ └── TopStoriesUIAPI.swift │ ├── Features │ │ ├── NewsDetailUI │ │ │ ├── Info.plist │ │ │ ├── NewsDetailTests │ │ │ │ ├── Info.plist │ │ │ │ ├── MockNewsItem.json │ │ │ │ ├── NewsDetailAPIPrivateTests.swift │ │ │ │ ├── NewsDetailsSectionTests │ │ │ │ │ ├── NewsDetailsCellModelTests.swift │ │ │ │ │ ├── NewsDetailsSectionHandlerTests.swift │ │ │ │ │ └── NewsDetailsSectionModelTests.swift │ │ │ │ ├── NewsDetailsViewModelTests.swift │ │ │ │ └── NewsImageSectionTests │ │ │ │ │ ├── NewsImageCellModelTests.swift │ │ │ │ │ ├── NewsImageSectionHandlerTests.swift │ │ │ │ │ └── NewsImageSectionModelTests.swift │ │ │ ├── NewsDetailUI.h │ │ │ ├── NewsDetailViewController │ │ │ │ ├── NewsDetailViewController.swift │ │ │ │ ├── NewsDetailViewModel.swift │ │ │ │ ├── NewsDetailsSection │ │ │ │ │ ├── NewsDetailSectionHandler.swift │ │ │ │ │ ├── NewsDetailSectionModel.swift │ │ │ │ │ ├── NewsDetailsCell.swift │ │ │ │ │ └── NewsDetailsCell.xib │ │ │ │ └── NewsImageSection │ │ │ │ │ ├── NewsImageCell.swift │ │ │ │ │ ├── NewsImageCell.xib │ │ │ │ │ ├── NewsImageSectionHandler.swift │ │ │ │ │ └── NewsImageSectionModel.swift │ │ │ ├── NewsDetailsUI.h │ │ │ └── Plugin │ │ │ │ ├── NewsDetailAPIPrivate.swift │ │ │ │ └── NewsDetailPlugin.swift │ │ └── TopStoriesUI │ │ │ ├── Info.plist │ │ │ ├── Plugin │ │ │ ├── TopStoriesAPIPrivate.swift │ │ │ └── TopStoriesPlugin.swift │ │ │ ├── TopStoriesTests │ │ │ ├── ErrorSection │ │ │ │ ├── ErrorCellModelTests.swift │ │ │ │ ├── ErrorSectionHandlerTests.swift │ │ │ │ └── ErrorSectionModelTests.swift │ │ │ ├── Info.plist │ │ │ ├── LoadingSection │ │ │ │ ├── LoadingCellModelTests.swift │ │ │ │ ├── LoadingSectionHandlerTests.swift │ │ │ │ └── LoadingSectionModelTests.swift │ │ │ ├── MockNewsService.swift │ │ │ ├── NewsSectionModel │ │ │ │ ├── NewsCellModelTests.swift │ │ │ │ ├── NewsSectionHandlerTests.swift │ │ │ │ └── NewsSectionModelTests.swift │ │ │ ├── TopStoriesInteractorTests.swift │ │ │ ├── TopStoriesTests-Bridging-Header.h │ │ │ ├── TopStoriesUI.h │ │ │ └── TopStoriesViewModelTests.swift │ │ │ ├── TopStoriesUI.h │ │ │ └── TopStoriesViewController │ │ │ ├── ErrorSection │ │ │ ├── ErrorCell.swift │ │ │ ├── ErrorCell.xib │ │ │ ├── ErrorCellModel.swift │ │ │ ├── ErrorSectionHandler.swift │ │ │ └── ErrorSectionModel.swift │ │ │ ├── LoadingSection │ │ │ ├── LoadingCell.swift │ │ │ ├── LoadingCell.xib │ │ │ ├── LoadingCellModel.swift │ │ │ ├── LoadingSectionHandler.swift │ │ │ └── LoadingSectionModel.swift │ │ │ ├── NewsSections │ │ │ ├── CardCell │ │ │ │ ├── CardCell.swift │ │ │ │ └── CardCell.xib │ │ │ ├── FullWidthCardCell │ │ │ │ ├── FullWidthCardCell.swift │ │ │ │ └── FullWidthCardCell.xib │ │ │ ├── Header │ │ │ │ ├── NewsHeaderModel.swift │ │ │ │ ├── NewsSectionHeaderView.swift │ │ │ │ └── NewsSectionHeaderView.xib │ │ │ ├── ListCell │ │ │ │ ├── NewsListCell.swift │ │ │ │ └── NewsListCell.xib │ │ │ ├── NewsCellModel.swift │ │ │ ├── NewsLayoutHandler.swift │ │ │ ├── NewsSectionHandler.swift │ │ │ └── NewsSectionModel.swift │ │ │ ├── TopStoriesInteractor.swift │ │ │ ├── TopStoriesViewController.swift │ │ │ └── TopStoriesViewModel.swift │ ├── Info.plist │ ├── MainViewController.swift │ ├── Platform │ │ ├── AppConfigService │ │ │ └── AppConfigService.swift │ │ └── NewsService │ │ │ ├── Info.plist │ │ │ ├── Model │ │ │ └── NewsPage.swift │ │ │ ├── NewsService.h │ │ │ ├── NewsServiceAPI │ │ │ ├── NewsServiceAPIPrivate.swift │ │ │ └── NewsServicePlugin.swift │ │ │ ├── NewsServiceTests │ │ │ ├── Info.plist │ │ │ ├── MockURLSession.swift │ │ │ ├── MockWebService.swift │ │ │ ├── MockedNewsPageErrorResponse.json │ │ │ ├── MockedNewsPageResponse.json │ │ │ ├── NewsServiceAPIPrivateTests.swift │ │ │ └── ServiceRequestTests.swift │ │ │ └── Service │ │ │ └── WebService.swift │ └── SceneDelegate.swift ├── TopStoriesTests │ ├── ErrorSection │ │ ├── ErrorCellModelTests.swift │ │ ├── ErrorSectionHandlerTests.swift │ │ └── ErrorSectionModelTests.swift │ ├── Info.plist │ ├── LoadingSection │ │ ├── LoadingCellModelTests.swift │ │ ├── LoadingSectionHandlerTests.swift │ │ └── LoadingSectionModelTests.swift │ ├── MockNewsService.swift │ ├── NewsSectionModel │ │ ├── NewsCellModelTests.swift │ │ ├── NewsSectionHandlerTests.swift │ │ └── NewsSectionModelTests.swift │ ├── TopStoriesInteractorTests.swift │ └── TopStoriesViewModelTests.swift └── TopStoriesUI │ └── Info.plist └── README.md /Images/AppScreenShots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/Images/AppScreenShots.png -------------------------------------------------------------------------------- /Images/Plugin_loading_topStoriesUI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/Images/Plugin_loading_topStoriesUI.png -------------------------------------------------------------------------------- /Images/Test_coverage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/Images/Test_coverage.png -------------------------------------------------------------------------------- /Images/TopStoriesUI_class_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/Images/TopStoriesUI_class_diagram.png -------------------------------------------------------------------------------- /NewsApp/Cartfile: -------------------------------------------------------------------------------- 1 | git "https://github.com/Vinodh-G/VSCollectionKit.git" >= 0.3 2 | 3 | -------------------------------------------------------------------------------- /NewsApp/Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "Vinodh-G/VSCollectionKit" "v0.3" 2 | -------------------------------------------------------------------------------- /NewsApp/Carthage/Build/.VSCollectionKit.version: -------------------------------------------------------------------------------- 1 | { 2 | "Mac" : [ 3 | 4 | ], 5 | "watchOS" : [ 6 | 7 | ], 8 | "tvOS" : [ 9 | 10 | ], 11 | "commitish" : "v0.3", 12 | "iOS" : [ 13 | { 14 | "name" : "VSCollectionKit", 15 | "hash" : "8a287ef90c997469e3f3364fe14ac864d74c5a41170937aef8218ccca0f0491d" 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /NewsApp/Carthage/Build/iOS/VSCollectionKit.framework.dSYM/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | English 7 | CFBundleIdentifier 8 | com.apple.xcode.dsym.com.vswamy.vscollectionkit.VSCollectionKit 9 | CFBundleInfoDictionaryVersion 10 | 6.0 11 | CFBundlePackageType 12 | dSYM 13 | CFBundleSignature 14 | ???? 15 | CFBundleShortVersionString 16 | 1.0 17 | CFBundleVersion 18 | 1 19 | 20 | 21 | -------------------------------------------------------------------------------- /NewsApp/Carthage/Build/iOS/VSCollectionKit.framework.dSYM/Contents/Resources/DWARF/VSCollectionKit: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Build/iOS/VSCollectionKit.framework.dSYM/Contents/Resources/DWARF/VSCollectionKit -------------------------------------------------------------------------------- /NewsApp/Carthage/Build/iOS/VSCollectionKit.framework/Headers/VSCollectionKit.h: -------------------------------------------------------------------------------- 1 | // 2 | // VSCollectionKit.h 3 | // VSCollectionKit 4 | // 5 | // Created by Vinodh Govindaswamy on 07/04/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for VSCollectionKit. 12 | FOUNDATION_EXPORT double VSCollectionKitVersionNumber; 13 | 14 | //! Project version string for VSCollectionKit. 15 | FOUNDATION_EXPORT const unsigned char VSCollectionKitVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /NewsApp/Carthage/Build/iOS/VSCollectionKit.framework/Info.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Build/iOS/VSCollectionKit.framework/Info.plist -------------------------------------------------------------------------------- /NewsApp/Carthage/Build/iOS/VSCollectionKit.framework/Modules/VSCollectionKit.swiftmodule/arm64-apple-ios.swiftdoc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Build/iOS/VSCollectionKit.framework/Modules/VSCollectionKit.swiftmodule/arm64-apple-ios.swiftdoc -------------------------------------------------------------------------------- /NewsApp/Carthage/Build/iOS/VSCollectionKit.framework/Modules/VSCollectionKit.swiftmodule/arm64-apple-ios.swiftmodule: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Build/iOS/VSCollectionKit.framework/Modules/VSCollectionKit.swiftmodule/arm64-apple-ios.swiftmodule -------------------------------------------------------------------------------- /NewsApp/Carthage/Build/iOS/VSCollectionKit.framework/Modules/VSCollectionKit.swiftmodule/arm64.swiftdoc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Build/iOS/VSCollectionKit.framework/Modules/VSCollectionKit.swiftmodule/arm64.swiftdoc -------------------------------------------------------------------------------- /NewsApp/Carthage/Build/iOS/VSCollectionKit.framework/Modules/VSCollectionKit.swiftmodule/arm64.swiftmodule: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Build/iOS/VSCollectionKit.framework/Modules/VSCollectionKit.swiftmodule/arm64.swiftmodule -------------------------------------------------------------------------------- /NewsApp/Carthage/Build/iOS/VSCollectionKit.framework/Modules/VSCollectionKit.swiftmodule/x86_64-apple-ios-simulator.swiftdoc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Build/iOS/VSCollectionKit.framework/Modules/VSCollectionKit.swiftmodule/x86_64-apple-ios-simulator.swiftdoc -------------------------------------------------------------------------------- /NewsApp/Carthage/Build/iOS/VSCollectionKit.framework/Modules/VSCollectionKit.swiftmodule/x86_64-apple-ios-simulator.swiftmodule: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Build/iOS/VSCollectionKit.framework/Modules/VSCollectionKit.swiftmodule/x86_64-apple-ios-simulator.swiftmodule -------------------------------------------------------------------------------- /NewsApp/Carthage/Build/iOS/VSCollectionKit.framework/Modules/VSCollectionKit.swiftmodule/x86_64.swiftdoc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Build/iOS/VSCollectionKit.framework/Modules/VSCollectionKit.swiftmodule/x86_64.swiftdoc -------------------------------------------------------------------------------- /NewsApp/Carthage/Build/iOS/VSCollectionKit.framework/Modules/VSCollectionKit.swiftmodule/x86_64.swiftmodule: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Build/iOS/VSCollectionKit.framework/Modules/VSCollectionKit.swiftmodule/x86_64.swiftmodule -------------------------------------------------------------------------------- /NewsApp/Carthage/Build/iOS/VSCollectionKit.framework/Modules/module.modulemap: -------------------------------------------------------------------------------- 1 | framework module VSCollectionKit { 2 | umbrella header "VSCollectionKit.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | 8 | module VSCollectionKit.Swift { 9 | header "VSCollectionKit-Swift.h" 10 | requires objc 11 | } 12 | -------------------------------------------------------------------------------- /NewsApp/Carthage/Build/iOS/VSCollectionKit.framework/VSCollectionKit: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Build/iOS/VSCollectionKit.framework/VSCollectionKit -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/Carthage/Build/iOS/VSCollectionKit.framework.dSYM/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | English 7 | CFBundleIdentifier 8 | com.apple.xcode.dsym.com.vswamy.vscollectionkit.VSCollectionKit 9 | CFBundleInfoDictionaryVersion 10 | 6.0 11 | CFBundlePackageType 12 | dSYM 13 | CFBundleSignature 14 | ???? 15 | CFBundleShortVersionString 16 | 1.0 17 | CFBundleVersion 18 | 1 19 | 20 | 21 | -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/Carthage/Build/iOS/VSCollectionKit.framework.dSYM/Contents/Resources/DWARF/VSCollectionKit: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/Carthage/Build/iOS/VSCollectionKit.framework.dSYM/Contents/Resources/DWARF/VSCollectionKit -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/Carthage/Build/iOS/VSCollectionKit.framework/Headers/VSCollectionKit.h: -------------------------------------------------------------------------------- 1 | // 2 | // VSCollectionKit.h 3 | // VSCollectionKit 4 | // 5 | // Created by Vinodh Govindaswamy on 07/04/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for VSCollectionKit. 12 | FOUNDATION_EXPORT double VSCollectionKitVersionNumber; 13 | 14 | //! Project version string for VSCollectionKit. 15 | FOUNDATION_EXPORT const unsigned char VSCollectionKitVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/Carthage/Build/iOS/VSCollectionKit.framework/Info.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/Carthage/Build/iOS/VSCollectionKit.framework/Info.plist -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/Carthage/Build/iOS/VSCollectionKit.framework/Modules/VSCollectionKit.swiftmodule/arm64-apple-ios.swiftdoc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/Carthage/Build/iOS/VSCollectionKit.framework/Modules/VSCollectionKit.swiftmodule/arm64-apple-ios.swiftdoc -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/Carthage/Build/iOS/VSCollectionKit.framework/Modules/VSCollectionKit.swiftmodule/arm64-apple-ios.swiftmodule: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/Carthage/Build/iOS/VSCollectionKit.framework/Modules/VSCollectionKit.swiftmodule/arm64-apple-ios.swiftmodule -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/Carthage/Build/iOS/VSCollectionKit.framework/Modules/VSCollectionKit.swiftmodule/arm64.swiftdoc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/Carthage/Build/iOS/VSCollectionKit.framework/Modules/VSCollectionKit.swiftmodule/arm64.swiftdoc -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/Carthage/Build/iOS/VSCollectionKit.framework/Modules/VSCollectionKit.swiftmodule/arm64.swiftmodule: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/Carthage/Build/iOS/VSCollectionKit.framework/Modules/VSCollectionKit.swiftmodule/arm64.swiftmodule -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/Carthage/Build/iOS/VSCollectionKit.framework/Modules/VSCollectionKit.swiftmodule/x86_64-apple-ios-simulator.swiftdoc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/Carthage/Build/iOS/VSCollectionKit.framework/Modules/VSCollectionKit.swiftmodule/x86_64-apple-ios-simulator.swiftdoc -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/Carthage/Build/iOS/VSCollectionKit.framework/Modules/VSCollectionKit.swiftmodule/x86_64-apple-ios-simulator.swiftmodule: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/Carthage/Build/iOS/VSCollectionKit.framework/Modules/VSCollectionKit.swiftmodule/x86_64-apple-ios-simulator.swiftmodule -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/Carthage/Build/iOS/VSCollectionKit.framework/Modules/VSCollectionKit.swiftmodule/x86_64.swiftdoc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/Carthage/Build/iOS/VSCollectionKit.framework/Modules/VSCollectionKit.swiftmodule/x86_64.swiftdoc -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/Carthage/Build/iOS/VSCollectionKit.framework/Modules/VSCollectionKit.swiftmodule/x86_64.swiftmodule: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/Carthage/Build/iOS/VSCollectionKit.framework/Modules/VSCollectionKit.swiftmodule/x86_64.swiftmodule -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/Carthage/Build/iOS/VSCollectionKit.framework/Modules/module.modulemap: -------------------------------------------------------------------------------- 1 | framework module VSCollectionKit { 2 | umbrella header "VSCollectionKit.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | 8 | module VSCollectionKit.Swift { 9 | header "VSCollectionKit-Swift.h" 10 | requires objc 11 | } 12 | -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/Carthage/Build/iOS/VSCollectionKit.framework/VSCollectionKit: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/Carthage/Build/iOS/VSCollectionKit.framework/VSCollectionKit -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/AlbumsCollectionController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlbumsCollectionController.swift 3 | // CollectionKitTestApp 4 | // 5 | // Created by Vinodh Govindaswamy on 19/04/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import VSCollectionKit 10 | 11 | enum AlbumSectionType: String { 12 | case photos 13 | } 14 | 15 | enum AlbumCellType: String { 16 | case photos 17 | } 18 | 19 | class AlbumsCollectionController: VSCollectionViewController { 20 | 21 | var viewModel: AlbumCollectionViewAPI? 22 | 23 | override func willAddSectionControllers() { 24 | super.willAddSectionControllers() 25 | let photSectionHandler = PhotosSectionHandler() 26 | sectionHandler.addSectionHandler(handler: photSectionHandler) 27 | } 28 | 29 | override func viewDidLoad() { 30 | super.viewDidLoad() 31 | 32 | viewModel?.fetchPhotos(callBack: { [weak self] (collectionData, errorString) in 33 | guard let self = self, 34 | let collectionData = collectionData else { return } 35 | self.apply(collectionData: collectionData, animated: true) 36 | }) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/AlbumsCollectionViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlbumsCollectionViewModel.swift 3 | // CollectionKitTestApp 4 | // 5 | // Created by Vinodh Govindaswamy on 19/04/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import VSCollectionKit 10 | 11 | protocol AlbumCollectionViewAPI { 12 | var collectionViewData: VSCollectionViewData? { get set } 13 | func fetchPhotos(callBack: @escaping CallBack) 14 | } 15 | 16 | typealias CallBack = (_ collectionData: VSCollectionViewData?, _ error: String?) -> Void 17 | 18 | // Main View Model 19 | class AlbumCollectionViewModel: AlbumCollectionViewAPI { 20 | 21 | let interactor: AlbumsInteractor 22 | init(interactor: AlbumsInteractor = AlbumsInteractor()) { 23 | self.interactor = interactor 24 | } 25 | 26 | var collectionViewData: VSCollectionViewData? 27 | 28 | func fetchPhotos(callBack: @escaping CallBack) { 29 | guard let urls = interactor.fetchAlbumUrls() else { return } 30 | let section = AlbumSectionModel(photoUrls: urls) 31 | var collectionData = VSCollectionViewData() 32 | collectionData.add(section: section) 33 | collectionViewData = collectionData 34 | callBack(collectionData, nil) 35 | } 36 | } 37 | 38 | struct AlbumSectionModel: SectionModel { 39 | var sectionType: String { 40 | return AlbumSectionType.photos.rawValue 41 | } 42 | 43 | var sectionID: String 44 | var header: HeaderViewModel? 45 | var items: [CellModel] = [] 46 | 47 | init(photoUrls: [String]) { 48 | self.sectionID = UUID().uuidString 49 | photoUrls.forEach { (url) in 50 | items.append(PhotoCellModel(photoUrl: url)) 51 | } 52 | } 53 | } 54 | 55 | struct PhotoCellModel: CellModel { 56 | var cellType: String { 57 | return AlbumCellType.photos.rawValue 58 | } 59 | 60 | let cellID: String 61 | let imageUrl: String 62 | init(photoUrl: String) { 63 | cellID = UUID().uuidString 64 | imageUrl = photoUrl 65 | } 66 | 67 | var photoURL: String { 68 | return "PhotoData/\(imageUrl)" 69 | } 70 | } 71 | 72 | class AlbumsInteractor { 73 | func fetchAlbumUrls() -> [String]? { 74 | 75 | let fileManager = FileManager.default 76 | guard let contents = try? fileManager.contentsOfDirectory(atPath: "\(Bundle.main.bundlePath)/PhotoData") else { return nil } 77 | return contents 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // CollectionKitTestApp 4 | // 5 | // Created by Vinodh Govindaswamy on 19/04/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | // MARK: UISceneSession Lifecycle 22 | 23 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 24 | // Called when a new scene session is being created. 25 | // Use this method to select a configuration to create the new scene with. 26 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 27 | } 28 | 29 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 30 | // Called when the user discards a scene session. 31 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 32 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 33 | } 34 | 35 | 36 | } 37 | 38 | -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/Assets.xcassets/first.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "first.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/Assets.xcassets/first.imageset/first.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/Assets.xcassets/first.imageset/first.pdf -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/Assets.xcassets/second.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "second.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/Assets.xcassets/second.imageset/second.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/Assets.xcassets/second.imageset/second.pdf -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/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 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | 37 | 38 | 39 | 40 | UILaunchStoryboardName 41 | LaunchScreen 42 | UIRequiredDeviceCapabilities 43 | 44 | armv7 45 | 46 | UIStatusBarTintParameters 47 | 48 | UINavigationBar 49 | 50 | Style 51 | UIBarStyleDefault 52 | Translucent 53 | 54 | 55 | 56 | UISupportedInterfaceOrientations 57 | 58 | UIInterfaceOrientationPortrait 59 | UIInterfaceOrientationLandscapeLeft 60 | UIInterfaceOrientationLandscapeRight 61 | 62 | UISupportedInterfaceOrientations~ipad 63 | 64 | UIInterfaceOrientationPortrait 65 | UIInterfaceOrientationPortraitUpsideDown 66 | UIInterfaceOrientationLandscapeLeft 67 | UIInterfaceOrientationLandscapeRight 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/PhotoData/DSCF6496.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/PhotoData/DSCF6496.jpg -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/PhotoData/DSCF6513.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/PhotoData/DSCF6513.jpg -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/PhotoData/DSCF6518.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/PhotoData/DSCF6518.jpg -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/PhotoData/DSCF6520.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/PhotoData/DSCF6520.jpg -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/PhotoData/DSCF6528.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/PhotoData/DSCF6528.jpg -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/PhotoData/DSCF6531.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/PhotoData/DSCF6531.jpg -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/PhotoData/DSCF6533.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/PhotoData/DSCF6533.jpg -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/PhotoData/DSCF6544.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/PhotoData/DSCF6544.jpg -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/PhotoData/DSCF6558.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/PhotoData/DSCF6558.jpg -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/PhotoData/DSCF6562.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/PhotoData/DSCF6562.jpg -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/PhotoData/DSCF6588.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/PhotoData/DSCF6588.jpg -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/PhotoData/DSCF6590.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/PhotoData/DSCF6590.jpg -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/PhotoData/DSCF6593.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/PhotoData/DSCF6593.jpg -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/PhotoData/DSCF6597.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/PhotoData/DSCF6597.jpg -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/PhotoData/DSCF6612.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/PhotoData/DSCF6612.jpg -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/PhotoData/DSCF6614.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/PhotoData/DSCF6614.jpg -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/PhotoData/DSCF6615.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/PhotoData/DSCF6615.jpg -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/PhotoData/DSCF6629.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/PhotoData/DSCF6629.jpg -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/PhotoData/DSCF6631.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/PhotoData/DSCF6631.jpg -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/PhotoData/DSCF6632.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/PhotoData/DSCF6632.jpg -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/PhotoTumbnailCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoTumbnailCell.swift 3 | // CollectionKitTestApp 4 | // 5 | // Created by Vinodh Govindaswamy on 19/04/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class PhotoTumbnailCell: UICollectionViewCell { 12 | 13 | static let resuseId: String = String(describing: PhotoTumbnailCell.self) 14 | 15 | lazy var imageView: UIImageView = { 16 | let imageV = UIImageView() 17 | imageV.translatesAutoresizingMaskIntoConstraints = false 18 | imageV.contentMode = .scaleAspectFill 19 | imageV.clipsToBounds = true 20 | return imageV 21 | }() 22 | 23 | var cellModel: PhotoCellModel? { 24 | didSet { 25 | guard let imageUrl = cellModel?.photoURL else { return } 26 | imageView.image = UIImage(named: imageUrl) 27 | } 28 | } 29 | 30 | override init(frame: CGRect) { 31 | super.init(frame: frame) 32 | setUpView() 33 | } 34 | 35 | required init?(coder: NSCoder) { 36 | super.init(coder: coder) 37 | setUpView() 38 | } 39 | 40 | private func setUpView() { 41 | contentView.addSubview(imageView) 42 | NSLayoutConstraint.activate([ 43 | imageView.topAnchor.constraint(equalTo: contentView.topAnchor), 44 | imageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), 45 | imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), 46 | imageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), 47 | ]) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/CollectionKitTestApp/PhotosSectionHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotosSectionHandler.swift 3 | // CollectionKitTestApp 4 | // 5 | // Created by Vinodh Govindaswamy on 19/04/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import VSCollectionKit 11 | 12 | class PhotosSectionHandler: SectionHandler { 13 | var type: String { 14 | return AlbumSectionType.photos.rawValue 15 | } 16 | 17 | func registerCells(for collectionView: UICollectionView) { 18 | collectionView.register(PhotoTumbnailCell.self, forCellWithReuseIdentifier: PhotoTumbnailCell.resuseId) 19 | } 20 | 21 | func cellProvider(_ collectionView: UICollectionView, _ indexPath: IndexPath, _ cellModel: CellModel) -> UICollectionViewCell { 22 | guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PhotoTumbnailCell.resuseId, 23 | for: indexPath) as? PhotoTumbnailCell, 24 | let photoCellModel = cellModel as? PhotoCellModel else { 25 | return UICollectionViewCell() 26 | } 27 | 28 | cell.cellModel = photoCellModel 29 | return cell 30 | } 31 | 32 | func sectionLayoutProvider(_ sectionModel: SectionModel, _ environment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? { 33 | let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.25), 34 | heightDimension: .fractionalHeight(1)) 35 | let itemLayout = NSCollectionLayoutItem(layoutSize: itemSize) 36 | itemLayout.contentInsets = NSDirectionalEdgeInsets(top: 2, leading: 2, bottom: 2, trailing: 2) 37 | let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), 38 | heightDimension: .fractionalWidth(0.25)) 39 | let groupLayout = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, 40 | subitems: [itemLayout]) 41 | let sectionLayout = NSCollectionLayoutSection(group: groupLayout) 42 | return sectionLayout 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/VSCollectionKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/VSCollectionKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/VSCollectionKit.xcodeproj/project.xcworkspace/xcuserdata/vkg0009.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/VSCollectionKit.xcodeproj/project.xcworkspace/xcuserdata/vkg0009.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/VSCollectionKit.xcodeproj/xcuserdata/vkg0009.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/VSCollectionKit/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 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/VSCollectionKit/VSCollectionKit.h: -------------------------------------------------------------------------------- 1 | // 2 | // VSCollectionKit.h 3 | // VSCollectionKit 4 | // 5 | // Created by Vinodh Govindaswamy on 07/04/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for VSCollectionKit. 12 | FOUNDATION_EXPORT double VSCollectionKitVersionNumber; 13 | 14 | //! Project version string for VSCollectionKit. 15 | FOUNDATION_EXPORT const unsigned char VSCollectionKitVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/VSCollectionKit/VSCollectionViewController/VSCollectionViewDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VNCollectionViewDelegate.swift 3 | // VSCollectionKit 4 | // 5 | // Created by Vinodh Govindaswamy on 07/04/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public class VSCollectionViewDelegate: NSObject, UICollectionViewDelegate { 12 | 13 | unowned private var collectionView: UICollectionView 14 | public var data: VSCollectionViewData? 15 | unowned private var sectionHandler: VSCollectionViewSectionHandller 16 | 17 | public init(collectionView: UICollectionView, 18 | sectionHandler: VSCollectionViewSectionHandller) { 19 | self.collectionView = collectionView 20 | self.sectionHandler = sectionHandler 21 | super.init() 22 | collectionView.delegate = self 23 | 24 | // TODO: Have to remove this 25 | collectionView.register(UICollectionReusableView.self, 26 | forSupplementaryViewOfKind: "section-header-element-kind", 27 | withReuseIdentifier: "EmptyView") 28 | sectionHandler.registerCells(for: collectionView) 29 | } 30 | 31 | public func collectionView(_ collectionView: UICollectionView, 32 | willDisplay cell: UICollectionViewCell, 33 | forItemAt indexPath: IndexPath) { 34 | guard let collectionData = data else { return } 35 | sectionHandler.willDisplayCell(collectionView: collectionView, 36 | indexPath: indexPath, 37 | cell: cell, 38 | sectionModel: collectionData.sections[indexPath.section]) 39 | } 40 | 41 | public func collectionView(_ collectionView: UICollectionView, 42 | didSelectItemAt indexPath: IndexPath) { 43 | guard let collectionData = data else { return } 44 | sectionHandler.didSelectItemAt(collectionView, 45 | indexPath: indexPath, 46 | sectionModel: collectionData.sections[indexPath.section]) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/VSCollectionKit/VSCollectionViewController/VSCollectionViewLayoutProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VNCollectionViewLayoutProvider.swift 3 | // VSCollectionKit 4 | // 5 | // Created by Vinodh Govindaswamy on 07/04/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public class VSCollectionViewLayoutProvider { 12 | 13 | unowned private var collectionView: UICollectionView 14 | unowned private var sectionHandler: VSCollectionViewSectionHandller 15 | public var data: VSCollectionViewData? 16 | 17 | public init(collectionView: UICollectionView, 18 | sectionHandler: VSCollectionViewSectionHandller) { 19 | self.collectionView = collectionView 20 | self.sectionHandler = sectionHandler 21 | } 22 | 23 | public func collectionLayout(for sectionIndex: Int, 24 | environment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? { 25 | guard let sectionModel = data?.sections[sectionIndex] else { return nil } 26 | return sectionHandler.collectionLayout(for: sectionModel, 27 | environment: environment) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/VSCollectionKit/VSCollectionViewController/VSCollectionViewUpdate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VNCollectionViewUpdate.swift 3 | // VSCollectionKit 4 | // 5 | // Created by Vinodh Govindaswamy on 07/04/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct VSCollectionViewUpdate { 12 | 13 | public enum UpdateType { 14 | case insert 15 | case delete 16 | case reload 17 | } 18 | 19 | public var updates: [Update] 20 | 21 | public init(updates: [Update]) { 22 | self.updates = updates 23 | } 24 | 25 | public struct Update { 26 | public let type: UpdateType 27 | public var updatedSections: IndexSet? = nil 28 | public var updatedRows: [IndexPath]? = nil 29 | 30 | public init(type: UpdateType, sections: IndexSet?, rows: [IndexPath]?) { 31 | self.type = type 32 | self.updatedSections = sections 33 | self.updatedRows = rows 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/VSCollectionKit/VSCollectionViewController/VSSectionHandlerProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VNSectionHandlerProtocol.swift 3 | // VSCollectionKit 4 | // 5 | // Created by Vinodh Govindaswamy on 07/04/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public protocol SectionDelegateHandler: AnyObject { 12 | func didSelect(_ collectionView: UICollectionView, 13 | _ indexPath: IndexPath, 14 | _ cellModel: CellModel) 15 | func willDisplayCell(_ collectionView: UICollectionView, 16 | _ indexPath: IndexPath, 17 | _ cell: UICollectionViewCell, 18 | _ cellModel: CellModel) 19 | } 20 | 21 | public protocol SectionHeaderFooter: AnyObject { 22 | func supplementaryViewProvider(_ collectionView: UICollectionView, 23 | _ kind: String, 24 | _ indexPath: IndexPath, 25 | _ headerViewModel: HeaderViewModel) -> UICollectionReusableView? 26 | } 27 | 28 | public protocol SectionLayoutInfo: AnyObject { 29 | func sectionLayoutProvider(_ sectionModel: SectionModel, 30 | _ environment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? 31 | } 32 | 33 | public protocol SectionHandler: SectionLayoutInfo, SectionHeaderFooter, SectionDelegateHandler { 34 | var type: String { get } 35 | func registerCells(for collectionView: UICollectionView) 36 | func cellProvider(_ collectionView: UICollectionView, 37 | _ indexPath: IndexPath, 38 | _ cellModel: CellModel) -> UICollectionViewCell 39 | } 40 | 41 | public extension SectionHeaderFooter { 42 | func supplementaryViewProvider(_ collectionView: UICollectionView, 43 | _ kind: String, 44 | _ indexPath: IndexPath, 45 | _ headerViewModel: HeaderViewModel) -> UICollectionReusableView? { 46 | return nil 47 | } 48 | } 49 | 50 | public extension SectionDelegateHandler { 51 | func didSelect(_ collectionView: UICollectionView, 52 | _ indexPath: IndexPath, 53 | _ cellModel: CellModel) {} 54 | func willDisplayCell(_ collectionView: UICollectionView, 55 | _ indexPath: IndexPath, 56 | _ cell: UICollectionViewCell, 57 | _ cellModel: CellModel) {} 58 | } 59 | 60 | public protocol SectionModel { 61 | var sectionType: String { get } 62 | var sectionID: String { get } 63 | var header: HeaderViewModel? { get } 64 | var items: [CellModel] { get set } 65 | } 66 | 67 | public protocol HeaderViewModel { 68 | var headerType: String { get } 69 | } 70 | 71 | public protocol CellModel { 72 | var cellType: String { get } 73 | var cellID: String { get } 74 | } 75 | -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/VSCollectionKitTests/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 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/VSCollectionKitTests/MockCellModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockCellModel.swift 3 | // VSCollectionKitTests 4 | // 5 | // Created by Vinodh Govindaswamy on 13/04/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | @testable import VSCollectionKit 10 | 11 | struct MockCellModel: CellModel { 12 | let cellType: String 13 | let info: String 14 | let cellID: String 15 | 16 | init(cellType: String, cellInfo: String) { 17 | self.cellType = cellType 18 | self.info = cellInfo 19 | cellID = UUID().uuidString 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/VSCollectionKitTests/MockSectionModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockSectionModel.swift 3 | // VSCollectionKitTests 4 | // 5 | // Created by Vinodh Govindaswamy on 13/04/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | @testable import VSCollectionKit 10 | 11 | struct MockSectionModel: SectionModel { 12 | var sectionType: String 13 | var header: HeaderViewModel? 14 | var items: [CellModel] = [] 15 | var sectionName: String 16 | var sectionID: String 17 | 18 | init(sectionType: String, sectionName: String) { 19 | self.sectionType = sectionType 20 | self.sectionName = sectionName 21 | sectionID = UUID().uuidString 22 | header = MockSectionHeader(headerType: sectionType) 23 | 24 | for index in 0..<20 { 25 | items.append(MockCellModel(cellType: "MockCell", cellInfo: "Cell \(index)")) 26 | } 27 | } 28 | } 29 | 30 | struct MockSectionHeader: HeaderViewModel { 31 | var headerType: String 32 | } 33 | -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/VSCollectionKitTests/VSCollectionKitTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VSCollectionKitTests.swift 3 | // VSCollectionKitTests 4 | // 5 | // Created by Vinodh Govindaswamy on 07/04/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import VSCollectionKit 11 | 12 | class VSCollectionKitTests: XCTestCase { 13 | 14 | override func setUp() { 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDown() { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | } 21 | 22 | func testExample() { 23 | // This is an example of a functional test case. 24 | // Use XCTAssert and related functions to verify your tests produce the correct results. 25 | } 26 | 27 | func testPerformanceExample() { 28 | // This is an example of a performance test case. 29 | self.measure { 30 | // Put the code you want to measure the time of here. 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/VSCollectionKitTests/VSCollectionViewDelegateTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VSCollectionViewDelegateTests.swift 3 | // VSCollectionKitTests 4 | // 5 | // Created by Vinodh Govindaswamy on 13/04/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import UIKit 11 | @testable import VSCollectionKit 12 | 13 | class VSCollectionViewDelegateTests: XCTestCase { 14 | 15 | let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) 16 | override func setUp() { 17 | // Put setup code here. This method is called before the invocation of each test method in the class. 18 | } 19 | 20 | override func tearDown() { 21 | // Put teardown code here. This method is called after the invocation of each test method in the class. 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | // Use XCTAssert and related functions to verify your tests produce the correct results. 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | private func vsDelegate() -> VSCollectionViewDelegate { 37 | let sectionHandler = VSCollectionViewSectionHandller() 38 | sectionHandler.addSectionHandler(handler: MockSectionHandler()) 39 | let delegate = VSCollectionViewDelegate(collectionView: collectionView, 40 | sectionHandler: sectionHandler) 41 | return delegate 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/VSCollectionKitTests/VSCollectionViewLayoutProviderTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VSCollectionViewLayoutProviderTests.swift 3 | // VSCollectionKitTests 4 | // 5 | // Created by Vinodh Govindaswamy on 17/04/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import UIKit 11 | @testable import VSCollectionKit 12 | 13 | class VSCollectionViewLayoutProviderTests: XCTestCase { 14 | 15 | var collectionView: UICollectionView! 16 | 17 | override func setUp() { 18 | let collectionViewLayout = UICollectionViewCompositionalLayout { (section, enivronment) -> NSCollectionLayoutSection? in 19 | return self.mockSectionHandler.collectionLayout(for: MockSectionModel(sectionType: "MockSection", 20 | sectionName: "Mock Seciton Name"), 21 | environment: MockLayoutEnvironment()) 22 | } 23 | 24 | collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) 25 | } 26 | 27 | override func tearDown() { 28 | // Put teardown code here. This method is called after the invocation of each test method in the class. 29 | } 30 | 31 | func testExample() { 32 | let layoutProvider = VSCollectionViewLayoutProvider(collectionView: collectionView, 33 | sectionHandler: mockSectionHandler) 34 | layoutProvider.data = mockCollectionViewData() 35 | XCTAssertNotNil(layoutProvider.collectionLayout(for: 0, 36 | environment: MockLayoutEnvironment())) 37 | } 38 | 39 | var mockSectionHandler: VSCollectionViewSectionHandller { 40 | let sectionHand = VSCollectionViewSectionHandller() 41 | sectionHand.addSectionHandler(handler: MockSectionHandler()) 42 | return sectionHand 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /NewsApp/Carthage/Checkouts/VSCollectionKit/VSCollectionKit/VSCollectionKitTests/VSCollectionViewSectionHandlerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VSCollectionViewSectionHandlerTests.swift 3 | // VSCollectionKitTests 4 | // 5 | // Created by Vinodh Govindaswamy on 17/04/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import UIKit 11 | @testable import VSCollectionKit 12 | 13 | class VSCollectionViewSectionHandlerTests: XCTestCase { 14 | 15 | var collectionView: UICollectionView! 16 | let mockSectionHandler = MockSectionHandler() 17 | override func setUp() { 18 | let collectionViewLayout = UICollectionViewCompositionalLayout { (section, enivronment) -> NSCollectionLayoutSection? in 19 | return self.mockSectionHandler.sectionLayoutProvider(MockSectionModel(sectionType: "MockSection", 20 | sectionName: "Mock Seciton Name"), MockLayoutEnvironment()) 21 | } 22 | 23 | collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) 24 | } 25 | 26 | override func tearDown() { 27 | // Put teardown code here. This method is called after the invocation of each test method in the class. 28 | } 29 | 30 | func testNumberRows() { 31 | let sectionHandler = vsSectionHandler() 32 | XCTAssertEqual(sectionHandler.numOfRows(for: MockSectionModel(sectionType: "MockSection", 33 | sectionName: "Mock Seciton Name"), sectionIndex: 0), 20) 34 | } 35 | 36 | func testCellType() { 37 | let sectionHandler = vsSectionHandler() 38 | let cell = sectionHandler.cell(for: collectionView, 39 | indexPath: IndexPath(item: 0, 40 | section: 0), 41 | sectionModel: MockSectionModel(sectionType: "MockSection", 42 | sectionName: "Mock Seciton Name")) 43 | XCTAssertNotNil(cell) 44 | XCTAssert(cell.isKind(of: MockCollectionViewCell.self)) 45 | } 46 | 47 | func testSectionLayoutInfo() { 48 | let sectionHandler = vsSectionHandler() 49 | let layoutInfo = sectionHandler.collectionLayout(for: MockSectionModel(sectionType: "MockSection", 50 | sectionName: "Mock Seciton Name"), 51 | environment: MockLayoutEnvironment()) 52 | XCTAssertNotNil(layoutInfo) 53 | } 54 | 55 | func vsSectionHandler() -> VSCollectionViewSectionHandller { 56 | let sectionHandler = VSCollectionViewSectionHandller() 57 | sectionHandler.addSectionHandler(handler: mockSectionHandler) 58 | sectionHandler.registerCells(for: collectionView) 59 | return sectionHandler 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /NewsApp/NewsApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /NewsApp/NewsApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /NewsApp/NewsApp.xcodeproj/project.xcworkspace/xcuserdata/vkg0009.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/NewsApp.xcodeproj/project.xcworkspace/xcuserdata/vkg0009.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /NewsApp/NewsApp.xcodeproj/xcshareddata/xcschemes/NewsDetailTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 14 | 15 | 17 | 23 | 24 | 25 | 27 | 33 | 34 | 35 | 36 | 37 | 47 | 48 | 54 | 55 | 57 | 58 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /NewsApp/NewsApp.xcodeproj/xcuserdata/vkg0009.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 9 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // NewsApp 4 | // 5 | // Created by Vinodh Govindaswamy on 07/04/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import Plugin 10 | import TopStoriesUI 11 | import NewsDetailUI 12 | import UIKit 13 | 14 | @UIApplicationMain 15 | class AppDelegate: UIResponder, UIApplicationDelegate { 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | PluginManager.shared.load(pluginTypes: [TopStoriesPlugin.self, NewsDetailPlugin.self]) 20 | 21 | return true 22 | } 23 | 24 | // MARK: UISceneSession Lifecycle 25 | 26 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 27 | // Called when a new scene session is being created. 28 | // Use this method to select a configuration to create the new scene with. 29 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 30 | } 31 | 32 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 33 | // Called when the user discards a scene session. 34 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 35 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 36 | } 37 | 38 | 39 | } 40 | 41 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /NewsApp/NewsApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /NewsApp/NewsApp/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Core/NewsShared/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /NewsApp/NewsApp/Core/NewsShared/Assets.xcassets/placeholder.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "placeholder-1.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "placeholder.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "placeholder-2.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /NewsApp/NewsApp/Core/NewsShared/Assets.xcassets/placeholder.imageset/placeholder-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/NewsApp/Core/NewsShared/Assets.xcassets/placeholder.imageset/placeholder-1.png -------------------------------------------------------------------------------- /NewsApp/NewsApp/Core/NewsShared/Assets.xcassets/placeholder.imageset/placeholder-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/NewsApp/Core/NewsShared/Assets.xcassets/placeholder.imageset/placeholder-2.png -------------------------------------------------------------------------------- /NewsApp/NewsApp/Core/NewsShared/Assets.xcassets/placeholder.imageset/placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinodh-G/NewsApp/1e23325c274dfa2b3b1cdc865edee8eec2b7bb01/NewsApp/NewsApp/Core/NewsShared/Assets.xcassets/placeholder.imageset/placeholder.png -------------------------------------------------------------------------------- /NewsApp/NewsApp/Core/NewsShared/Extentions/Date+ElapsedTime.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Date+ElapsedTime.swift 3 | // NewsShared 4 | // 5 | // Created by Vinodh Govindaswamy on 24/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public extension Date { 12 | func elapsedTimeString(fromDate: Date = Date()) -> String { 13 | 14 | let calendar = Calendar.current 15 | let interval = calendar.dateComponents([.year, 16 | .month, 17 | .weekOfMonth, 18 | .day, 19 | .hour, 20 | .minute], 21 | from: self, 22 | to: fromDate) 23 | 24 | if let year = interval.year, year > 0 { 25 | return "\(year) year\(year == 1 ? "": "s") ago" 26 | } else if let month = interval.month, month > 0 { 27 | return "\(month) month\(month == 1 ? "":"s") ago" 28 | } else if let day = interval.day, day > 0 { 29 | return "\(day) day\(day == 1 ? "":"s") ago" 30 | } else if let hour = interval.hour, hour > 0 { 31 | return "\(hour) hour\(hour == 1 ? "":"s") ago" 32 | } else if let minutes = interval.minute, minutes > 0 { 33 | return "\(minutes) minute\(minutes == 1 ? "":"s") ago" 34 | } 35 | return "Just now" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Core/NewsShared/Extentions/ImageDownloadManager.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | 4 | class ImageDownloadManager: NSObject { 5 | 6 | static let shared: ImageDownloadManager = ImageDownloadManager() 7 | 8 | private override init() {} 9 | 10 | var imageCache: NSCache = NSCache() 11 | lazy var downloadsSession: URLSession = URLSession(configuration: URLSessionConfiguration.default) 12 | 13 | func getImageFromURL(imageURLString:String, 14 | completionHandler:@escaping DownloadHandler) { 15 | 16 | if let cachedImage = imageCache.object(forKey: imageURLString as NSString) as UIImage? { 17 | completionHandler(cachedImage, nil) 18 | return 19 | } 20 | 21 | downloadImageFor(imageURLString: imageURLString, 22 | downloadHandler: completionHandler) 23 | } 24 | 25 | private func downloadImageFor(imageURLString: String, 26 | downloadHandler: @escaping DownloadHandler) { 27 | let imageLoaderTask = downloadsSession.dataTask(with: URL(string: imageURLString)!, completionHandler: { (data : Data?, response : URLResponse?, error : Error?) in 28 | 29 | DispatchQueue.main.async { 30 | guard let validData = data else { 31 | downloadHandler(nil, error) 32 | return; 33 | } 34 | 35 | guard let image = UIImage(data: validData) else { 36 | downloadHandler(nil, error) 37 | return; 38 | } 39 | 40 | self.imageCache.setObject(image, forKey: imageURLString as NSString) 41 | downloadHandler(image, nil) 42 | } 43 | }) 44 | 45 | imageLoaderTask.resume() 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Core/NewsShared/Extentions/UIImageView+AsyncLoad.swift: -------------------------------------------------------------------------------- 1 | 2 | import UIKit 3 | 4 | public protocol AsyncLoad { 5 | func setImageFrom(imageURLString: String, 6 | placeHolderImage: UIImage?, 7 | completionHandler: DownloadHandler?) 8 | } 9 | 10 | public typealias DownloadHandler = (_ image: UIImage?, _ error: Error?) -> Void 11 | 12 | private var kImageURLKey: String = "imageURLKey" 13 | 14 | extension UIImageView: AsyncLoad { 15 | 16 | var imageURLId: String{ 17 | 18 | get{ 19 | return objc_getAssociatedObject(self, &kImageURLKey) as! String 20 | } 21 | set(newValue){ 22 | objc_setAssociatedObject(self, &kImageURLKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 23 | } 24 | } 25 | 26 | public func setImageFrom(imageURLString: String, 27 | placeHolderImage: UIImage? = nil, 28 | completionHandler: DownloadHandler? = nil) { 29 | 30 | guard !imageURLString.isEmpty else { 31 | if let handler = completionHandler { 32 | handler(nil, nil) 33 | } 34 | return 35 | } 36 | 37 | if placeHolderImage != nil { 38 | image = placeHolderImage; 39 | } 40 | 41 | imageURLId = imageURLString 42 | ImageDownloadManager.shared.getImageFromURL(imageURLString: imageURLString) { (image: UIImage?, error: Error?) in 43 | 44 | guard let image = image else { 45 | if let handler = completionHandler { 46 | handler(nil, error) 47 | } 48 | return 49 | } 50 | 51 | self.updateImage(image: image, imageUrl: imageURLString) 52 | if let handler = completionHandler { 53 | handler(image, nil); 54 | } 55 | } 56 | } 57 | 58 | private func updateImage(image: UIImage, imageUrl: String) { 59 | 60 | if imageUrl == imageURLId { 61 | UIView.transition(with: self, 62 | duration: 0.2, 63 | options: .transitionCrossDissolve, 64 | animations: { 65 | self.image = image; 66 | }, 67 | completion: nil) 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Core/NewsShared/Extentions/UIView+AutoLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+AutoLayout.swift 3 | // NewsShared 4 | // 5 | // Created by Vinodh Govindaswamy on 24/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public extension UIView { 12 | convenience init(autoLayout: Bool) { 13 | self.init() 14 | translatesAutoresizingMaskIntoConstraints = !autoLayout 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Core/NewsShared/Extentions/UIView+Shadow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+Shadow.swift 3 | // NewsShared 4 | // 5 | // Created by Vinodh Govindaswamy on 28/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public extension UIView { 12 | func addShadow(shadowColor: UIColor = UIColor.black, 13 | shadowOpacity: CGFloat = 0.5, 14 | shadowRadius: CGFloat = 1, 15 | shadowOffset: CGSize = .zero, 16 | cornerRadius: CGFloat) { 17 | 18 | let layer = self.layer 19 | layer.shadowOffset = shadowOffset 20 | layer.shadowOpacity = Float(shadowOpacity) 21 | layer.shadowRadius = shadowRadius 22 | layer.shadowColor = shadowColor.cgColor 23 | layer.cornerRadius = cornerRadius 24 | layer.shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius).cgPath 25 | layer.shouldRasterize = true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Core/NewsShared/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 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Core/NewsShared/NewsImageSize.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsImageSize.swift 3 | // NewsShared 4 | // 5 | // Created by Vinodh Govindaswamy on 27/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum NewsImageSize: String { 12 | case small = "Normal" 13 | case medium = "mediumThreeByTwo210" 14 | case large = "superJumbo" 15 | } 16 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Core/NewsShared/NewsShared.h: -------------------------------------------------------------------------------- 1 | // 2 | // NewsShared.h 3 | // NewsShared 4 | // 5 | // Created by Vinodh Govindaswamy on 07/04/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for NewsShared. 12 | FOUNDATION_EXPORT double NewsSharedVersionNumber; 13 | 14 | //! Project version string for NewsShared. 15 | FOUNDATION_EXPORT const unsigned char NewsSharedVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Core/NewsShared/NewsTestUtil.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsTestUtil.swift 3 | // NewsServiceTests 4 | // 5 | // Created by Vinodh Govindaswamy on 24/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum NewsXCTestError: Error { 12 | case failedToLoad 13 | } 14 | 15 | public class TestUtil { 16 | public func get (from filePath : String, 17 | in bundle: Bundle) throws -> T { 18 | 19 | if let jsonData = data(from: filePath, 20 | in: bundle) { 21 | do { 22 | let decoder = JSONDecoder() 23 | decoder.dateDecodingStrategy = .iso8601 24 | let val = try decoder.decode(T.self, 25 | from: jsonData) 26 | return val 27 | } catch { 28 | throw NewsXCTestError.failedToLoad 29 | } 30 | } else { 31 | throw NewsXCTestError.failedToLoad 32 | } 33 | } 34 | 35 | public func data(from filePath: String, 36 | in bundle: Bundle) -> Data? { 37 | 38 | guard let validFullPath = bundle.path(forResource: filePath, 39 | ofType: nil), 40 | FileManager.default.fileExists(atPath: validFullPath), 41 | let rawFileData = try? Data(contentsOf: URL(fileURLWithPath: validFullPath)) else { 42 | return nil 43 | } 44 | return rawFileData 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Core/NewsShared/UIConfig/AppColor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppColor.swift 3 | // NewsShared 4 | // 5 | // Created by Vinodh Govindaswamy on 28/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public class AppColor { 12 | 13 | public static let defaultBackgroundColor: UIColor = UIColor.init(red: 245.0/255, 14 | green: 245.0/255, 15 | blue: 245.0/255, alpha: 1) 16 | 17 | public static let defaultTitleTextColor: UIColor = UIColor.init(white: 0, alpha: 0.9) 18 | public static let defaultContentTextColor: UIColor = UIColor.init(white: 0, alpha: 0.7) 19 | public static let defaulttimeStampTextColor: UIColor = UIColor.init(white: 0, alpha: 0.7) 20 | 21 | public static let highlightTitleTextColor: UIColor = UIColor.init(white: 1, alpha: 1) 22 | public static let highlighttimeStampTextColor: UIColor = UIColor.init(white: 0, alpha: 0.7) 23 | 24 | public static let defaultShadowColor: UIColor = UIColor.black 25 | 26 | public struct ErrorCell { 27 | public static let errorTitleColor: UIColor = .darkGray 28 | public static let actionButtonTint: UIColor = UIColor.init(white: 0, alpha: 0.7) 29 | public static let actionButtonBorder: UIColor = UIColor.init(white: 0, alpha: 0.3) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Core/NewsShared/UIConfig/AppSpacing.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppSpacing.swift 3 | // NewsShared 4 | // 5 | // Created by Vinodh Govindaswamy on 28/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public struct AppSpacing { 12 | 13 | public static let defaultSpacing: CGFloat = 20 14 | 15 | public struct FullWidthCell { 16 | 17 | public static let itemWidth: NSCollectionLayoutDimension = .fractionalWidth(1.0) 18 | public static let itemHeight: NSCollectionLayoutDimension = .fractionalHeight(1.0) 19 | 20 | public static let groupWidth: NSCollectionLayoutDimension = .fractionalWidth(0.95) 21 | public static let groupHeight: NSCollectionLayoutDimension = .absolute(220) 22 | public static let contentInset = NSDirectionalEdgeInsets(top: 0, leading: 0, 23 | bottom: 0, trailing: 8) 24 | } 25 | 26 | public struct CardCell { 27 | 28 | public static let itemWidth: NSCollectionLayoutDimension = .fractionalWidth(1.0) 29 | public static let itemHeight: NSCollectionLayoutDimension = .fractionalHeight(1.0) 30 | 31 | public static let groupWidth: NSCollectionLayoutDimension = .absolute(260) 32 | public static let groupHeight: NSCollectionLayoutDimension = .absolute(230) 33 | public static let contentInset = NSDirectionalEdgeInsets(top: 0, leading: 16, 34 | bottom: 0, trailing: 0) 35 | } 36 | 37 | public struct ListCell { 38 | 39 | public static let itemWidth: NSCollectionLayoutDimension = .fractionalWidth(1.0) 40 | public static let itemHeight: NSCollectionLayoutDimension = .fractionalHeight(1.0) 41 | 42 | public static let groupWidth: NSCollectionLayoutDimension = .fractionalWidth(0.86) 43 | public static let groupHeight: NSCollectionLayoutDimension = .absolute(130) 44 | public static let contentInset = NSDirectionalEdgeInsets(top: 0, leading: 0, 45 | bottom: 0, trailing: 0) 46 | } 47 | 48 | public struct SectionHeader { 49 | 50 | public static let headerWidth: NSCollectionLayoutDimension = .fractionalWidth(1.0) 51 | public static let headerHeight: NSCollectionLayoutDimension = .estimated(30) 52 | } 53 | 54 | public static let defaultSectionContentInset = NSDirectionalEdgeInsets(top: 0, 55 | leading: 12, 56 | bottom: 0, 57 | trailing: 0) 58 | } 59 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Core/Plugin/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 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Core/Plugin/NewsDetailUIAPI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsDetailUIAPI.swift 3 | // Plugin 4 | // 5 | // Created by Vinodh Govindaswamy on 27/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public let NewsDetailsUIPluginId: String = "NewsDetailsUIPluginId" 12 | 13 | public struct NewsDetailsPageParam { 14 | public let pageId: String 15 | public let data: Any 16 | public init (pageId: String, data: Any) { 17 | self.pageId = pageId 18 | self.data = data 19 | } 20 | } 21 | 22 | public protocol NewsDetailsUIAPI { 23 | func newsDetailsViewController(param: NewsDetailsPageParam) -> UIViewController? 24 | } 25 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Core/Plugin/Plugin.h: -------------------------------------------------------------------------------- 1 | // 2 | // Plugin.h 3 | // Plugin 4 | // 5 | // Created by Vinodh Govindaswamy on 07/04/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for Plugin. 12 | FOUNDATION_EXPORT double PluginVersionNumber; 13 | 14 | //! Project version string for Plugin. 15 | FOUNDATION_EXPORT const unsigned char PluginVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Core/Plugin/PluginManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PluginManager.swift 3 | // Plugin 4 | // 5 | // Created by Vinodh Govindaswamy on 24/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol Plugable { 12 | init() 13 | var pluginId: String { get } 14 | func plug() -> AnyObject? 15 | } 16 | 17 | public protocol PluginAPI { 18 | func load(pluginTypes: [Plugable.Type]) 19 | func plugin(for pluginId: String) -> Plugable? 20 | } 21 | 22 | public class PluginManager: PluginAPI { 23 | 24 | static public let shared: PluginManager = PluginManager() 25 | private var plugins: [String: Plugable] = [:] 26 | 27 | public func load(pluginTypes: [Plugable.Type]) { 28 | 29 | pluginTypes.forEach { (pluginType) in 30 | let plugin = pluginType.init() 31 | plugins[plugin.pluginId] = plugin 32 | } 33 | } 34 | 35 | public func plugin(for pluginId: String) -> Plugable? { 36 | return plugins[pluginId] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Core/Plugin/TopStoriesUIAPI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TopStoriesUIAPI.swift 3 | // Plugin 4 | // 5 | // Created by Vinodh Govindaswamy on 24/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | public let TopStoriesUIPluginId: String = "TopStoriesUIPlugin" 12 | 13 | public protocol TopStoriesUIAPI { 14 | func topStoriesViewController(pageId: String) -> UIViewController? 15 | } 16 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/NewsDetailUI/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 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/NewsDetailUI/NewsDetailTests/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 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/NewsDetailUI/NewsDetailTests/MockNewsItem.json: -------------------------------------------------------------------------------- 1 | {"section":"world","subsection":"","title":"Coronavirus Live Updates: $2 Trillion Aid Bill Becomes Law as U.S. Cases Reach 100,000","abstract":"President Trump said the government would buy thousands of ventilators, but it seemed doubtful they could be produced in time to help overwhelmed hospitals.","url":"https://www.nytimes.com/2020/03/28/world/coronavirus-live-news-updates.html","uri":"nyt://article/66bac4ca-d12f-50b5-aafe-cc379640d61c","byline":"","item_type":"Article","updated_date":"2020-03-28T04:48:37-04:00","created_date":"2020-03-28T00:01:51-04:00","published_date":"2020-03-28T00:01:51-04:00","material_type_facet":"","kicker":"","des_facet":["Coronavirus (2019-nCoV)"],"org_facet":[],"per_facet":[],"geo_facet":[],"multimedia":[{"url":"https://static01.nyt.com/images/2020/03/03/world/coronavirus-map-promo/coronavirus-map-promo-superJumbo-v195.png","format":"superJumbo","height":1366,"width":2048,"type":"image","subtype":"photo","caption":"","copyright":"The New York Times"},{"url":"https://static01.nyt.com/images/2020/03/03/world/coronavirus-map-promo/coronavirus-map-promo-thumbStandard-v201.png","format":"Standard Thumbnail","height":75,"width":75,"type":"image","subtype":"photo","caption":"","copyright":"The New York Times"},{"url":"https://static01.nyt.com/images/2020/03/03/world/coronavirus-map-promo/coronavirus-map-promo-thumbLarge-v201.png","format":"thumbLarge","height":150,"width":150,"type":"image","subtype":"photo","caption":"","copyright":"The New York Times"},{"url":"https://static01.nyt.com/images/2020/03/03/world/coronavirus-map-promo/coronavirus-map-promo-mediumThreeByTwo210-v201.png","format":"mediumThreeByTwo210","height":140,"width":210,"type":"image","subtype":"photo","caption":"","copyright":"The New York Times"},{"url":"https://static01.nyt.com/images/2020/03/03/world/coronavirus-map-promo/coronavirus-map-promo-articleInline-v195.png","format":"Normal","height":127,"width":190,"type":"image","subtype":"photo","caption":"","copyright":"The New York Times"}],"short_url":"https://nyti.ms/2WLzQpP"} 2 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/NewsDetailUI/NewsDetailTests/NewsDetailAPIPrivateTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsDetailAPIPrivateTests.swift 3 | // NewsDetailsTests 4 | // 5 | // Created by Vinodh Govindaswamy on 28/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Plugin 11 | @testable import NewsDetailUI 12 | 13 | 14 | class NewsDetailAPIPrivateTests: XCTestCase { 15 | 16 | override func setUp() { 17 | // Put setup code here. This method is called before the invocation of each test method in the class. 18 | } 19 | 20 | override func tearDown() { 21 | // Put teardown code here. This method is called after the invocation of each test method in the class. 22 | } 23 | 24 | func testNewsDetailsViewControllerClass() { 25 | let pluginPrivate = NewsDetailAPIPrivate() 26 | guard let newsData = mockNewsItem() else { return } 27 | let param = NewsDetailsPageParam(pageId: "23123321", data: newsData) 28 | let newsDetailsViewController = pluginPrivate.newsDetailsViewController(param: param) 29 | 30 | XCTAssert(newsDetailsViewController is NewsDetailViewController) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/NewsDetailUI/NewsDetailTests/NewsDetailsSectionTests/NewsDetailsCellModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsDetailsCellModelTests.swift 3 | // NewsDetailsTests 4 | // 5 | // Created by Vinodh Govindaswamy on 28/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import NewsDetailUI 11 | @testable import NewsShared 12 | @testable import NewsService 13 | 14 | class NewsDetailsCellModelTests: XCTestCase { 15 | 16 | override func setUp() { 17 | // Put setup code here. This method is called before the invocation of each test method in the class. 18 | } 19 | 20 | override func tearDown() { 21 | // Put teardown code here. This method is called after the invocation of each test method in the class. 22 | } 23 | 24 | 25 | func testCellModelType() { 26 | let cellModel = newsDetailsCellModel() 27 | XCTAssertEqual(cellModel.cellType, NewsDetailsCellType.newsImage.rawValue) 28 | } 29 | 30 | func testTitle() { 31 | let cellModel = newsDetailsCellModel() 32 | XCTAssertEqual(cellModel.newsDetailsText, "Test details") 33 | } 34 | func newsDetailsCellModel() -> NewsDetailsCellModel { 35 | return NewsDetailsCellModel(details: "Test details") 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/NewsDetailUI/NewsDetailTests/NewsDetailsSectionTests/NewsDetailsSectionHandlerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsDetailsSectionHandlerTests.swift 3 | // NewsDetailsTests 4 | // 5 | // Created by Vinodh Govindaswamy on 28/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import NewsDetailUI 11 | @testable import NewsShared 12 | 13 | class NewsDetailsSectionHandlerTests: XCTestCase { 14 | 15 | let sectionHandler: NewsDetailSectionHandler = NewsDetailSectionHandler() 16 | let collectionView: UICollectionView = UICollectionView(frame: .zero, 17 | collectionViewLayout: UICollectionViewFlowLayout()) 18 | 19 | override func setUp() { 20 | sectionHandler.registerCells(for: collectionView) 21 | } 22 | 23 | func testSectionHandlerType() { 24 | XCTAssertEqual(sectionHandler.type, NewsDetailsSectionType.newsDetails.rawValue) 25 | } 26 | 27 | func testSectionHandlerCell() { 28 | let cell = sectionHandler.cellProvider(collectionView, 29 | IndexPath(item: 0, section: 0), 30 | NewsDetailsCellModel(details: "Test details")) 31 | 32 | XCTAssert(cell.isKind(of: NewsDetailsCell.self), "Cell should be of type NewsDetailsCell") 33 | } 34 | 35 | func testSectionHandlerLayoutNotNil() { 36 | let layout = sectionHandler.sectionLayoutProvider(NewsDetailsSectionModel(details: "Test details"), 37 | MockLayoutEnvironment()) 38 | XCTAssertNotNil(layout) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/NewsDetailUI/NewsDetailTests/NewsDetailsSectionTests/NewsDetailsSectionModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsDetailsSectionModelTests.swift 3 | // NewsDetailsTests 4 | // 5 | // Created by Vinodh Govindaswamy on 28/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import NewsDetailUI 11 | @testable import NewsShared 12 | @testable import NewsService 13 | 14 | class NewsDetailsSectionModelTests: XCTestCase { 15 | 16 | override func setUp() { 17 | // Put setup code here. This method is called before the invocation of each test method in the class. 18 | } 19 | 20 | override func tearDown() { 21 | // Put teardown code here. This method is called after the invocation of each test method in the class. 22 | } 23 | 24 | func testSectionType() { 25 | let sectionModel = newsDetailsSectionModel() 26 | XCTAssertEqual(sectionModel.sectionType, NewsDetailsSectionType.newsDetails.rawValue) 27 | } 28 | 29 | func testCellModelCount() { 30 | let sectionModel = newsDetailsSectionModel() 31 | XCTAssertEqual(sectionModel.items.count, 1) 32 | } 33 | 34 | func testCellModelType() { 35 | let sectionModel = newsDetailsSectionModel() 36 | XCTAssert(sectionModel.items[0] is NewsDetailsCellModel) 37 | } 38 | 39 | func newsDetailsSectionModel() -> NewsDetailsSectionModel { 40 | return NewsDetailsSectionModel(details: "President Trump said the government would buy thousands of ventilators, but it seemed doubtful they could be produced in time to help overwhelmed hospitals.") 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/NewsDetailUI/NewsDetailTests/NewsDetailsViewModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsDetailsViewModelTests.swift 3 | // NewsServiceTests 4 | // 5 | // Created by Vinodh Govindaswamy on 28/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import NewsDetailUI 11 | @testable import NewsShared 12 | @testable import NewsService 13 | 14 | class NewsDetailsViewModelTests: XCTestCase { 15 | 16 | override func setUp() { 17 | // Put setup code here. This method is called before the invocation of each test method in the class. 18 | } 19 | 20 | override func tearDown() { 21 | // Put teardown code here. This method is called after the invocation of each test method in the class. 22 | } 23 | 24 | func testCollectionDataSections() { 25 | let viewModel = newsDetailsViewModel() 26 | XCTAssertNotNil(viewModel?.collectionViewData) 27 | } 28 | 29 | func testCollectionDataSectionsCount() { 30 | let viewModel = newsDetailsViewModel() 31 | guard let data = viewModel?.collectionViewData else { 32 | XCTAssertTrue(false, "Should have a valid conllectiondata") 33 | return 34 | } 35 | 36 | XCTAssertEqual(data.sections.count, 2) 37 | } 38 | 39 | func testCollectionDataSectionsNewsImageSectionType() { 40 | let viewModel = newsDetailsViewModel() 41 | guard let data = viewModel?.collectionViewData else { 42 | XCTAssertTrue(false, "Should have a valid conllectiondata") 43 | return 44 | } 45 | 46 | XCTAssert(data.sections[0] is NewsImageSectionModel) 47 | } 48 | 49 | func testCollectionDataSectionsNewsDetailSectionType() { 50 | let viewModel = newsDetailsViewModel() 51 | guard let data = viewModel?.collectionViewData else { 52 | XCTAssertTrue(false, "Should have a valid conllectiondata") 53 | return 54 | } 55 | 56 | XCTAssert(data.sections[1] is NewsDetailsSectionModel) 57 | } 58 | 59 | func newsDetailsViewModel() -> NewsDetailsViewModel? { 60 | guard let newsItem = mockNewsItem() else { return nil } 61 | return NewsDetailsViewModel(newsItem: newsItem) 62 | } 63 | } 64 | 65 | extension XCTestCase { 66 | 67 | func mockNewsItem(filePath: String = "MockNewsItem.json") -> NewsPageResponse.NewsItem? { 68 | do { 69 | let newsItem: NewsPageResponse.NewsItem = try TestUtil().get(from: filePath, 70 | in: Bundle(for: NewsDetailsViewModelTests.self)) 71 | return newsItem 72 | } catch { 73 | return nil 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/NewsDetailUI/NewsDetailTests/NewsImageSectionTests/NewsImageCellModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsImageCellModelTests.swift 3 | // NewsDetailsTests 4 | // 5 | // Created by Vinodh Govindaswamy on 28/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import NewsDetailUI 11 | @testable import NewsShared 12 | @testable import NewsService 13 | 14 | class NewsImageCellModelTests: XCTestCase { 15 | 16 | override func setUp() { 17 | // Put setup code here. This method is called before the invocation of each test method in the class. 18 | } 19 | 20 | override func tearDown() { 21 | // Put teardown code here. This method is called after the invocation of each test method in the class. 22 | } 23 | 24 | func testCellModelType() { 25 | guard let cellModel = newsImageCellModel() else { 26 | XCTAssertTrue(false, "Should have valid Cell Model") 27 | return 28 | } 29 | 30 | XCTAssertEqual(cellModel.cellType, NewsDetailsCellType.newsImage.rawValue) 31 | } 32 | 33 | func testTitle() { 34 | guard let cellModel = newsImageCellModel() else { 35 | XCTAssertTrue(false, "Should have valid Cell Model") 36 | return 37 | } 38 | 39 | XCTAssertEqual(cellModel.title, "Coronavirus Live Updates: $2 Trillion Aid Bill Becomes Law as U.S. Cases Reach 100,000") 40 | } 41 | 42 | func testTimestamp() { 43 | guard let cellModel = newsImageCellModel() else { 44 | XCTAssertTrue(false, "Should have valid Cell Model") 45 | return 46 | } 47 | 48 | XCTAssertNotNil(cellModel.timestamp) 49 | } 50 | 51 | func testImageUrl() { 52 | guard let cellModel = newsImageCellModel() else { 53 | XCTAssertTrue(false, "Should have valid Cell Model") 54 | return 55 | } 56 | 57 | XCTAssertEqual(cellModel.imageUrl, "https://static01.nyt.com/images/2020/03/03/world/coronavirus-map-promo/coronavirus-map-promo-superJumbo-v195.png") 58 | } 59 | 60 | func testCachedImageUrl() { 61 | guard let cellModel = newsImageCellModel() else { 62 | XCTAssertTrue(false, "Should have valid Cell Model") 63 | return 64 | } 65 | 66 | XCTAssertEqual(cellModel.cachedImageUrl, "https://static01.nyt.com/images/2020/03/03/world/coronavirus-map-promo/coronavirus-map-promo-mediumThreeByTwo210-v201.png") 67 | } 68 | } 69 | 70 | extension XCTestCase { 71 | func newsImageCellModel() -> NewsImageCellModel? { 72 | guard let newsItem = mockNewsItem() else { return nil} 73 | return NewsImageCellModel(item: newsItem) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/NewsDetailUI/NewsDetailTests/NewsImageSectionTests/NewsImageSectionHandlerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsImageSectionHandlerTests.swift 3 | // NewsDetailsTests 4 | // 5 | // Created by Vinodh Govindaswamy on 28/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import NewsDetailUI 11 | @testable import NewsShared 12 | 13 | class NewsImageSectionHandlerTests: XCTestCase { 14 | 15 | let sectionHandler: NewsImageSectionHandler = NewsImageSectionHandler() 16 | let collectionView: UICollectionView = UICollectionView(frame: .zero, 17 | collectionViewLayout: UICollectionViewFlowLayout()) 18 | 19 | override func setUp() { 20 | sectionHandler.registerCells(for: collectionView) 21 | } 22 | 23 | func testSectionHandlerType() { 24 | XCTAssertEqual(sectionHandler.type, NewsDetailsSectionType.newsImage.rawValue) 25 | } 26 | 27 | func testSectionHandlerCell() { 28 | let cell = sectionHandler.cellProvider(collectionView, 29 | IndexPath(item: 0, section: 0), 30 | newsImageCellModel()!) 31 | 32 | XCTAssert(cell.isKind(of: NewsImageCell.self), "Cell should be of type NewsImageCell") 33 | } 34 | 35 | func testSectionHandlerLayoutNotNil() { 36 | let layout = sectionHandler.sectionLayoutProvider(newsImageSectionModel()!, 37 | MockLayoutEnvironment()) 38 | XCTAssertNotNil(layout) 39 | } 40 | } 41 | 42 | class MockLayoutEnvironment: NSObject, NSCollectionLayoutEnvironment { 43 | var container: NSCollectionLayoutContainer = MockLayoutContainer() 44 | var traitCollection: UITraitCollection = .current 45 | 46 | class MockLayoutContainer: NSObject, NSCollectionLayoutContainer { 47 | var contentSize: CGSize = .zero 48 | var effectiveContentSize: CGSize = .zero 49 | var contentInsets: NSDirectionalEdgeInsets = .zero 50 | var effectiveContentInsets: NSDirectionalEdgeInsets = .zero 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/NewsDetailUI/NewsDetailTests/NewsImageSectionTests/NewsImageSectionModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsImageSectionModelTests.swift 3 | // NewsDetailsTests 4 | // 5 | // Created by Vinodh Govindaswamy on 28/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import NewsDetailUI 11 | @testable import NewsShared 12 | @testable import NewsService 13 | 14 | class NewsImageSectionModelTests: XCTestCase { 15 | 16 | override func setUp() { 17 | // Put setup code here. This method is called before the invocation of each test method in the class. 18 | } 19 | 20 | override func tearDown() { 21 | // Put teardown code here. This method is called after the invocation of each test method in the class. 22 | } 23 | 24 | func testSectionType() { 25 | guard let sectionModel = newsImageSectionModel() else { 26 | XCTAssertTrue(false, "Should have valid section model") 27 | return 28 | } 29 | 30 | XCTAssertEqual(sectionModel.sectionType, NewsDetailsSectionType.newsImage.rawValue) 31 | } 32 | 33 | func testCellModelCount() { 34 | guard let sectionModel = newsImageSectionModel() else { 35 | XCTAssertTrue(false, "Should have valid section model") 36 | return 37 | } 38 | 39 | XCTAssertEqual(sectionModel.items.count, 1) 40 | } 41 | 42 | func testCellModelType() { 43 | guard let sectionModel = newsImageSectionModel() else { 44 | XCTAssertTrue(false, "Should have valid section model") 45 | return 46 | } 47 | 48 | XCTAssert(sectionModel.items[0] is NewsImageCellModel) 49 | } 50 | } 51 | 52 | extension XCTestCase { 53 | func newsImageSectionModel() -> NewsImageSectionModel? { 54 | guard let newsItem = mockNewsItem() else { 55 | return nil 56 | } 57 | 58 | return NewsImageSectionModel(item: newsItem) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/NewsDetailUI/NewsDetailUI.h: -------------------------------------------------------------------------------- 1 | // 2 | // NewsDetailUI.h 3 | // NewsDetailUI 4 | // 5 | // Created by Vinodh Govindaswamy on 12/04/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for NewsDetailUI. 12 | FOUNDATION_EXPORT double NewsDetailUIVersionNumber; 13 | 14 | //! Project version string for NewsDetailUI. 15 | FOUNDATION_EXPORT const unsigned char NewsDetailUIVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/NewsDetailUI/NewsDetailViewController/NewsDetailViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsDetailViewController.swift 3 | // NewsDetailsUI 4 | // 5 | // Created by Vinodh Govindaswamy on 27/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import NewsShared 10 | import UIKit 11 | import VSCollectionKit 12 | 13 | enum NewsDetailsSectionType: String { 14 | case newsImage 15 | case newsDetails 16 | } 17 | 18 | enum NewsDetailsCellType: String { 19 | case newsImage 20 | case newsDetails 21 | } 22 | 23 | class NewsDetailViewController: VSCollectionViewController { 24 | 25 | var viewModel: NewsDetailsViewAPI? 26 | 27 | override func willAddSectionControllers() { 28 | super.willAddSectionControllers() 29 | sectionHandler.addSectionHandler(handler: NewsImageSectionHandler()) 30 | sectionHandler.addSectionHandler(handler: NewsDetailSectionHandler()) 31 | } 32 | 33 | override func viewDidLoad() { 34 | super.viewDidLoad() 35 | 36 | guard let collectionData = viewModel?.collectionViewData else { return } 37 | apply(collectionData: collectionData, animated: true) 38 | } 39 | 40 | override func configureCollectionView() { 41 | super.configureCollectionView() 42 | collectionView.backgroundColor = .white 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/NewsDetailUI/NewsDetailViewController/NewsDetailViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsDetailViewModel.swift 3 | // NewsDetailsUI 4 | // 5 | // Created by Vinodh Govindaswamy on 27/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import NewsService 11 | import VSCollectionKit 12 | 13 | protocol NewsDetailsViewAPI { 14 | var collectionViewData: VSCollectionViewData? { get } 15 | } 16 | 17 | class NewsDetailsViewModel: NewsDetailsViewAPI { 18 | 19 | let item: NewsPageResponse.NewsItem 20 | var collectionViewData: VSCollectionViewData? 21 | 22 | init(newsItem: NewsPageResponse.NewsItem) { 23 | self.item = newsItem 24 | collectionViewData = collectionViewData(for: newsItem) 25 | } 26 | 27 | private func collectionViewData(for newsItem: NewsPageResponse.NewsItem) -> VSCollectionViewData { 28 | var collectionData = VSCollectionViewData() 29 | collectionData.add(section: NewsImageSectionModel(item: newsItem)) 30 | collectionData.add(section: NewsDetailsSectionModel(details: newsItem.abstract)) 31 | return collectionData 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/NewsDetailUI/NewsDetailViewController/NewsDetailsSection/NewsDetailSectionHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsDetailSectionHandler.swift 3 | // NewsDetailsUI 4 | // 5 | // Created by Vinodh Govindaswamy on 28/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import VSCollectionKit 10 | import UIKit 11 | 12 | class NewsDetailSectionHandler: SectionHandler { 13 | 14 | var type: String { 15 | return NewsDetailsSectionType.newsDetails.rawValue 16 | } 17 | 18 | func registerCells(for collectionView: UICollectionView) { 19 | collectionView.register(UINib(nibName: String(describing: NewsDetailsCell.self), 20 | bundle: Bundle(for: NewsDetailsCell.self)), 21 | forCellWithReuseIdentifier: NewsDetailsCell.newsdetailCellID) 22 | } 23 | 24 | func cellProvider(_ collectionView: UICollectionView, _ indexPath: IndexPath, _ cellModel: CellModel) -> UICollectionViewCell { 25 | 26 | guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: NewsDetailsCell.newsdetailCellID, for: indexPath) as? NewsDetailsCell, let newsImageCellModel = cellModel as? NewsDetailsCellModel else { return UICollectionViewCell() } 27 | 28 | cell.cellModel = newsImageCellModel 29 | return cell 30 | } 31 | 32 | func sectionLayoutProvider(_ sectionModel: SectionModel, _ environment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? { 33 | return newImageLayout() 34 | } 35 | 36 | private func newImageLayout() -> NSCollectionLayoutSection { 37 | 38 | let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), 39 | heightDimension: .estimated(100)) 40 | let item = NSCollectionLayoutItem(layoutSize: itemSize) 41 | let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), 42 | heightDimension: .estimated(100)) 43 | let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, 44 | subitems: [item]) 45 | let sectionLayout = NSCollectionLayoutSection(group: group) 46 | return sectionLayout 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/NewsDetailUI/NewsDetailViewController/NewsDetailsSection/NewsDetailSectionModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsDetailSectionModel.swift 3 | // NewsDetailsUI 4 | // 5 | // Created by Vinodh Govindaswamy on 28/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import VSCollectionKit 10 | import NewsService 11 | 12 | struct NewsDetailsSectionModel: SectionModel { 13 | var sectionType: String { 14 | return NewsDetailsSectionType.newsDetails.rawValue 15 | } 16 | 17 | let newsDetailsText: String 18 | let sectionID: String 19 | 20 | init(details: String) { 21 | newsDetailsText = details 22 | items = [NewsDetailsCellModel(details: details)] 23 | sectionID = UUID().uuidString 24 | } 25 | 26 | var header: HeaderViewModel? = nil 27 | var items: [CellModel] 28 | } 29 | 30 | struct NewsDetailsCellModel: CellModel { 31 | let newsDetailsText: String 32 | let cellID: String 33 | init(details: String) { 34 | newsDetailsText = details 35 | cellID = UUID().uuidString 36 | } 37 | 38 | var cellType: String { 39 | return NewsDetailsCellType.newsImage.rawValue 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/NewsDetailUI/NewsDetailViewController/NewsDetailsSection/NewsDetailsCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsDetailsCell.swift 3 | // NewsDetailsUI 4 | // 5 | // Created by Vinodh Govindaswamy on 28/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class NewsDetailsCell: UICollectionViewCell { 12 | 13 | public static let newsdetailCellID = String(describing: NewsDetailsCell.self) 14 | 15 | @IBOutlet weak var newsDetailsLabel: UILabel! 16 | 17 | var cellModel: NewsDetailsCellModel? { 18 | didSet { 19 | newsDetailsLabel.text = cellModel?.newsDetailsText 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/NewsDetailUI/NewsDetailViewController/NewsImageSection/NewsImageCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsImageCell.swift 3 | // NewsDetailsUI 4 | // 5 | // Created by Vinodh Govindaswamy on 28/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import NewsShared 10 | import UIKit 11 | 12 | class NewsImageCell: UICollectionViewCell { 13 | public static let newsImageCellID = String(describing: NewsImageCell.self) 14 | 15 | @IBOutlet weak var titleLabel: UILabel! 16 | @IBOutlet weak var timeStampLabel: UILabel! 17 | @IBOutlet weak var newsImageView: UIImageView! 18 | @IBOutlet weak var blurView: UIVisualEffectView! 19 | var gradientBackground: CAGradientLayer! 20 | 21 | 22 | var cellModel: NewsImageCellModel? { 23 | didSet { 24 | if let viewModel = cellModel { 25 | configureCell(cellModel: viewModel) 26 | } 27 | } 28 | } 29 | 30 | override func awakeFromNib() { 31 | super.awakeFromNib() 32 | setUpGradiantBackground() 33 | } 34 | 35 | override func layoutSubviews() { 36 | super.layoutSubviews() 37 | gradientBackground.frame = contentView.bounds 38 | } 39 | 40 | func configureCell(cellModel: NewsImageCellModel) { 41 | if let imageUrl = cellModel.imageUrl, 42 | let cachedImageUrl = cellModel.cachedImageUrl { 43 | newsImageView.setImageFrom(imageURLString: cachedImageUrl) 44 | blurView.isHidden = false 45 | newsImageView.setImageFrom(imageURLString: imageUrl) { [weak self] (image, error) in 46 | 47 | DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.5) { 48 | self?.blurView.isHidden = true 49 | } 50 | } 51 | } 52 | 53 | titleLabel.text = cellModel.title 54 | timeStampLabel.text = cellModel.timestamp 55 | } 56 | 57 | private func setUpGradiantBackground() { 58 | gradientBackground = CAGradientLayer() 59 | gradientBackground.frame = newsImageView.frame 60 | gradientBackground.colors = [UIColor.clear.cgColor, 61 | UIColor.black.withAlphaComponent(0.9).cgColor] 62 | gradientBackground.startPoint = CGPoint(x: 0, y: 0.5) 63 | gradientBackground.endPoint = CGPoint(x: 0, y: 1) 64 | contentView.layer.insertSublayer(gradientBackground, above: newsImageView.layer) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/NewsDetailUI/NewsDetailViewController/NewsImageSection/NewsImageSectionHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsImageSectionHandler.swift 3 | // NewsDetailsUI 4 | // 5 | // Created by Vinodh Govindaswamy on 28/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import VSCollectionKit 10 | import UIKit 11 | 12 | class NewsImageSectionHandler: SectionHandler { 13 | 14 | var type: String { 15 | return NewsDetailsSectionType.newsImage.rawValue 16 | } 17 | 18 | func registerCells(for collectionView: UICollectionView) { 19 | collectionView.register(UINib(nibName: String(describing: NewsImageCell.self), 20 | bundle: Bundle(for: NewsImageCell.self)), 21 | forCellWithReuseIdentifier: NewsImageCell.newsImageCellID) 22 | } 23 | 24 | func cellProvider(_ collectionView: UICollectionView, _ indexPath: IndexPath, _ cellModel: CellModel) -> UICollectionViewCell { 25 | 26 | guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: NewsImageCell.newsImageCellID, for: indexPath) as? NewsImageCell, 27 | let newsImageCellModel = cellModel as? NewsImageCellModel else { return UICollectionViewCell() } 28 | 29 | cell.cellModel = newsImageCellModel 30 | return cell 31 | } 32 | 33 | func sectionLayoutProvider(_ sectionModel: SectionModel, _ environment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? { 34 | return newImageLayout() 35 | } 36 | 37 | private func newImageLayout() -> NSCollectionLayoutSection { 38 | 39 | let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), 40 | heightDimension: .fractionalHeight(1.0)) 41 | let item = NSCollectionLayoutItem(layoutSize: itemSize) 42 | let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), 43 | heightDimension: .fractionalHeight(0.65)) 44 | let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, 45 | subitems: [item]) 46 | let sectionLayout = NSCollectionLayoutSection(group: group) 47 | return sectionLayout 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/NewsDetailUI/NewsDetailViewController/NewsImageSection/NewsImageSectionModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsImageSectionModel.swift 3 | // NewsDetailsUI 4 | // 5 | // Created by Vinodh Govindaswamy on 28/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import VSCollectionKit 10 | import NewsService 11 | import NewsShared 12 | 13 | struct NewsImageSectionModel: SectionModel { 14 | 15 | var sectionType: String { 16 | return NewsDetailsSectionType.newsImage.rawValue 17 | } 18 | 19 | private let newsItem: NewsPageResponse.NewsItem 20 | let sectionID: String 21 | init (item: NewsPageResponse.NewsItem) { 22 | self.newsItem = item 23 | items = [NewsImageCellModel(item: item)] 24 | sectionID = UUID().uuidString 25 | } 26 | 27 | var header: HeaderViewModel? 28 | var items: [CellModel] 29 | } 30 | 31 | struct NewsImageCellModel: CellModel { 32 | 33 | let newsItem: NewsPageResponse.NewsItem 34 | let cellID: String 35 | init (item: NewsPageResponse.NewsItem) { 36 | self.newsItem = item 37 | cellID = UUID().uuidString 38 | (self.imageUrl, self.cachedImageUrl) = imageUrls(media: item.multimedia) 39 | } 40 | 41 | var cellType: String { 42 | return NewsDetailsCellType.newsImage.rawValue 43 | } 44 | 45 | var title: String { 46 | return newsItem.title 47 | } 48 | 49 | var imageUrl: String? 50 | var cachedImageUrl: String? 51 | 52 | var timestamp: String { 53 | return newsItem.publishedDate.elapsedTimeString() 54 | } 55 | 56 | private func imageUrls(media: [NewsPageResponse.NewsItem.NewsMultiMedia]?) -> (String?, String?) { 57 | var imageUrl: String? = nil 58 | var cacheImageUrl: String? = nil 59 | 60 | media?.forEach { (imageDetails) in 61 | if imageDetails.size == NewsImageSize.large.rawValue { 62 | imageUrl = imageDetails.url 63 | } 64 | 65 | if imageDetails.size == NewsImageSize.medium.rawValue { 66 | cacheImageUrl = imageDetails.url 67 | } 68 | } 69 | 70 | return (imageUrl, cacheImageUrl) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/NewsDetailUI/NewsDetailsUI.h: -------------------------------------------------------------------------------- 1 | // 2 | // NewsDetailsUI.h 3 | // NewsDetailsUI 4 | // 5 | // Created by Vinodh Govindaswamy on 27/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for NewsDetailsUI. 12 | FOUNDATION_EXPORT double NewsDetailsUIVersionNumber; 13 | 14 | //! Project version string for NewsDetailsUI. 15 | FOUNDATION_EXPORT const unsigned char NewsDetailsUIVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/NewsDetailUI/Plugin/NewsDetailAPIPrivate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsDetailAPIPrivate.swift 3 | // TopStoriesUI 4 | // 5 | // Created by Vinodh Govindaswamy on 24/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Plugin 11 | import NewsService 12 | 13 | class NewsDetailAPIPrivate: NewsDetailsUIAPI { 14 | 15 | func newsDetailsViewController(param: NewsDetailsPageParam) -> UIViewController? { 16 | guard let newsItem = param.data as? NewsPageResponse.NewsItem else { return nil } 17 | let viewModel = NewsDetailsViewModel(newsItem: newsItem) 18 | let newsDetailsView = NewsDetailViewController() 19 | newsDetailsView.viewModel = viewModel 20 | return newsDetailsView 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/NewsDetailUI/Plugin/NewsDetailPlugin.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsDetailPlugin.swift 3 | // TopStoriesUI 4 | // 5 | // Created by Vinodh Govindaswamy on 24/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import Plugin 10 | 11 | public class NewsDetailPlugin: Plugable { 12 | 13 | public required init() {} 14 | 15 | public var pluginId: String { 16 | return NewsDetailsUIPluginId 17 | } 18 | 19 | public func plug() -> AnyObject? { 20 | return NewsDetailAPIPrivate() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/TopStoriesUI/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 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/TopStoriesUI/Plugin/TopStoriesAPIPrivate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TopStoriesAPIPrivate.swift 3 | // TopStoriesUI 4 | // 5 | // Created by Vinodh Govindaswamy on 24/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Plugin 11 | 12 | class TopStoriesAPIPrivate: TopStoriesUIAPI { 13 | 14 | func topStoriesViewController(pageId: String) -> UIViewController? { 15 | let viewModel = TopStoriesViewModel(newsPageName: pageId) 16 | let topStoriesViewController = TopStoriesViewController() 17 | topStoriesViewController.viewModel = viewModel 18 | return topStoriesViewController 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/TopStoriesUI/Plugin/TopStoriesPlugin.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TopStoriesPlugin.swift 3 | // TopStoriesUI 4 | // 5 | // Created by Vinodh Govindaswamy on 24/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import Plugin 10 | 11 | public class TopStoriesPlugin: Plugable { 12 | 13 | public required init() {} 14 | 15 | public var pluginId: String { 16 | return TopStoriesUIPluginId 17 | } 18 | 19 | public func plug() -> AnyObject? { 20 | return TopStoriesAPIPrivate() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/TopStoriesUI/TopStoriesTests/ErrorSection/ErrorCellModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ErrorCellModelTests.swift 3 | // TopStoriesTests 4 | // 5 | // Created by Vinodh Govindaswamy on 28/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import TopStoriesUI 11 | 12 | class ErrorCellModelTests: XCTestCase { 13 | 14 | override func setUp() { 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDown() { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | } 21 | 22 | func testCellModelType() { 23 | let cellModel = ErrorCellModel(errorMessage: "Error Message", actionTitle: "Retry") 24 | XCTAssertEqual(cellModel.cellType, NewsCellType.error.rawValue) 25 | } 26 | 27 | func testMessage() { 28 | let cellModel = ErrorCellModel(errorMessage: "Error Message", actionTitle: "Retry") 29 | XCTAssertEqual(cellModel.errorMessage, "Error Message") 30 | } 31 | 32 | func testActionTitle() { 33 | let cellModel = ErrorCellModel(errorMessage: "Error Message", actionTitle: "Retry") 34 | XCTAssertEqual(cellModel.actionTitle, "Retry") 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/TopStoriesUI/TopStoriesTests/ErrorSection/ErrorSectionHandlerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ErrorSectionHandlerTests.swift 3 | // TopStoriesTests 4 | // 5 | // Created by Vinodh Govindaswamy on 28/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import TopStoriesUI 11 | 12 | class ErrorSectionHandlerTests: XCTestCase { 13 | 14 | let sectionHandler: ErrorSectionHandler = ErrorSectionHandler(viewModel: nil) 15 | let collectionView: UICollectionView = UICollectionView(frame: .zero, 16 | collectionViewLayout: UICollectionViewFlowLayout()) 17 | override func setUp() { 18 | sectionHandler.registerCells(for: collectionView) 19 | } 20 | 21 | func testSectionHandlerType() { 22 | XCTAssertEqual(sectionHandler.type, NewsSectionType.error.rawValue) 23 | } 24 | 25 | func testSectionHandlerCell() { 26 | let cell = sectionHandler.cellProvider(collectionView, 27 | IndexPath(item: 0, section: 0), 28 | ErrorCellModel(errorMessage: "Error Message", actionTitle: "Retry")) 29 | 30 | XCTAssert(cell.isKind(of: ErrorCell.self), "Cell should be of type ErrorCell") 31 | } 32 | 33 | func testSectionHandlerLayoutNotNil() { 34 | let layout = sectionHandler.collectionLayout(for: ErrorSectionModel(errorMessage: "Error Message", actionTitle: "Retry"), 35 | environment: MockLayoutEnvironment()) 36 | XCTAssertNotNil(layout) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/TopStoriesUI/TopStoriesTests/ErrorSection/ErrorSectionModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ErrorSectionModelTests.swift 3 | // TopStoriesTests 4 | // 5 | // Created by Vinodh Govindaswamy on 28/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import TopStoriesUI 11 | 12 | class ErrorSectionModelTests: XCTestCase { 13 | 14 | override func setUp() { 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDown() { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | } 21 | 22 | func testSectionModelType() { 23 | let sectionModel = errorSectionModel() 24 | XCTAssertEqual(sectionModel.sectionType, NewsSectionType.error.rawValue) 25 | } 26 | 27 | func testCellModelCount() { 28 | let sectionModel = errorSectionModel() 29 | XCTAssertEqual(sectionModel.items.count, 1) 30 | } 31 | 32 | func errorSectionModel() -> ErrorSectionModel { 33 | return ErrorSectionModel(errorMessage: "Error Message", actionTitle: "Retry") 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/TopStoriesUI/TopStoriesTests/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 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/TopStoriesUI/TopStoriesTests/LoadingSection/LoadingCellModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadingCellModelTests.swift 3 | // TopStoriesTests 4 | // 5 | // Created by Vinodh Govindaswamy on 28/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import TopStoriesUI 11 | 12 | class LoadingCellModelTests: XCTestCase { 13 | 14 | override func setUp() { 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDown() { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | } 21 | 22 | func testCellModelCount() { 23 | let cellModel = LoadingCellModel() 24 | XCTAssertEqual(cellModel.cellType, NewsCellType.loadingskeleton.rawValue) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/TopStoriesUI/TopStoriesTests/LoadingSection/LoadingSectionHandlerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadingSectionHandlerTests.swift 3 | // TopStoriesTests 4 | // 5 | // Created by Vinodh Govindaswamy on 28/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import TopStoriesUI 11 | 12 | class LoadingSectionHandlerTests: XCTestCase { 13 | 14 | let sectionHandler: LoadingSectionHandler = LoadingSectionHandler() 15 | let collectionView: UICollectionView = UICollectionView(frame: .zero, 16 | collectionViewLayout: UICollectionViewFlowLayout()) 17 | override func setUp() { 18 | sectionHandler.registerCells(for: collectionView) 19 | } 20 | 21 | func testSectionHandlerType() { 22 | XCTAssertEqual(sectionHandler.type, NewsSectionType.loading.rawValue) 23 | } 24 | 25 | func testSectionHandlerCell() { 26 | let cell = sectionHandler.cellProvider(collectionView, 27 | IndexPath(item: 0, section: 0), 28 | LoadingCellModel()) 29 | 30 | XCTAssert(cell.isKind(of: LoadingCell.self), "Cell should be of type LoadingCell") 31 | } 32 | 33 | func testSectionHandlerLayoutNotNil() { 34 | let layout = sectionHandler.collectionLayout(for: LoadingSectionModel(), 35 | environment: MockLayoutEnvironment()) 36 | XCTAssertNotNil(layout) 37 | } 38 | } 39 | 40 | class MockLayoutEnvironment: NSObject, NSCollectionLayoutEnvironment { 41 | var container: NSCollectionLayoutContainer = MockLayoutContainer() 42 | var traitCollection: UITraitCollection = .current 43 | 44 | class MockLayoutContainer: NSObject, NSCollectionLayoutContainer { 45 | var contentSize: CGSize = .zero 46 | var effectiveContentSize: CGSize = .zero 47 | var contentInsets: NSDirectionalEdgeInsets = .zero 48 | var effectiveContentInsets: NSDirectionalEdgeInsets = .zero 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/TopStoriesUI/TopStoriesTests/LoadingSection/LoadingSectionModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadingSectionModelTests.swift 3 | // NewsServiceTests 4 | // 5 | // Created by Vinodh Govindaswamy on 28/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import TopStoriesUI 11 | 12 | class LoadingSectionModelTests: XCTestCase { 13 | 14 | override func setUp() { 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDown() { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | } 21 | 22 | func testSectionModelType() { 23 | let sectionModel = loagingsectionModel() 24 | XCTAssertEqual(sectionModel.sectionType, NewsSectionType.loading.rawValue) 25 | } 26 | 27 | func testCellModelCount() { 28 | let sectionModel = loagingsectionModel() 29 | XCTAssertEqual(sectionModel.items.count, 1) 30 | } 31 | 32 | func loagingsectionModel() -> LoadingSectionModel { 33 | return LoadingSectionModel() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/TopStoriesUI/TopStoriesTests/MockNewsService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockNewsService.swift 3 | // VNews 4 | // 5 | // Created by Vinodh Govindaswamy on 28/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | @testable import TopStoriesUI 11 | @testable import NewsService 12 | @testable import NewsShared 13 | 14 | class MockNewsService: NewsServiceAPIPrivate { 15 | let path: String 16 | init(path: String) { 17 | self.path = path 18 | } 19 | override func newsPage(for request: NewsRequestParam, 20 | callback: @escaping (NewsResponseParam) -> Void) { 21 | do { 22 | let newsPage: NewsPageResponse = try TestUtil().get(from: path, in: Bundle(for: MockNewsService.self)) 23 | let response = NewsResponseParam(newsPage: newsPage, 24 | error: nil) 25 | callback(response) 26 | } catch let error { 27 | let response = NewsResponseParam(newsPage: nil, 28 | error: NewsServiceError.unKnown(error: error)) 29 | callback(response) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/TopStoriesUI/TopStoriesTests/NewsSectionModel/NewsSectionHandlerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsSectionHandlerTests.swift 3 | // TopStoriesTests 4 | // 5 | // Created by Vinodh Govindaswamy on 28/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import TopStoriesUI 11 | @testable import NewsShared 12 | 13 | class NewsSectionHandlerTests: XCTestCase { 14 | 15 | let sectionHandler: NewsSectionHandler = NewsSectionHandler(parentViewController: nil) 16 | let collectionView: UICollectionView = UICollectionView(frame: .zero, 17 | collectionViewLayout: UICollectionViewFlowLayout()) 18 | override func setUp() { 19 | sectionHandler.registerCells(for: collectionView) 20 | } 21 | 22 | func testSectionHandlerType() { 23 | XCTAssertEqual(sectionHandler.type, NewsSectionType.news.rawValue) 24 | } 25 | 26 | func testSectionHandlerFullWidthCell() { 27 | guard let cellModel = newsItemCellModel(cellType: .fullWidthCard) else { return } 28 | let cell = sectionHandler.cellProvider(collectionView, 29 | IndexPath(item: 0, section: 0), 30 | cellModel) 31 | 32 | XCTAssert(cell.isKind(of: FullWidthCardCell.self), "Cell should be of type FullWidthCardCell") 33 | } 34 | 35 | func testSectionHandlerCardCell() { 36 | guard let cellModel = newsItemCellModel(cellType: .card) else { return } 37 | let cell = sectionHandler.cellProvider(collectionView, 38 | IndexPath(item: 0, section: 0), 39 | cellModel) 40 | 41 | XCTAssert(cell.isKind(of: CardCell.self), "Cell should be of type CardCell") 42 | } 43 | 44 | func testSectionHandlerListCell() { 45 | guard let cellModel = newsItemCellModel(cellType: .list) else { return } 46 | let cell = sectionHandler.cellProvider(collectionView, 47 | IndexPath(item: 0, section: 0), 48 | cellModel) 49 | 50 | XCTAssert(cell.isKind(of: NewsListCell.self), "Cell should be of type NewsListCell") 51 | } 52 | 53 | func testSectionHandlerLayoutNotNil() { 54 | guard let sectionModel = newsSectionModel(catType: "world") else { return } 55 | 56 | let layout = sectionHandler.collectionLayout(for: sectionModel, 57 | environment: MockLayoutEnvironment()) 58 | XCTAssertNotNil(layout) 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/TopStoriesUI/TopStoriesTests/TopStoriesInteractorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TopStoriesInteractorTests.swift 3 | // NewsServiceTests 4 | // 5 | // Created by Vinodh Govindaswamy on 28/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import TopStoriesUI 11 | @testable import NewsService 12 | 13 | class TopStoriesInteractorTests: XCTestCase { 14 | 15 | override func setUp() { 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | } 22 | 23 | func testFetchTopNews() { 24 | let interactor = topStoriesInteractor() 25 | let param = NewsRequestParam(newsPageId: "home") 26 | interactor.fetchTopNews(requestParam: param) { (items, error) in 27 | XCTAssertNotNil(items) 28 | } 29 | } 30 | 31 | func topStoriesInteractor() -> TopStoriesInteractor { 32 | let interactor = TopStoriesInteractor(service: MockNewsService(path: "MockedNewsPageResponse.json")) 33 | return interactor 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/TopStoriesUI/TopStoriesTests/TopStoriesTests-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/TopStoriesUI/TopStoriesTests/TopStoriesUI.h: -------------------------------------------------------------------------------- 1 | // 2 | // TopStoriesUI.h 3 | // TopStoriesUI 4 | // 5 | // Created by Vinodh Govindaswamy on 07/04/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for TopStoriesUI. 12 | FOUNDATION_EXPORT double TopStoriesUIVersionNumber; 13 | 14 | //! Project version string for TopStoriesUI. 15 | FOUNDATION_EXPORT const unsigned char TopStoriesUIVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/TopStoriesUI/TopStoriesUI.h: -------------------------------------------------------------------------------- 1 | // 2 | // TopStoriesUI.h 3 | // TopStoriesUI 4 | // 5 | // Created by Vinodh Govindaswamy on 24/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for TopStoriesUI. 12 | FOUNDATION_EXPORT double TopStoriesUIVersionNumber; 13 | 14 | //! Project version string for TopStoriesUI. 15 | FOUNDATION_EXPORT const unsigned char TopStoriesUIVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/TopStoriesUI/TopStoriesViewController/ErrorSection/ErrorCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ErrorCell.swift 3 | // VNews 4 | // 5 | // Created by Vinodh Govindaswamy on 28/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import NewsShared 11 | 12 | protocol ErrorActionDelegate: class { 13 | func didTapOn(cell: ErrorCell, errorAction: String) 14 | } 15 | 16 | class ErrorCell: UICollectionViewCell { 17 | 18 | public static let errorCellIdentifier = String(describing: ErrorCell.self) 19 | 20 | @IBOutlet weak var actionButton: UIButton! 21 | @IBOutlet weak var errorMessageLabel: UILabel! 22 | 23 | weak var errorActionDelegate: ErrorActionDelegate? 24 | var cellModel: ErrorCellModel? { 25 | didSet { 26 | errorMessageLabel.text = cellModel?.errorMessage 27 | actionButton.setTitle(cellModel?.actionTitle, 28 | for: .normal) 29 | } 30 | } 31 | 32 | override func awakeFromNib() { 33 | super.awakeFromNib() 34 | setUpView() 35 | } 36 | 37 | private func setUpView() { 38 | actionButton.layer.borderWidth = 2 39 | actionButton.layer.borderColor = AppColor.ErrorCell.actionButtonBorder.cgColor 40 | actionButton.tintColor = AppColor.ErrorCell.actionButtonTint 41 | actionButton.layer.cornerRadius = 8 42 | errorMessageLabel.textColor = AppColor.ErrorCell.errorTitleColor 43 | } 44 | 45 | @IBAction func retryButtonAction(_ sender: Any) { 46 | errorActionDelegate?.didTapOn(cell: self, errorAction: cellModel?.actionTitle ?? "") 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/TopStoriesUI/TopStoriesViewController/ErrorSection/ErrorCellModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ErrorCellModel.swift 3 | // VNews 4 | // 5 | // Created by Vinodh Govindaswamy on 28/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import VSCollectionKit 10 | 11 | struct ErrorCellModel: CellModel { 12 | var cellType: String { 13 | return NewsCellType.error.rawValue 14 | } 15 | 16 | let errorMessage: String 17 | let actionTitle: String 18 | let cellID: String 19 | init(errorMessage: String, actionTitle: String) { 20 | self.errorMessage = errorMessage 21 | self.actionTitle = actionTitle 22 | cellID = UUID().uuidString 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/TopStoriesUI/TopStoriesViewController/ErrorSection/ErrorSectionHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ErrorSectionHandler.swift 3 | // VNews 4 | // 5 | // Created by Vinodh Govindaswamy on 28/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import VSCollectionKit 10 | import UIKit 11 | 12 | class ErrorSectionHandler: SectionHandler { 13 | var type: String { 14 | return NewsSectionType.error.rawValue 15 | } 16 | 17 | let viewModel: TopStoriesViewRetryAction? 18 | init(viewModel: TopStoriesViewRetryAction?) { 19 | self.viewModel = viewModel 20 | } 21 | 22 | func registerCells(for collectionView: UICollectionView) { 23 | 24 | let bundle = Bundle(for: LoadingCell.self) 25 | 26 | collectionView.register(UINib(nibName: String(describing: ErrorCell.self), 27 | bundle: bundle), 28 | forCellWithReuseIdentifier: ErrorCell.errorCellIdentifier) 29 | } 30 | 31 | func cellProvider(_ collectionView: UICollectionView, _ indexPath: IndexPath, _ cellModel: CellModel) -> UICollectionViewCell { 32 | guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ErrorCell.errorCellIdentifier, 33 | for: indexPath) as? ErrorCell, 34 | let errorCellModel = cellModel as? ErrorCellModel else { return UICollectionViewCell() } 35 | cell.cellModel = errorCellModel 36 | cell.errorActionDelegate = self 37 | return cell 38 | } 39 | } 40 | 41 | extension ErrorSectionHandler: ErrorActionDelegate { 42 | func didTapOn(cell: ErrorCell, errorAction: String) { 43 | viewModel?.retry() 44 | } 45 | } 46 | 47 | extension ErrorSectionHandler { 48 | 49 | func sectionLayoutProvider(_ sectionModel: SectionModel, _ environment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? { 50 | let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), 51 | heightDimension: .fractionalHeight(1.0)) 52 | let item = NSCollectionLayoutItem(layoutSize: itemSize) 53 | let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), 54 | heightDimension: .absolute(140)) 55 | let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, 56 | subitems: [item]) 57 | let section = NSCollectionLayoutSection(group: group) 58 | section.contentInsets = NSDirectionalEdgeInsets(top: 12, 59 | leading: 12, 60 | bottom: 12, 61 | trailing: 12) 62 | return section 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/TopStoriesUI/TopStoriesViewController/ErrorSection/ErrorSectionModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ErrorSectionModel.swift 3 | // VNews 4 | // 5 | // Created by Vinodh Govindaswamy on 28/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import VSCollectionKit 10 | 11 | struct ErrorSectionModel: SectionModel { 12 | var sectionType: String { 13 | return NewsSectionType.error.rawValue 14 | } 15 | 16 | var header: HeaderViewModel? = nil 17 | var items: [CellModel] 18 | let sectionID: String 19 | 20 | init(errorMessage: String, actionTitle: String) { 21 | items = [ErrorCellModel(errorMessage: errorMessage, actionTitle: actionTitle)] 22 | sectionID = UUID().uuidString 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/TopStoriesUI/TopStoriesViewController/LoadingSection/LoadingCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadingCell.swift 3 | // TopStoriesUI 4 | // 5 | // Created by Vinodh Govindaswamy on 27/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import NewsShared 10 | import UIKit 11 | 12 | class LoadingCell: UICollectionViewCell { 13 | 14 | public static let loadingCellID = String(describing: LoadingCell.self) 15 | 16 | @IBOutlet weak var loadingContentView: UIView! 17 | @IBOutlet weak var shadowView: UIView! 18 | @IBOutlet weak var loadingMessageLabel: UILabel! 19 | 20 | override func awakeFromNib() { 21 | super.awakeFromNib() 22 | contentView.backgroundColor = AppColor.defaultBackgroundColor 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/TopStoriesUI/TopStoriesViewController/LoadingSection/LoadingCellModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadingCellModel.swift 3 | // TopStoriesUI 4 | // 5 | // Created by Vinodh Govindaswamy on 25/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import VSCollectionKit 11 | 12 | struct LoadingCellModel: CellModel { 13 | var cellID: String 14 | var cellType: String { 15 | return NewsCellType.loadingskeleton.rawValue 16 | } 17 | 18 | init() { 19 | cellID = UUID().uuidString 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/TopStoriesUI/TopStoriesViewController/LoadingSection/LoadingSectionHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadingSectionHandler.swift 3 | // TopStoriesUI 4 | // 5 | // Created by Vinodh Govindaswamy on 25/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import VSCollectionKit 10 | import UIKit 11 | 12 | class LoadingSectionHandler: SectionHandler { 13 | 14 | var type: String { 15 | return NewsSectionType.loading.rawValue 16 | } 17 | 18 | func registerCells(for collectionView: UICollectionView) { 19 | 20 | let bundle = Bundle(for: LoadingCell.self) 21 | 22 | // FullWidthCardCell 23 | collectionView.register(UINib(nibName: String(describing: LoadingCell.self), 24 | bundle: bundle), 25 | forCellWithReuseIdentifier: LoadingCell.loadingCellID) 26 | } 27 | 28 | func cellProvider(_ collectionView: UICollectionView, _ indexPath: IndexPath, _ cellModel: CellModel) -> UICollectionViewCell { 29 | guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: LoadingCell.loadingCellID, 30 | for: indexPath) as? LoadingCell else { return UICollectionViewCell() } 31 | return cell 32 | } 33 | } 34 | 35 | extension LoadingSectionHandler { 36 | 37 | func sectionLayoutProvider(_ sectionModel: SectionModel, _ environment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? { 38 | 39 | let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), 40 | heightDimension: .fractionalHeight(1.0)) 41 | let item = NSCollectionLayoutItem(layoutSize: itemSize) 42 | let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), 43 | heightDimension: .absolute(130)) 44 | let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, 45 | subitems: [item]) 46 | let section = NSCollectionLayoutSection(group: group) 47 | section.contentInsets = NSDirectionalEdgeInsets(top: 12, 48 | leading: 12, 49 | bottom: 12, 50 | trailing: 12) 51 | return section 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/TopStoriesUI/TopStoriesViewController/LoadingSection/LoadingSectionModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadingSectionModel.swift 3 | // TopStoriesUI 4 | // 5 | // Created by Vinodh Govindaswamy on 25/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import VSCollectionKit 11 | 12 | struct LoadingSectionModel: SectionModel { 13 | var sectionType: String { 14 | return NewsSectionType.loading.rawValue 15 | } 16 | var header: HeaderViewModel? 17 | var items: [CellModel] = [] 18 | let sectionID: String 19 | init() { 20 | items.append(LoadingCellModel()) 21 | sectionID = UUID().uuidString 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/TopStoriesUI/TopStoriesViewController/NewsSections/CardCell/CardCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScienceNewsItemCell.swift 3 | // TopStoriesUI 4 | // 5 | // Created by Vinodh Govindaswamy on 27/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import NewsShared 10 | import UIKit 11 | 12 | class CardCell: UICollectionViewCell { 13 | 14 | public static let newsItemCellID = String(describing: CardCell.self) 15 | private static let cornerRadius: CGFloat = 8 16 | 17 | @IBOutlet private weak var newsContentView: UIView! 18 | @IBOutlet private weak var contentShadowView: UIView! 19 | @IBOutlet private weak var newsImageView: UIImageView! 20 | @IBOutlet private weak var titleLabel: UILabel! 21 | @IBOutlet private weak var timestampLabel: UILabel! 22 | 23 | var cellModel: NewsCellViewAPI? { 24 | didSet { 25 | if let viewModel = cellModel { 26 | configureCell(for: viewModel) 27 | } 28 | } 29 | } 30 | 31 | override func awakeFromNib() { 32 | super.awakeFromNib() 33 | configureView() 34 | } 35 | 36 | override func prepareForReuse() { 37 | super.prepareForReuse() 38 | newsImageView.image = nil 39 | } 40 | 41 | override func layoutSubviews() { 42 | super.layoutSubviews() 43 | contentShadowView.addShadow(cornerRadius: CardCell.cornerRadius) 44 | } 45 | 46 | func configureView() { 47 | newsContentView.layer.cornerRadius = CardCell.cornerRadius 48 | newsContentView.layer.masksToBounds = true 49 | 50 | contentView.backgroundColor = AppColor.defaultBackgroundColor 51 | titleLabel.textColor = AppColor.defaultTitleTextColor 52 | timestampLabel.textColor = AppColor.defaulttimeStampTextColor 53 | } 54 | 55 | func configureCell(for cellModel: NewsCellViewAPI) { 56 | if let imageUrl = cellModel.imageURL(size: .medium) { 57 | newsImageView.setImageFrom(imageURLString: imageUrl) 58 | } 59 | titleLabel.text = cellModel.title 60 | timestampLabel.text = cellModel.timeStamp 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/TopStoriesUI/TopStoriesViewController/NewsSections/FullWidthCardCell/FullWidthCardCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FullWidthCardCell.swift 3 | // TopStoriesUI 4 | // 5 | // Created by Vinodh Govindaswamy on 25/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import NewsShared 11 | 12 | class FullWidthCardCell: UICollectionViewCell { 13 | 14 | public static let newsItemCellID = String(describing: FullWidthCardCell.self) 15 | 16 | @IBOutlet private weak var newsContentView: UIView! 17 | @IBOutlet private weak var newsImageView: UIImageView! 18 | @IBOutlet private weak var titleLabel: UILabel! 19 | @IBOutlet private weak var timestampLabel: UILabel! 20 | var gradientBackground: CAGradientLayer = CAGradientLayer() 21 | 22 | var cellModel: NewsCellViewAPI? { 23 | didSet { 24 | if let viewModel = cellModel { 25 | configureCell(for: viewModel) 26 | } 27 | } 28 | } 29 | 30 | override func awakeFromNib() { 31 | super.awakeFromNib() 32 | contentView.backgroundColor = AppColor.defaultBackgroundColor 33 | newsContentView.layer.insertSublayer(gradientBackground, above: newsImageView.layer) 34 | } 35 | 36 | override func layoutSubviews() { 37 | super.layoutSubviews() 38 | setUpGradiantBackground() 39 | } 40 | 41 | override func prepareForReuse() { 42 | super.prepareForReuse() 43 | newsImageView.image = nil 44 | } 45 | 46 | private func configureCell(for cellModel: NewsCellViewAPI) { 47 | if let imageUrl = cellModel.imageURL(size: .medium) { 48 | newsImageView.setImageFrom(imageURLString: imageUrl) 49 | } 50 | titleLabel.text = cellModel.title 51 | timestampLabel.text = cellModel.timeStamp 52 | } 53 | 54 | private func configureUI() { 55 | titleLabel.textColor = AppColor.highlightTitleTextColor 56 | timestampLabel.textColor = AppColor.highlighttimeStampTextColor 57 | } 58 | 59 | 60 | private func setUpGradiantBackground() { 61 | gradientBackground.frame = newsContentView.bounds 62 | gradientBackground.colors = [UIColor.clear.cgColor, 63 | UIColor.black.withAlphaComponent(0.9).cgColor] 64 | gradientBackground.startPoint = CGPoint(x: 0, y: 0.5) 65 | gradientBackground.endPoint = CGPoint(x: 0, y: 1) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/TopStoriesUI/TopStoriesViewController/NewsSections/Header/NewsHeaderModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsHeaderModel.swift 3 | // TopStoriesUI 4 | // 5 | // Created by Vinodh Govindaswamy on 25/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import VSCollectionKit 10 | 11 | struct NewsHeaderModel: HeaderViewModel { 12 | 13 | let headerTitle: String 14 | init(title: String) { 15 | headerTitle = title 16 | } 17 | 18 | var headerType: String { 19 | return "" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/TopStoriesUI/TopStoriesViewController/NewsSections/Header/NewsSectionHeaderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsSectionHeaderView.swift 3 | // TopStoriesUI 4 | // 5 | // Created by Vinodh Govindaswamy on 25/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import NewsShared 10 | import UIKit 11 | 12 | class NewsSectionHeaderView: UICollectionReusableView { 13 | 14 | public static let newsHeaderViewReuseID = String(describing: NewsSectionHeaderView.self) 15 | @IBOutlet weak var titleLabel: UILabel! 16 | 17 | var viewModel: NewsHeaderModel? { 18 | didSet { 19 | titleLabel.text = viewModel?.headerTitle 20 | } 21 | } 22 | 23 | override func awakeFromNib() { 24 | super.awakeFromNib() 25 | backgroundColor = AppColor.defaultBackgroundColor 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/TopStoriesUI/TopStoriesViewController/NewsSections/ListCell/NewsListCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsListCell.swift 3 | // TopStoriesUI 4 | // 5 | // Created by Vinodh Govindaswamy on 26/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import NewsShared 10 | import UIKit 11 | 12 | class NewsListCell: UICollectionViewCell { 13 | 14 | public static let newsItemCellID = String(describing: NewsListCell.self) 15 | 16 | private static let cornerRadius: CGFloat = 8 17 | 18 | @IBOutlet weak var contentShadowView: UIView! 19 | @IBOutlet weak var newsContentView: UIView! 20 | @IBOutlet weak var newsImageView: UIImageView! 21 | @IBOutlet weak var titleLabel: UILabel! 22 | @IBOutlet weak var timestampLabel: UILabel! 23 | 24 | var cellModel: NewsCellViewAPI? { 25 | didSet { 26 | if let viewModel = cellModel { 27 | configureCell(for: viewModel) 28 | } 29 | } 30 | } 31 | 32 | override func awakeFromNib() { 33 | super.awakeFromNib() 34 | configureView() 35 | } 36 | 37 | override func prepareForReuse() { 38 | super.prepareForReuse() 39 | newsImageView.image = nil 40 | } 41 | 42 | override func layoutSubviews() { 43 | super.layoutSubviews() 44 | contentShadowView.addShadow(cornerRadius: NewsListCell.cornerRadius) 45 | } 46 | 47 | func configureView() { 48 | newsContentView.layer.cornerRadius = NewsListCell.cornerRadius 49 | newsContentView.layer.masksToBounds = true 50 | 51 | contentView.backgroundColor = AppColor.defaultBackgroundColor 52 | titleLabel.textColor = AppColor.defaultTitleTextColor 53 | timestampLabel.textColor = AppColor.defaulttimeStampTextColor 54 | } 55 | 56 | func configureCell(for cellModel: NewsCellViewAPI) { 57 | if let imageUrl = cellModel.imageURL(size: .medium) { 58 | newsImageView.setImageFrom(imageURLString: imageUrl) 59 | } 60 | titleLabel.text = cellModel.title 61 | timestampLabel.text = cellModel.timeStamp 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/TopStoriesUI/TopStoriesViewController/NewsSections/NewsCellModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsCellModel.swift 3 | // TopStoriesUI 4 | // 5 | // Created by Vinodh Govindaswamy on 25/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import NewsShared 10 | import NewsService 11 | import VSCollectionKit 12 | 13 | protocol NewsCellViewAPI { 14 | var title: String { get } 15 | var abstract: String { get } 16 | var timeStamp: String { get } 17 | var newsData: Any { get } 18 | func imageURL(size: NewsImageSize) -> String? 19 | } 20 | 21 | struct NewsCellModel: CellModel, NewsCellViewAPI { 22 | let cellID: String 23 | var cellType: String 24 | private let newsItem: NewsPageResponse.NewsItem 25 | private let imageURLCache: [String: String] 26 | 27 | init(cellType: NewsCellType, 28 | newsItem: NewsPageResponse.NewsItem) { 29 | self.newsItem = newsItem 30 | self.cellType = cellType.rawValue 31 | self.imageURLCache = NewsCellModel.newsImageURLCache(multimedia: newsItem.multimedia) 32 | cellID = UUID().uuidString 33 | } 34 | 35 | var title: String { 36 | return newsItem.title 37 | } 38 | 39 | var abstract: String { 40 | return newsItem.abstract 41 | } 42 | 43 | var timeStamp: String { 44 | return newsItem.publishedDate.elapsedTimeString() 45 | } 46 | 47 | var newsData: Any { 48 | return newsItem 49 | } 50 | 51 | func imageURL(size: NewsImageSize) -> String? { 52 | return imageURLCache[size.rawValue] 53 | } 54 | 55 | private static func newsImageURLCache(multimedia: [NewsPageResponse.NewsItem.NewsMultiMedia]?) -> [String: String] { 56 | var cache: [String: String] = [:] 57 | multimedia?.forEach{ (media) in 58 | cache[media.size] = media.url 59 | } 60 | 61 | return cache 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/TopStoriesUI/TopStoriesViewController/NewsSections/NewsSectionModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StoriesSectionModel.swift 3 | // TopStoriesUI 4 | // 5 | // Created by Vinodh Govindaswamy on 25/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import VSCollectionKit 10 | import NewsService 11 | 12 | struct NewsSectionModel: SectionModel { 13 | 14 | var sectionType: String { 15 | return NewsSectionType.news.rawValue 16 | } 17 | 18 | var header: HeaderViewModel? 19 | var items: [CellModel] = [] 20 | let categoryName: String 21 | let categoryType: String 22 | let sectionID: String 23 | init(categoryType: String, 24 | categoryName: String, 25 | newsItems: [NewsPageResponse.NewsItem]) { 26 | self.categoryName = categoryName 27 | self.categoryType = categoryType 28 | header = NewsHeaderModel(title: categoryName) 29 | sectionID = UUID().uuidString 30 | if let cellType = NewsCellType(rawValue: NewsLayoutHandler.layoutType(for: categoryType).rawValue) { 31 | newsItems.forEach { (newsItem) in 32 | items.append(NewsCellModel(cellType: cellType, 33 | newsItem: newsItem)) 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/TopStoriesUI/TopStoriesViewController/TopStoriesInteractor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TopStoriesInteractor.swift 3 | // TopStoriesUI 4 | // 5 | // Created by Vinodh Govindaswamy on 25/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import NewsService 10 | 11 | class TopStoriesInteractor { 12 | let service: NewsServiceAPI 13 | init(service: NewsServiceAPI = NewsServicePlugin.newsServicePlugin()) { 14 | self.service = service 15 | } 16 | 17 | func fetchTopNews(requestParam: NewsRequestParam, 18 | callback: @escaping (_ newsResponse: [NewsPageResponse.NewsItem]?, _ error: Error?) -> Void ) { 19 | service.newsPage(for: requestParam) { (response) in 20 | guard let newsItems = response.newsPage?.items else { 21 | callback(nil, response.error) 22 | return 23 | } 24 | 25 | // TODO: write the news to database service, so next time, when user launches the app, 26 | // uses the news from DB and fetch the latest from service 27 | callback(newsItems, nil) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Features/TopStoriesUI/TopStoriesViewController/TopStoriesViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TopStoriesViewController.swift 3 | // TopStoriesUI 4 | // 5 | // Created by Vinodh Govindaswamy on 24/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import NewsShared 10 | import UIKit 11 | import VSCollectionKit 12 | 13 | enum NewsSectionType: String { 14 | case loading 15 | case news 16 | case error 17 | } 18 | 19 | enum NewsCellType: String { 20 | case loadingskeleton 21 | case fullWidthCard 22 | case card 23 | case groupedList 24 | case list 25 | case error 26 | } 27 | 28 | class TopStoriesViewController: VSCollectionViewController { 29 | 30 | var viewModel: TopStoriesViewAPI? 31 | 32 | override func willAddSectionControllers() { 33 | super.willAddSectionControllers() 34 | sectionHandler.addSectionHandler(handler: LoadingSectionHandler()) 35 | sectionHandler.addSectionHandler(handler: newsSectionHandler()) 36 | sectionHandler.addSectionHandler(handler: errorSectionHandler()) 37 | } 38 | 39 | override func viewDidLoad() { 40 | super.viewDidLoad() 41 | observeViewModelUpdates() 42 | viewModel?.fetchTopStories() 43 | 44 | configureNavigationBar() 45 | view.backgroundColor = .white 46 | } 47 | 48 | override func viewDidAppear(_ animated: Bool) { 49 | super.viewDidAppear(animated) 50 | } 51 | 52 | func newsSectionHandler() -> SectionHandler { 53 | return NewsSectionHandler(parentViewController: self.parent) 54 | } 55 | 56 | func errorSectionHandler() -> SectionHandler { 57 | return ErrorSectionHandler(viewModel: viewModel as? TopStoriesViewRetryAction) 58 | } 59 | 60 | override func configureCollectionView() { 61 | super.configureCollectionView() 62 | collectionView.backgroundColor = AppColor.defaultBackgroundColor 63 | } 64 | 65 | private func configureNavigationBar() { 66 | navigationItem.title = viewModel?.title 67 | navigationController?.navigationBar.prefersLargeTitles = true 68 | } 69 | 70 | private func observeViewModelUpdates() { 71 | viewModel?.viewUpdateHandler = { [weak self] (collectionData, errrMessage) in 72 | guard let self = self else { return } 73 | guard let collectionViewData = collectionData else { return } 74 | self.apply(collectionData: collectionViewData, animated: true) 75 | } 76 | } 77 | } 78 | 79 | extension TopStoriesViewController { 80 | override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) { 81 | coordinator.animate(alongsideTransition: { (context) in 82 | self.collectionView.collectionViewLayout.invalidateLayout() 83 | self.collectionView.reloadData() 84 | }, completion: nil) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/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 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | 37 | 38 | 39 | 40 | UILaunchStoryboardName 41 | LaunchScreen 42 | UIRequiredDeviceCapabilities 43 | 44 | armv7 45 | 46 | UISupportedInterfaceOrientations 47 | 48 | UIInterfaceOrientationPortrait 49 | UIInterfaceOrientationLandscapeRight 50 | UIInterfaceOrientationLandscapeLeft 51 | 52 | UISupportedInterfaceOrientations~ipad 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationPortraitUpsideDown 56 | UIInterfaceOrientationLandscapeLeft 57 | UIInterfaceOrientationLandscapeRight 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/MainViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainViewController.swift 3 | // NewsApp 4 | // 5 | // Created by Vinodh Govindaswamy on 07/04/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import Plugin 10 | import UIKit 11 | 12 | class MainViewController: UIViewController { 13 | 14 | override func viewDidLoad() { 15 | super.viewDidLoad() 16 | setUpTabViewController() 17 | } 18 | 19 | 20 | func topStoriesViewController() -> UIViewController? { 21 | guard let topStoriesPlugin = PluginManager.shared.plugin(for: TopStoriesUIPluginId)?.plug() as? TopStoriesUIAPI, 22 | let storiesView = topStoriesPlugin.topStoriesViewController(pageId: "home") else { return nil } 23 | return storiesView 24 | } 25 | 26 | func setUpTabViewController() { 27 | 28 | let tabBarController = UITabBarController() 29 | guard let topStoriesView = topStoriesViewController() else { return } 30 | let navigationController = UINavigationController(rootViewController: topStoriesView) 31 | navigationController.view.backgroundColor = navigationController.navigationBar.barTintColor 32 | 33 | tabBarController.setViewControllers([navigationController], animated: false) 34 | 35 | view.addSubview(tabBarController.view) 36 | addChild(tabBarController) 37 | tabBarController.overrideUserInterfaceStyle = .light 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Platform/AppConfigService/AppConfigService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppConfigService.swift 3 | // AppConfigService 4 | // 5 | // Created by Vinodh Govindaswamy on 07/04/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | public class AppConfigService { 10 | public static func topStoriesUIConfig() -> TopStoriesUIConfig { 11 | return TopStoriesUIConfig() 12 | } 13 | } 14 | 15 | public struct TopStoriesUIConfig { 16 | static public var worldNewsCategory: NewsCategory = NewsCategory(name: "World", type: "world") 17 | static public var usNewsCategory: NewsCategory = NewsCategory(name: "US", type: "us") 18 | static public var sportsNewsCategory: NewsCategory = NewsCategory(name: "Sports", type: "") 19 | static public var scienceNewsCategory: NewsCategory = NewsCategory(name: "Science", type: "science") 20 | static public var technologyNewsCategory: NewsCategory = NewsCategory(name: "Technology", type: "technology") 21 | static public var businessNewsCategory: NewsCategory = NewsCategory(name: "Business", type: "business") 22 | static public var otherNewsCategory: NewsCategory = NewsCategory(name: "Others", type: "others") 23 | 24 | 25 | public var newsCategories: [NewsCategory] = { 26 | return [TopStoriesUIConfig.worldNewsCategory, 27 | TopStoriesUIConfig.usNewsCategory, 28 | TopStoriesUIConfig.scienceNewsCategory, 29 | TopStoriesUIConfig.businessNewsCategory, 30 | TopStoriesUIConfig.technologyNewsCategory, 31 | TopStoriesUIConfig.sportsNewsCategory] 32 | }() 33 | } 34 | 35 | public struct NewsCategory { 36 | public let name: String 37 | public let type: String 38 | } 39 | 40 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Platform/NewsService/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 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Platform/NewsService/Model/NewsPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsPage.swift 3 | // NewsService 4 | // 5 | // Created by Vinodh Govindaswamy on 24/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct NewsPageResponse: Decodable { 12 | public let status: String 13 | public let updated: Date 14 | public let items: [NewsItem] 15 | 16 | public struct NewsItem: Decodable { 17 | public let sectionName: String 18 | public let title: String 19 | public let abstract: String 20 | public let url: String 21 | public let publishedDate: Date 22 | public let multimedia: [NewsMultiMedia]? 23 | 24 | public struct NewsMultiMedia: Decodable { 25 | public let url: String 26 | public let size: String 27 | public let type: String 28 | 29 | enum CodingKeys: String, CodingKey { 30 | case url = "url" 31 | case size = "format" 32 | case type = "type" 33 | } 34 | } 35 | 36 | enum CodingKeys: String, CodingKey { 37 | case sectionName = "section" 38 | case title = "title" 39 | case abstract = "abstract" 40 | case url = "url" 41 | case publishedDate = "published_date" 42 | case multimedia = "multimedia" 43 | } 44 | } 45 | 46 | enum CodingKeys: String, CodingKey { 47 | case status = "status" 48 | case updated = "last_updated" 49 | case items = "results" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Platform/NewsService/NewsService.h: -------------------------------------------------------------------------------- 1 | // 2 | // NewsService.h 3 | // NewsService 4 | // 5 | // Created by Vinodh Govindaswamy on 07/04/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for NewsService. 12 | FOUNDATION_EXPORT double NewsServiceVersionNumber; 13 | 14 | //! Project version string for NewsService. 15 | FOUNDATION_EXPORT const unsigned char NewsServiceVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Platform/NewsService/NewsServiceAPI/NewsServiceAPIPrivate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsServicePluginPrivate.swift 3 | // NewsService 4 | // 5 | // Created by Vinodh Govindaswamy on 24/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public let apiSecret = "DDg70NUMC2esrUKHXabfXAbmnhpD5n5f" 12 | public let apiKey = "api-key" 13 | 14 | class NewsServiceAPIPrivate: NewsServiceAPI { 15 | 16 | static let topStoriesPath = "topstories/v2" 17 | private let service: WebService 18 | init(service: WebService = WebService()) { 19 | self.service = service 20 | } 21 | 22 | func newsPage(for request: NewsRequestParam, 23 | callback: @escaping (NewsResponseParam) -> Void) { 24 | 25 | let path = "\(NewsServiceAPIPrivate.topStoriesPath)/\(request.newsPageId).json" 26 | let request = service.request(requestType: .GET, 27 | requestPath: path) 28 | request.setQuery(params: [apiKey: apiSecret]) 29 | request.response { (data, response) in 30 | do { 31 | let decoder = JSONDecoder() 32 | decoder.dateDecodingStrategy = .iso8601 33 | let newsPageResponse = try decoder.decode(NewsPageResponse.self, 34 | from: data) 35 | let responseParam = NewsResponseParam(newsPage: newsPageResponse, 36 | error: nil) 37 | callback(responseParam) 38 | } catch let error { 39 | let decodeError = NewsServiceError.decodedError(decodedError: error) 40 | let responseParam = NewsResponseParam(newsPage: nil, 41 | error: decodeError) 42 | callback(responseParam) 43 | } 44 | }.responseError { (error) in 45 | let responseParam = NewsResponseParam(newsPage: nil, 46 | error: error) 47 | callback(responseParam) 48 | } 49 | request.fetch() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Platform/NewsService/NewsServiceAPI/NewsServicePlugin.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsServicePlugin.swift 3 | // NewsService 4 | // 5 | // Created by Vinodh Govindaswamy on 24/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct NewsRequestParam { 12 | public let newsPageId: String 13 | public let pageToken: String? 14 | public init(newsPageId: String, pageToken: String? = nil) { 15 | self.newsPageId = newsPageId 16 | self.pageToken = pageToken 17 | } 18 | } 19 | 20 | public struct NewsResponseParam { 21 | public let newsPage: NewsPageResponse? 22 | public let error: NewsServiceError? 23 | } 24 | 25 | public protocol NewsServiceAPI { 26 | func newsPage(for request: NewsRequestParam, 27 | callback: @escaping (_ messagesResponse: NewsResponseParam) -> Void) 28 | } 29 | 30 | public class NewsServicePlugin { 31 | public static func newsServicePlugin() -> NewsServiceAPI { 32 | return NewsServiceAPIPrivate() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Platform/NewsService/NewsServiceTests/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 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Platform/NewsService/NewsServiceTests/MockURLSession.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockURLSession.swift 3 | // MusicServiceTests 4 | // 5 | // Created by Vinodh Govindaswamy on 28/06/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class MockURLSession: URLSession { 12 | 13 | var fileName: String = "" 14 | init(fileName: String) { 15 | self.fileName = fileName 16 | } 17 | 18 | override func dataTask(with request: URLRequest, 19 | completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { 20 | return MockDataTask(fileName: fileName, 21 | handler: completionHandler) 22 | } 23 | } 24 | 25 | class MockDataTask: URLSessionDataTask { 26 | let handler: (Data?, URLResponse?, Error?) -> Void 27 | let fileName: String 28 | init(fileName: String, handler: @escaping (Data?, URLResponse?, Error?) -> Void) { 29 | self.fileName = fileName 30 | self.handler = handler 31 | } 32 | 33 | override func resume() { 34 | guard let path = Bundle(for: MockDataTask.self).path(forResource: fileName, ofType: "json") else { 35 | self.handler(nil, nil, nil) 36 | return 37 | } 38 | 39 | do { 40 | let fileUrl = URL(fileURLWithPath: path) 41 | let data = try Data(contentsOf: fileUrl) 42 | self.handler(data, HTTPURLResponse(url: fileUrl, 43 | statusCode: 200, 44 | httpVersion: nil, 45 | headerFields: nil), nil) 46 | } catch let error { 47 | self.handler(nil, nil, error) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Platform/NewsService/NewsServiceTests/MockWebService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockWebService.swift 3 | // NewsServiceTests 4 | // 5 | // Created by Vinodh Govindaswamy on 24/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | @testable import NewsService 10 | @testable import NewsShared 11 | 12 | class MockWebServiceValidResponse: WebService { 13 | override func request(requestType: ServiceRequestType, requestPath: String) -> ServiceRequest { 14 | return MockServiceRequestValid(requestPath: requestPath, 15 | requestType: requestType, session: MockSession.shared) 16 | } 17 | } 18 | 19 | 20 | class MockServiceRequestValid: ServiceRequest { 21 | override func fetch() { 22 | guard let jsonData = TestUtil().data(from: "MockedNewsPageResponse.json", 23 | in: Bundle(for: TestUtil.self)) else { 24 | return 25 | } 26 | 27 | if let responseBlock = self.successBlock { 28 | responseBlock(jsonData, URLResponse()) 29 | } 30 | } 31 | } 32 | 33 | 34 | class MockSession: URLSession {} 35 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Platform/NewsService/NewsServiceTests/MockedNewsPageErrorResponse.json: -------------------------------------------------------------------------------- 1 | 2 | {section: "world",subsection: "",title: "Coronavirus Live Updates: House Passes $2 Trillion Relief Bill"} 3 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Platform/NewsService/NewsServiceTests/NewsServiceAPIPrivateTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsServiceAPIPrivateTests.swift 3 | // NewsServiceTests 4 | // 5 | // Created by Vinodh Govindaswamy on 24/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import NewsService 11 | 12 | class NewsServiceAPIPrivateTests: XCTestCase { 13 | 14 | override func setUp() { 15 | } 16 | 17 | override func tearDown() { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testChatMessagesDecodingMessageResponse() { 22 | let param = NewsRequestParam(newsPageId: "home") 23 | let newsServiceApi = NewsServiceAPIPrivate(service: MockWebServiceValidResponse()) 24 | 25 | newsServiceApi.newsPage(for: param) { (responseParam) in 26 | guard let newsItems = responseParam.newsPage?.items else { 27 | XCTAssert(false, "Unable to Decode the mock newsResponse") 28 | return 29 | } 30 | XCTAssertEqual(newsItems.count, 25) 31 | } 32 | } 33 | 34 | // TODO: Yet to handle other error cases 35 | } 36 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/Platform/NewsService/NewsServiceTests/ServiceRequestTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebServiceTests.swift 3 | // NewsServiceTests 4 | // 5 | // Created by Vinodh Govindaswamy on 28/06/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import NewsService 11 | 12 | class ServiceRequestTests: XCTestCase { 13 | 14 | override func setUp() { 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDown() { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | } 21 | 22 | func testServiceRequestFetchVerifyData() { 23 | let request = ServiceRequest(requestPath: "https://api.nytimes.com/svc", session: MockURLSession(fileName: "MockedNewsPageResponse")) 24 | request.response { (data, response) in 25 | XCTAssertNotNil(data) 26 | }.responseError { (error) in 27 | XCTAssertNil(error) 28 | } 29 | request.fetch() 30 | } 31 | 32 | func testServiceRequestFetchVerifyResponse() { 33 | let request = ServiceRequest(requestPath: "https://api.nytimes.com/svc", session: MockURLSession(fileName: "MockedNewsPageResponse")) 34 | request.response { (data, response) in 35 | XCTAssertNotNil(data) 36 | }.responseError { (error) in 37 | XCTAssertNil(error) 38 | } 39 | request.fetch() 40 | } 41 | 42 | func testServiceRequestFetchVerifyInvalidURLError() { 43 | let request = ServiceRequest(requestPath: "https://api.nytimes.com svc", session: MockURLSession(fileName: "MockedNewsPageErrorResponse")) 44 | request.response { (data, response) in 45 | XCTAssertNil(data) 46 | }.responseError { (error) in 47 | switch error { 48 | case .invalidURL: 49 | XCTAssertTrue(true) 50 | default: 51 | XCTAssertTrue(false) 52 | } 53 | } 54 | request.fetch() 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /NewsApp/NewsApp/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // NewsApp 4 | // 5 | // Created by Vinodh Govindaswamy on 07/04/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 12 | 13 | var window: UIWindow? 14 | 15 | 16 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 17 | 18 | if let windowScene = scene as? UIWindowScene { 19 | let window = UIWindow(windowScene: windowScene) 20 | window.rootViewController = MainViewController() 21 | self.window = window 22 | window.makeKeyAndVisible() 23 | } 24 | } 25 | 26 | func sceneDidDisconnect(_ scene: UIScene) { 27 | // Called as the scene is being released by the system. 28 | // This occurs shortly after the scene enters the background, or when its session is discarded. 29 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 30 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 31 | } 32 | 33 | func sceneDidBecomeActive(_ scene: UIScene) { 34 | // Called when the scene has moved from an inactive state to an active state. 35 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 36 | } 37 | 38 | func sceneWillResignActive(_ scene: UIScene) { 39 | // Called when the scene will move from an active state to an inactive state. 40 | // This may occur due to temporary interruptions (ex. an incoming phone call). 41 | } 42 | 43 | func sceneWillEnterForeground(_ scene: UIScene) { 44 | // Called as the scene transitions from the background to the foreground. 45 | // Use this method to undo the changes made on entering the background. 46 | } 47 | 48 | func sceneDidEnterBackground(_ scene: UIScene) { 49 | // Called as the scene transitions from the foreground to the background. 50 | // Use this method to save data, release shared resources, and store enough scene-specific state information 51 | // to restore the scene back to its current state. 52 | } 53 | 54 | 55 | } 56 | 57 | -------------------------------------------------------------------------------- /NewsApp/TopStoriesTests/ErrorSection/ErrorCellModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ErrorCellModelTests.swift 3 | // TopStoriesTests 4 | // 5 | // Created by Vinodh Govindaswamy on 28/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import TopStoriesUI 11 | 12 | class ErrorCellModelTests: XCTestCase { 13 | 14 | override func setUp() { 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDown() { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | } 21 | 22 | func testCellModelType() { 23 | let cellModel = ErrorCellModel(errorMessage: "Error Message", actionTitle: "Retry") 24 | XCTAssertEqual(cellModel.cellType, NewsCellType.error.rawValue) 25 | } 26 | 27 | func testMessage() { 28 | let cellModel = ErrorCellModel(errorMessage: "Error Message", actionTitle: "Retry") 29 | XCTAssertEqual(cellModel.errorMessage, "Error Message") 30 | } 31 | 32 | func testActionTitle() { 33 | let cellModel = ErrorCellModel(errorMessage: "Error Message", actionTitle: "Retry") 34 | XCTAssertEqual(cellModel.actionTitle, "Retry") 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /NewsApp/TopStoriesTests/ErrorSection/ErrorSectionHandlerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ErrorSectionHandlerTests.swift 3 | // TopStoriesTests 4 | // 5 | // Created by Vinodh Govindaswamy on 28/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import TopStoriesUI 11 | 12 | class ErrorSectionHandlerTests: XCTestCase { 13 | 14 | let sectionHandler: ErrorSectionHandler = ErrorSectionHandler(viewModel: nil) 15 | let collectionView: UICollectionView = UICollectionView(frame: .zero, 16 | collectionViewLayout: UICollectionViewFlowLayout()) 17 | override func setUp() { 18 | sectionHandler.registerCells(for: collectionView) 19 | } 20 | 21 | func testSectionHandlerType() { 22 | XCTAssertEqual(sectionHandler.type, NewsSectionType.error.rawValue) 23 | } 24 | 25 | func testSectionHandlerCell() { 26 | let cell = sectionHandler.cellProvider(collectionView, 27 | IndexPath(item: 0, section: 0), 28 | ErrorCellModel(errorMessage: "Error Message", actionTitle: "Retry")) 29 | 30 | XCTAssert(cell.isKind(of: ErrorCell.self), "Cell should be of type ErrorCell") 31 | } 32 | 33 | func testSectionHandlerLayoutNotNil() { 34 | let layout = sectionHandler.sectionLayoutProvider(ErrorSectionModel(errorMessage: "Error Message", actionTitle: "Retry"), 35 | MockLayoutEnvironment()) 36 | XCTAssertNotNil(layout) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /NewsApp/TopStoriesTests/ErrorSection/ErrorSectionModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ErrorSectionModelTests.swift 3 | // TopStoriesTests 4 | // 5 | // Created by Vinodh Govindaswamy on 28/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import TopStoriesUI 11 | 12 | class ErrorSectionModelTests: XCTestCase { 13 | 14 | override func setUp() { 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDown() { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | } 21 | 22 | func testSectionModelType() { 23 | let sectionModel = errorSectionModel() 24 | XCTAssertEqual(sectionModel.sectionType, NewsSectionType.error.rawValue) 25 | } 26 | 27 | func testCellModelCount() { 28 | let sectionModel = errorSectionModel() 29 | XCTAssertEqual(sectionModel.items.count, 1) 30 | } 31 | 32 | func errorSectionModel() -> ErrorSectionModel { 33 | return ErrorSectionModel(errorMessage: "Error Message", actionTitle: "Retry") 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /NewsApp/TopStoriesTests/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 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /NewsApp/TopStoriesTests/LoadingSection/LoadingCellModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadingCellModelTests.swift 3 | // TopStoriesTests 4 | // 5 | // Created by Vinodh Govindaswamy on 28/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import TopStoriesUI 11 | 12 | class LoadingCellModelTests: XCTestCase { 13 | 14 | override func setUp() { 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDown() { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | } 21 | 22 | func testCellModelCount() { 23 | let cellModel = LoadingCellModel() 24 | XCTAssertEqual(cellModel.cellType, NewsCellType.loadingskeleton.rawValue) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /NewsApp/TopStoriesTests/LoadingSection/LoadingSectionHandlerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadingSectionHandlerTests.swift 3 | // TopStoriesTests 4 | // 5 | // Created by Vinodh Govindaswamy on 28/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import TopStoriesUI 11 | 12 | class LoadingSectionHandlerTests: XCTestCase { 13 | 14 | let sectionHandler: LoadingSectionHandler = LoadingSectionHandler() 15 | let collectionView: UICollectionView = UICollectionView(frame: .zero, 16 | collectionViewLayout: UICollectionViewFlowLayout()) 17 | override func setUp() { 18 | sectionHandler.registerCells(for: collectionView) 19 | } 20 | 21 | func testSectionHandlerType() { 22 | XCTAssertEqual(sectionHandler.type, NewsSectionType.loading.rawValue) 23 | } 24 | 25 | func testSectionHandlerCell() { 26 | let cell = sectionHandler.cellProvider(collectionView, 27 | IndexPath(item: 0, section: 0), 28 | LoadingCellModel()) 29 | 30 | XCTAssert(cell.isKind(of: LoadingCell.self), "Cell should be of type LoadingCell") 31 | } 32 | 33 | func testSectionHandlerLayoutNotNil() { 34 | let layout = sectionHandler.sectionLayoutProvider(LoadingSectionModel(), 35 | MockLayoutEnvironment()) 36 | XCTAssertNotNil(layout) 37 | } 38 | } 39 | 40 | class MockLayoutEnvironment: NSObject, NSCollectionLayoutEnvironment { 41 | var container: NSCollectionLayoutContainer = MockLayoutContainer() 42 | var traitCollection: UITraitCollection = .current 43 | 44 | class MockLayoutContainer: NSObject, NSCollectionLayoutContainer { 45 | var contentSize: CGSize = .zero 46 | var effectiveContentSize: CGSize = .zero 47 | var contentInsets: NSDirectionalEdgeInsets = .zero 48 | var effectiveContentInsets: NSDirectionalEdgeInsets = .zero 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /NewsApp/TopStoriesTests/LoadingSection/LoadingSectionModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadingSectionModelTests.swift 3 | // NewsServiceTests 4 | // 5 | // Created by Vinodh Govindaswamy on 28/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import TopStoriesUI 11 | 12 | class LoadingSectionModelTests: XCTestCase { 13 | 14 | override func setUp() { 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDown() { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | } 21 | 22 | func testSectionModelType() { 23 | let sectionModel = loagingsectionModel() 24 | XCTAssertEqual(sectionModel.sectionType, NewsSectionType.loading.rawValue) 25 | } 26 | 27 | func testCellModelCount() { 28 | let sectionModel = loagingsectionModel() 29 | XCTAssertEqual(sectionModel.items.count, 1) 30 | } 31 | 32 | func loagingsectionModel() -> LoadingSectionModel { 33 | return LoadingSectionModel() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /NewsApp/TopStoriesTests/MockNewsService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockNewsService.swift 3 | // VNews 4 | // 5 | // Created by Vinodh Govindaswamy on 28/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | @testable import TopStoriesUI 11 | @testable import NewsService 12 | @testable import NewsShared 13 | 14 | class MockNewsService: NewsServiceAPIPrivate { 15 | let path: String 16 | init(path: String) { 17 | self.path = path 18 | } 19 | override func newsPage(for request: NewsRequestParam, 20 | callback: @escaping (NewsResponseParam) -> Void) { 21 | do { 22 | let newsPage: NewsPageResponse = try TestUtil().get(from: path, in: Bundle(for: MockNewsService.self)) 23 | let response = NewsResponseParam(newsPage: newsPage, 24 | error: nil) 25 | callback(response) 26 | } catch let error { 27 | let response = NewsResponseParam(newsPage: nil, 28 | error: NewsServiceError.unKnown(error: error)) 29 | callback(response) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /NewsApp/TopStoriesTests/NewsSectionModel/NewsSectionHandlerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewsSectionHandlerTests.swift 3 | // TopStoriesTests 4 | // 5 | // Created by Vinodh Govindaswamy on 28/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import TopStoriesUI 11 | @testable import NewsShared 12 | 13 | class NewsSectionHandlerTests: XCTestCase { 14 | 15 | let sectionHandler: NewsSectionHandler = NewsSectionHandler(parentViewController: nil) 16 | let collectionView: UICollectionView = UICollectionView(frame: .zero, 17 | collectionViewLayout: UICollectionViewFlowLayout()) 18 | override func setUp() { 19 | sectionHandler.registerCells(for: collectionView) 20 | } 21 | 22 | func testSectionHandlerType() { 23 | XCTAssertEqual(sectionHandler.type, NewsSectionType.news.rawValue) 24 | } 25 | 26 | func testSectionHandlerFullWidthCell() { 27 | guard let cellModel = newsItemCellModel(cellType: .fullWidthCard) else { return } 28 | let cell = sectionHandler.cellProvider(collectionView, 29 | IndexPath(item: 0, section: 0), 30 | cellModel) 31 | 32 | XCTAssert(cell.isKind(of: FullWidthCardCell.self), "Cell should be of type FullWidthCardCell") 33 | } 34 | 35 | func testSectionHandlerCardCell() { 36 | guard let cellModel = newsItemCellModel(cellType: .card) else { return } 37 | let cell = sectionHandler.cellProvider(collectionView, 38 | IndexPath(item: 0, section: 0), 39 | cellModel) 40 | 41 | XCTAssert(cell.isKind(of: CardCell.self), "Cell should be of type CardCell") 42 | } 43 | 44 | func testSectionHandlerListCell() { 45 | guard let cellModel = newsItemCellModel(cellType: .list) else { return } 46 | let cell = sectionHandler.cellProvider(collectionView, 47 | IndexPath(item: 0, section: 0), 48 | cellModel) 49 | 50 | XCTAssert(cell.isKind(of: NewsListCell.self), "Cell should be of type NewsListCell") 51 | } 52 | 53 | func testSectionHandlerLayoutNotNil() { 54 | guard let sectionModel = newsSectionModel(catType: "world") else { return } 55 | 56 | let layout = sectionHandler.sectionLayoutProvider(sectionModel, 57 | MockLayoutEnvironment()) 58 | XCTAssertNotNil(layout) 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /NewsApp/TopStoriesTests/TopStoriesInteractorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TopStoriesInteractorTests.swift 3 | // NewsServiceTests 4 | // 5 | // Created by Vinodh Govindaswamy on 28/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import TopStoriesUI 11 | @testable import NewsService 12 | 13 | class TopStoriesInteractorTests: XCTestCase { 14 | 15 | override func setUp() { 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | } 22 | 23 | func testFetchTopNews() { 24 | let interactor = topStoriesInteractor() 25 | let param = NewsRequestParam(newsPageId: "home") 26 | interactor.fetchTopNews(requestParam: param) { (items, error) in 27 | XCTAssertNotNil(items) 28 | } 29 | } 30 | 31 | func topStoriesInteractor() -> TopStoriesInteractor { 32 | let interactor = TopStoriesInteractor(service: MockNewsService(path: "MockedNewsPageResponse.json")) 33 | return interactor 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /NewsApp/TopStoriesTests/TopStoriesViewModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TopStoriesViewModelTests.swift 3 | // TopStoriesTests 4 | // 5 | // Created by Vinodh Govindaswamy on 28/03/20. 6 | // Copyright © 2020 Vinodh Govindaswamy. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import TopStoriesUI 11 | @testable import NewsService 12 | 13 | class TopStoriesViewModelTests: XCTestCase { 14 | 15 | override func setUp() { 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | } 22 | 23 | func testCollectionDataNotNilBeforeFetch() { 24 | let viewModel = mockViewModel() 25 | XCTAssertNil(viewModel.collectionViewData) 26 | } 27 | 28 | func testCollectionDataNotNilAfterFetch() { 29 | let viewModel = mockViewModel() 30 | viewModel.fetchTopStories() 31 | XCTAssertNotNil(viewModel.collectionViewData) 32 | } 33 | 34 | func testCollectionDataSectionCountForValidData() { 35 | let viewModel = mockViewModel() 36 | viewModel.fetchTopStories() 37 | XCTAssertEqual(viewModel.collectionViewData?.sections.count, 3) 38 | } 39 | 40 | func testCollectionDataSectionCountForInValidData() { 41 | let viewModel = mockViewModel(mockFile: "MockedNewsPageErrorResponse.json") 42 | viewModel.fetchTopStories() 43 | XCTAssertEqual(viewModel.collectionViewData?.sections.count, 1) 44 | } 45 | 46 | func testCollectionDataErrorSectionTypeForInValidData() { 47 | let viewModel = mockViewModel(mockFile: "MockedNewsPageErrorResponse.json") 48 | viewModel.fetchTopStories() 49 | XCTAssertEqual(viewModel.collectionViewData?.sections[0].sectionType, NewsSectionType.error.rawValue) 50 | } 51 | 52 | func testCollectionDataSectionNewsCatTypeWorld() { 53 | let viewModel = mockViewModel() 54 | viewModel.fetchTopStories() 55 | XCTAssertEqual((viewModel.collectionViewData?.sections[0] as? NewsSectionModel)?.categoryType, "world") 56 | } 57 | 58 | 59 | func testCollectionDataSectionNewsCatTypeScience() { 60 | let viewModel = mockViewModel() 61 | viewModel.fetchTopStories() 62 | XCTAssertEqual((viewModel.collectionViewData?.sections[1] as? NewsSectionModel)?.categoryType, "science") 63 | } 64 | 65 | 66 | func testCollectionDataSectionNewsCatTypeOthers() { 67 | let viewModel = mockViewModel() 68 | viewModel.fetchTopStories() 69 | XCTAssertEqual((viewModel.collectionViewData?.sections[2] as? NewsSectionModel)?.categoryType, "others") 70 | } 71 | 72 | func mockViewModel(mockFile: String = "MockedNewsPageResponse.json") -> TopStoriesViewModel { 73 | let interactor = TopStoriesInteractor(service: MockNewsService(path: mockFile)) 74 | return TopStoriesViewModel(newsPageName: "home", 75 | interator: interactor) 76 | } 77 | } 78 | 79 | -------------------------------------------------------------------------------- /NewsApp/TopStoriesUI/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 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | --------------------------------------------------------------------------------