├── 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 |
--------------------------------------------------------------------------------