diff --git a/NewsApp.xcodeproj/project.pbxproj b/NewsApp.xcodeproj/project.pbxproj new file mode 100644 index 0000000..3014122 --- /dev/null +++ b/NewsApp.xcodeproj/project.pbxproj @@ -0,0 +1,972 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 51; + objects = { + +/* Begin PBXBuildFile section */ + 215DF85F5039819CAF1F2763 /* Pods_NewsAppTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2AB066F34EFE88039B2F430B /* Pods_NewsAppTests.framework */; }; + 899CBAE4BD4F1774DBCAC341 /* Pods_NewsApp_NewsAppUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 12FD3A2C390216D77AF2191D /* Pods_NewsApp_NewsAppUITests.framework */; }; + C82B95F69D401E3B9A758A82 /* Pods_NewsApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F28C51CE83B2C2881282DDA6 /* Pods_NewsApp.framework */; }; + F582EF6525BF76A800302CBF /* NewsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F582EF6425BF76A800302CBF /* NewsService.swift */; }; + F582EF6A25BF912900302CBF /* NewsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F582EF6925BF912900302CBF /* NewsProvider.swift */; }; + F582EF8425BFA25D00302CBF /* Date+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F582EF8325BFA25D00302CBF /* Date+Extension.swift */; }; + F582EF8D25BFA58700302CBF /* NewsListElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = F582EF8C25BFA58700302CBF /* NewsListElement.swift */; }; + F582EF9525BFC4AE00302CBF /* Bundle+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F582EF9425BFC4AE00302CBF /* Bundle+Extension.swift */; }; + F582EFB425C04FB400302CBF /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = F582EFB325C04FB400302CBF /* Constants.swift */; }; + F582EFC325C07F2C00302CBF /* ServiceError+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F582EFC225C07F2B00302CBF /* ServiceError+Extension.swift */; }; + F582EFC825C080BE00302CBF /* DefaultServerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F582EFC725C080BE00302CBF /* DefaultServerError.swift */; }; + F582EFDA25C11FF500302CBF /* Codable+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F582EFD925C11FF500302CBF /* Codable+Extension.swift */; }; + F582EFDF25C12AC300302CBF /* UIViewController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F582EFDE25C12AC200302CBF /* UIViewController+Extension.swift */; }; + F582EFE425C1352100302CBF /* NewsListTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F582EFE325C1352100302CBF /* NewsListTableViewController.swift */; }; + F582EFE925C1356600302CBF /* NewsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F582EFE825C1356600302CBF /* NewsTableViewCell.swift */; }; + F582EFFB25C1463A00302CBF /* LoggedCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F582EFFA25C1463A00302CBF /* LoggedCoordinator.swift */; }; + F582F01125C1495500302CBF /* NewsListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F582F01025C1495500302CBF /* NewsListViewModel.swift */; }; + F582F02D25C21F2D00302CBF /* ImageCornerRadius.swift in Sources */ = {isa = PBXBuildFile; fileRef = F582F02C25C21F2D00302CBF /* ImageCornerRadius.swift */; }; + F582F03225C23A5C00302CBF /* NewsDetailTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F582F03125C23A5C00302CBF /* NewsDetailTableViewController.swift */; }; + F582F03725C23BA000302CBF /* NewsDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F582F03625C23BA000302CBF /* NewsDetailViewModel.swift */; }; + F582F03D25C2420600302CBF /* NewsDetailWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = F582F03C25C2420500302CBF /* NewsDetailWrapper.swift */; }; + F5E3197325BDEF50002D1034 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E3197225BDEF50002D1034 /* AppDelegate.swift */; }; + F5E3197525BDEF50002D1034 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E3197425BDEF50002D1034 /* SceneDelegate.swift */; }; + F5E3197A25BDEF50002D1034 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F5E3197825BDEF50002D1034 /* Main.storyboard */; }; + F5E3197C25BDEF53002D1034 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F5E3197B25BDEF53002D1034 /* Assets.xcassets */; }; + F5E3197F25BDEF53002D1034 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F5E3197D25BDEF53002D1034 /* LaunchScreen.storyboard */; }; + F5E3198A25BDEF53002D1034 /* NewsAppTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E3198925BDEF53002D1034 /* NewsAppTests.swift */; }; + F5E3199525BDEF54002D1034 /* NewsAppUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E3199425BDEF54002D1034 /* NewsAppUITests.swift */; }; + F5E319A925BDF548002D1034 /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E319A825BDF548002D1034 /* AppCoordinator.swift */; }; + F5E319BA25BE4E2E002D1034 /* ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E319B925BE4E2E002D1034 /* ViewModel.swift */; }; + F5E319C325BE4E74002D1034 /* UIViewController+Storyboardable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E319C225BE4E74002D1034 /* UIViewController+Storyboardable.swift */; }; + F5E319D125BE5651002D1034 /* UserLogin.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E319D025BE5651002D1034 /* UserLogin.swift */; }; + F5E319E725BE594F002D1034 /* LoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E319E625BE594F002D1034 /* LoginViewController.swift */; }; + F5E319EC25BE5A71002D1034 /* LoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E319EB25BE5A71002D1034 /* LoginViewModel.swift */; }; + F5E319F125BE6DF9002D1034 /* TokenJwt.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E319F025BE6DF9002D1034 /* TokenJwt.swift */; }; + F5E31A1325BEA8F8002D1034 /* Color.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F5E31A1225BEA8F7002D1034 /* Color.xcassets */; }; + F5E31A1E25BEAEF4002D1034 /* NewsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E31A1D25BEAEF4002D1034 /* NewsButton.swift */; }; + F5E31A2725BEB2F9002D1034 /* RoundedImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E31A2625BEB2F9002D1034 /* RoundedImage.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + F5E3198625BDEF53002D1034 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F5E3196725BDEF50002D1034 /* Project object */; + proxyType = 1; + remoteGlobalIDString = F5E3196E25BDEF50002D1034; + remoteInfo = NewsApp; + }; + F5E3199125BDEF54002D1034 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F5E3196725BDEF50002D1034 /* Project object */; + proxyType = 1; + remoteGlobalIDString = F5E3196E25BDEF50002D1034; + remoteInfo = NewsApp; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 065E37E010A3687594509B07 /* Pods-NewsApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewsApp.release.xcconfig"; path = "Target Support Files/Pods-NewsApp/Pods-NewsApp.release.xcconfig"; sourceTree = ""; }; + 12FD3A2C390216D77AF2191D /* Pods_NewsApp_NewsAppUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NewsApp_NewsAppUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 2AB066F34EFE88039B2F430B /* Pods_NewsAppTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NewsAppTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 47EF3A55144D896DB8A8209C /* Pods-NewsApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewsApp.debug.xcconfig"; path = "Target Support Files/Pods-NewsApp/Pods-NewsApp.debug.xcconfig"; sourceTree = ""; }; + 53DB52C778CF4FC908FCD8AF /* Pods-NewsApp-NewsAppUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewsApp-NewsAppUITests.release.xcconfig"; path = "Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests.release.xcconfig"; sourceTree = ""; }; + 78513634FB1CA7FC7ADBA430 /* Pods-NewsAppTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewsAppTests.release.xcconfig"; path = "Target Support Files/Pods-NewsAppTests/Pods-NewsAppTests.release.xcconfig"; sourceTree = ""; }; + 8AF3838C628881C64578C1FE /* Pods-NewsApp-NewsAppUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewsApp-NewsAppUITests.debug.xcconfig"; path = "Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests.debug.xcconfig"; sourceTree = ""; }; + F28C51CE83B2C2881282DDA6 /* Pods_NewsApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NewsApp.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + F582EF6425BF76A800302CBF /* NewsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsService.swift; sourceTree = ""; }; + F582EF6925BF912900302CBF /* NewsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsProvider.swift; sourceTree = ""; }; + F582EF8325BFA25D00302CBF /* Date+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extension.swift"; sourceTree = ""; }; + F582EF8C25BFA58700302CBF /* NewsListElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsListElement.swift; sourceTree = ""; }; + F582EF9425BFC4AE00302CBF /* Bundle+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+Extension.swift"; sourceTree = ""; }; + F582EFB325C04FB400302CBF /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; + F582EFC225C07F2B00302CBF /* ServiceError+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ServiceError+Extension.swift"; sourceTree = ""; }; + F582EFC725C080BE00302CBF /* DefaultServerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultServerError.swift; sourceTree = ""; }; + F582EFD925C11FF500302CBF /* Codable+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Codable+Extension.swift"; sourceTree = ""; }; + F582EFDE25C12AC200302CBF /* UIViewController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Extension.swift"; sourceTree = ""; }; + F582EFE325C1352100302CBF /* NewsListTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsListTableViewController.swift; sourceTree = ""; }; + F582EFE825C1356600302CBF /* NewsTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsTableViewCell.swift; sourceTree = ""; }; + F582EFFA25C1463A00302CBF /* LoggedCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggedCoordinator.swift; sourceTree = ""; }; + F582F01025C1495500302CBF /* NewsListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsListViewModel.swift; sourceTree = ""; }; + F582F02C25C21F2D00302CBF /* ImageCornerRadius.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCornerRadius.swift; sourceTree = ""; }; + F582F03125C23A5C00302CBF /* NewsDetailTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsDetailTableViewController.swift; sourceTree = ""; }; + F582F03625C23BA000302CBF /* NewsDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsDetailViewModel.swift; sourceTree = ""; }; + F582F03B25C23D5100302CBF /* NewsList.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = NewsList.json; sourceTree = ""; }; + F582F03C25C2420500302CBF /* NewsDetailWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsDetailWrapper.swift; sourceTree = ""; }; + F582F05025C242B400302CBF /* NewsDetail.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = NewsDetail.json; sourceTree = ""; }; + F5E3196F25BDEF50002D1034 /* NewsApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NewsApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; + F5E3197225BDEF50002D1034 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + F5E3197425BDEF50002D1034 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + F5E3197925BDEF50002D1034 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + F5E3197B25BDEF53002D1034 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + F5E3197E25BDEF53002D1034 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + F5E3198025BDEF53002D1034 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + F5E3198525BDEF53002D1034 /* NewsAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NewsAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + F5E3198925BDEF53002D1034 /* NewsAppTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsAppTests.swift; sourceTree = ""; }; + F5E3198B25BDEF53002D1034 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + F5E3199025BDEF54002D1034 /* NewsAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NewsAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + F5E3199425BDEF54002D1034 /* NewsAppUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsAppUITests.swift; sourceTree = ""; }; + F5E3199625BDEF54002D1034 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + F5E319A825BDF548002D1034 /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = ""; }; + F5E319B925BE4E2E002D1034 /* ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModel.swift; sourceTree = ""; }; + F5E319C225BE4E74002D1034 /* UIViewController+Storyboardable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Storyboardable.swift"; sourceTree = ""; }; + F5E319D025BE5651002D1034 /* UserLogin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserLogin.swift; sourceTree = ""; }; + F5E319E625BE594F002D1034 /* LoginViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewController.swift; sourceTree = ""; }; + F5E319EB25BE5A71002D1034 /* LoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModel.swift; sourceTree = ""; }; + F5E319F025BE6DF9002D1034 /* TokenJwt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenJwt.swift; sourceTree = ""; }; + F5E31A0325BEA171002D1034 /* TokenJwt.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = TokenJwt.json; sourceTree = ""; }; + F5E31A0825BEA5D0002D1034 /* EmptyTokenJwt.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = EmptyTokenJwt.json; sourceTree = ""; }; + F5E31A1225BEA8F7002D1034 /* Color.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Color.xcassets; sourceTree = ""; }; + F5E31A1D25BEAEF4002D1034 /* NewsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsButton.swift; sourceTree = ""; }; + F5E31A2625BEB2F9002D1034 /* RoundedImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedImage.swift; sourceTree = ""; }; + FF4FB029991FAF7116D35CAA /* Pods-NewsAppTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NewsAppTests.debug.xcconfig"; path = "Target Support Files/Pods-NewsAppTests/Pods-NewsAppTests.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + F5E3196C25BDEF50002D1034 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C82B95F69D401E3B9A758A82 /* Pods_NewsApp.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F5E3198225BDEF53002D1034 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 215DF85F5039819CAF1F2763 /* Pods_NewsAppTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F5E3198D25BDEF53002D1034 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 899CBAE4BD4F1774DBCAC341 /* Pods_NewsApp_NewsAppUITests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 562BC8548692B72FEBF0BD61 /* Frameworks */ = { + isa = PBXGroup; + children = ( + F28C51CE83B2C2881282DDA6 /* Pods_NewsApp.framework */, + 12FD3A2C390216D77AF2191D /* Pods_NewsApp_NewsAppUITests.framework */, + 2AB066F34EFE88039B2F430B /* Pods_NewsAppTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 951202BE6700D448C72BBA9E /* Pods */ = { + isa = PBXGroup; + children = ( + 47EF3A55144D896DB8A8209C /* Pods-NewsApp.debug.xcconfig */, + 065E37E010A3687594509B07 /* Pods-NewsApp.release.xcconfig */, + 8AF3838C628881C64578C1FE /* Pods-NewsApp-NewsAppUITests.debug.xcconfig */, + 53DB52C778CF4FC908FCD8AF /* Pods-NewsApp-NewsAppUITests.release.xcconfig */, + FF4FB029991FAF7116D35CAA /* Pods-NewsAppTests.debug.xcconfig */, + 78513634FB1CA7FC7ADBA430 /* Pods-NewsAppTests.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + F56B3C7A25C2637A0048C1DE /* Mocks */ = { + isa = PBXGroup; + children = ( + F5E31A0325BEA171002D1034 /* TokenJwt.json */, + F5E31A0825BEA5D0002D1034 /* EmptyTokenJwt.json */, + F582F03B25C23D5100302CBF /* NewsList.json */, + F582F05025C242B400302CBF /* NewsDetail.json */, + ); + path = Mocks; + sourceTree = ""; + }; + F582EF7925BFA1C600302CBF /* Utils */ = { + isa = PBXGroup; + children = ( + F5E319BE25BE4E56002D1034 /* Extensions */, + F582EFB325C04FB400302CBF /* Constants.swift */, + ); + path = Utils; + sourceTree = ""; + }; + F582EF8825BFA3B200302CBF /* NewsList */ = { + isa = PBXGroup; + children = ( + F582EF8B25BFA56700302CBF /* Model */, + F582EFE325C1352100302CBF /* NewsListTableViewController.swift */, + F582F01025C1495500302CBF /* NewsListViewModel.swift */, + F582EFE825C1356600302CBF /* NewsTableViewCell.swift */, + ); + path = NewsList; + sourceTree = ""; + }; + F582EF8A25BFA54900302CBF /* News Detail */ = { + isa = PBXGroup; + children = ( + F582F03125C23A5C00302CBF /* NewsDetailTableViewController.swift */, + F582F03625C23BA000302CBF /* NewsDetailViewModel.swift */, + F582F04125C2420B00302CBF /* Model */, + ); + path = "News Detail"; + sourceTree = ""; + }; + F582EF8B25BFA56700302CBF /* Model */ = { + isa = PBXGroup; + children = ( + F582EF8C25BFA58700302CBF /* NewsListElement.swift */, + ); + path = Model; + sourceTree = ""; + }; + F582F04125C2420B00302CBF /* Model */ = { + isa = PBXGroup; + children = ( + F582F03C25C2420500302CBF /* NewsDetailWrapper.swift */, + ); + path = Model; + sourceTree = ""; + }; + F5917F9925C260EF00B54EF3 /* Logged Area */ = { + isa = PBXGroup; + children = ( + F582EFFA25C1463A00302CBF /* LoggedCoordinator.swift */, + F582EF8825BFA3B200302CBF /* NewsList */, + F582EF8A25BFA54900302CBF /* News Detail */, + ); + path = "Logged Area"; + sourceTree = ""; + }; + F5E3196625BDEF50002D1034 = { + isa = PBXGroup; + children = ( + F5E3197125BDEF50002D1034 /* NewsApp */, + F5E3198825BDEF53002D1034 /* NewsAppTests */, + F5E3199325BDEF54002D1034 /* NewsAppUITests */, + F5E3197025BDEF50002D1034 /* Products */, + 951202BE6700D448C72BBA9E /* Pods */, + 562BC8548692B72FEBF0BD61 /* Frameworks */, + ); + sourceTree = ""; + }; + F5E3197025BDEF50002D1034 /* Products */ = { + isa = PBXGroup; + children = ( + F5E3196F25BDEF50002D1034 /* NewsApp.app */, + F5E3198525BDEF53002D1034 /* NewsAppTests.xctest */, + F5E3199025BDEF54002D1034 /* NewsAppUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + F5E3197125BDEF50002D1034 /* NewsApp */ = { + isa = PBXGroup; + children = ( + F5E3197225BDEF50002D1034 /* AppDelegate.swift */, + F5E3197425BDEF50002D1034 /* SceneDelegate.swift */, + F5E31A2225BEB295002D1034 /* Components */, + F5E319DE25BE58DC002D1034 /* Scenes */, + F5E319CC25BE549F002D1034 /* Service */, + F582EF7925BFA1C600302CBF /* Utils */, + F5E3197B25BDEF53002D1034 /* Assets.xcassets */, + F5E31A1225BEA8F7002D1034 /* Color.xcassets */, + F5E3197D25BDEF53002D1034 /* LaunchScreen.storyboard */, + F5E3198025BDEF53002D1034 /* Info.plist */, + ); + path = NewsApp; + sourceTree = ""; + }; + F5E3198825BDEF53002D1034 /* NewsAppTests */ = { + isa = PBXGroup; + children = ( + F5E3198925BDEF53002D1034 /* NewsAppTests.swift */, + F5E3198B25BDEF53002D1034 /* Info.plist */, + ); + path = NewsAppTests; + sourceTree = ""; + }; + F5E3199325BDEF54002D1034 /* NewsAppUITests */ = { + isa = PBXGroup; + children = ( + F5E3199425BDEF54002D1034 /* NewsAppUITests.swift */, + F5E3199625BDEF54002D1034 /* Info.plist */, + ); + path = NewsAppUITests; + sourceTree = ""; + }; + F5E319BE25BE4E56002D1034 /* Extensions */ = { + isa = PBXGroup; + children = ( + F5E319B925BE4E2E002D1034 /* ViewModel.swift */, + F5E319C225BE4E74002D1034 /* UIViewController+Storyboardable.swift */, + F582EF8325BFA25D00302CBF /* Date+Extension.swift */, + F582EF9425BFC4AE00302CBF /* Bundle+Extension.swift */, + F582EFD925C11FF500302CBF /* Codable+Extension.swift */, + F582EFDE25C12AC200302CBF /* UIViewController+Extension.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + F5E319CC25BE549F002D1034 /* Service */ = { + isa = PBXGroup; + children = ( + F582EFC225C07F2B00302CBF /* ServiceError+Extension.swift */, + F582EFC725C080BE00302CBF /* DefaultServerError.swift */, + F582EF6425BF76A800302CBF /* NewsService.swift */, + F582EF6925BF912900302CBF /* NewsProvider.swift */, + F56B3C7A25C2637A0048C1DE /* Mocks */, + ); + path = Service; + sourceTree = ""; + }; + F5E319D525BE56D8002D1034 /* Model */ = { + isa = PBXGroup; + children = ( + F5E319D025BE5651002D1034 /* UserLogin.swift */, + F5E319F025BE6DF9002D1034 /* TokenJwt.swift */, + ); + path = Model; + sourceTree = ""; + }; + F5E319DE25BE58DC002D1034 /* Scenes */ = { + isa = PBXGroup; + children = ( + F5E3197825BDEF50002D1034 /* Main.storyboard */, + F5E319A825BDF548002D1034 /* AppCoordinator.swift */, + F5E319E225BE58E8002D1034 /* Login */, + F5917F9925C260EF00B54EF3 /* Logged Area */, + ); + path = Scenes; + sourceTree = ""; + }; + F5E319E225BE58E8002D1034 /* Login */ = { + isa = PBXGroup; + children = ( + F5E319D525BE56D8002D1034 /* Model */, + F5E319E625BE594F002D1034 /* LoginViewController.swift */, + F5E319EB25BE5A71002D1034 /* LoginViewModel.swift */, + ); + path = Login; + sourceTree = ""; + }; + F5E31A2225BEB295002D1034 /* Components */ = { + isa = PBXGroup; + children = ( + F5E31A1D25BEAEF4002D1034 /* NewsButton.swift */, + F5E31A2625BEB2F9002D1034 /* RoundedImage.swift */, + F582F02C25C21F2D00302CBF /* ImageCornerRadius.swift */, + ); + path = Components; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + F5E3196E25BDEF50002D1034 /* NewsApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = F5E3199925BDEF54002D1034 /* Build configuration list for PBXNativeTarget "NewsApp" */; + buildPhases = ( + 8B7CC7C4CFE07ACA35534CF6 /* [CP] Check Pods Manifest.lock */, + F5E3196B25BDEF50002D1034 /* Sources */, + F5E3196C25BDEF50002D1034 /* Frameworks */, + F5E3196D25BDEF50002D1034 /* Resources */, + E47157A87217EA711A250B2B /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = NewsApp; + productName = NewsApp; + productReference = F5E3196F25BDEF50002D1034 /* NewsApp.app */; + productType = "com.apple.product-type.application"; + }; + F5E3198425BDEF53002D1034 /* NewsAppTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = F5E3199C25BDEF54002D1034 /* Build configuration list for PBXNativeTarget "NewsAppTests" */; + buildPhases = ( + 2D4E87CB829D9C513F5E0AA5 /* [CP] Check Pods Manifest.lock */, + F5E3198125BDEF53002D1034 /* Sources */, + F5E3198225BDEF53002D1034 /* Frameworks */, + F5E3198325BDEF53002D1034 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + F5E3198725BDEF53002D1034 /* PBXTargetDependency */, + ); + name = NewsAppTests; + productName = NewsAppTests; + productReference = F5E3198525BDEF53002D1034 /* NewsAppTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + F5E3198F25BDEF53002D1034 /* NewsAppUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = F5E3199F25BDEF54002D1034 /* Build configuration list for PBXNativeTarget "NewsAppUITests" */; + buildPhases = ( + CA98F1FF4376E8BFD1BEA4FF /* [CP] Check Pods Manifest.lock */, + F5E3198C25BDEF53002D1034 /* Sources */, + F5E3198D25BDEF53002D1034 /* Frameworks */, + F5E3198E25BDEF53002D1034 /* Resources */, + 815B8988ED75800A5FE0F2F7 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + F5E3199225BDEF54002D1034 /* PBXTargetDependency */, + ); + name = NewsAppUITests; + productName = NewsAppUITests; + productReference = F5E3199025BDEF54002D1034 /* NewsAppUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + F5E3196725BDEF50002D1034 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1230; + LastUpgradeCheck = 1230; + TargetAttributes = { + F5E3196E25BDEF50002D1034 = { + CreatedOnToolsVersion = 12.3; + }; + F5E3198425BDEF53002D1034 = { + CreatedOnToolsVersion = 12.3; + TestTargetID = F5E3196E25BDEF50002D1034; + }; + F5E3198F25BDEF53002D1034 = { + CreatedOnToolsVersion = 12.3; + TestTargetID = F5E3196E25BDEF50002D1034; + }; + }; + }; + buildConfigurationList = F5E3196A25BDEF50002D1034 /* Build configuration list for PBXProject "NewsApp" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = F5E3196625BDEF50002D1034; + productRefGroup = F5E3197025BDEF50002D1034 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + F5E3196E25BDEF50002D1034 /* NewsApp */, + F5E3198425BDEF53002D1034 /* NewsAppTests */, + F5E3198F25BDEF53002D1034 /* NewsAppUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + F5E3196D25BDEF50002D1034 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F5E3197F25BDEF53002D1034 /* LaunchScreen.storyboard in Resources */, + F5E31A1325BEA8F8002D1034 /* Color.xcassets in Resources */, + F5E3197C25BDEF53002D1034 /* Assets.xcassets in Resources */, + F5E3197A25BDEF50002D1034 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F5E3198325BDEF53002D1034 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F5E3198E25BDEF53002D1034 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 2D4E87CB829D9C513F5E0AA5 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-NewsAppTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 815B8988ED75800A5FE0F2F7 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-NewsApp-NewsAppUITests/Pods-NewsApp-NewsAppUITests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 8B7CC7C4CFE07ACA35534CF6 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-NewsApp-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + CA98F1FF4376E8BFD1BEA4FF /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-NewsApp-NewsAppUITests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + E47157A87217EA711A250B2B /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-NewsApp/Pods-NewsApp-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-NewsApp/Pods-NewsApp-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-NewsApp/Pods-NewsApp-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + F5E3196B25BDEF50002D1034 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F582F03725C23BA000302CBF /* NewsDetailViewModel.swift in Sources */, + F582EF6525BF76A800302CBF /* NewsService.swift in Sources */, + F582EFB425C04FB400302CBF /* Constants.swift in Sources */, + F5E319E725BE594F002D1034 /* LoginViewController.swift in Sources */, + F582F03D25C2420600302CBF /* NewsDetailWrapper.swift in Sources */, + F5E319C325BE4E74002D1034 /* UIViewController+Storyboardable.swift in Sources */, + F582F02D25C21F2D00302CBF /* ImageCornerRadius.swift in Sources */, + F582EF9525BFC4AE00302CBF /* Bundle+Extension.swift in Sources */, + F582EF8D25BFA58700302CBF /* NewsListElement.swift in Sources */, + F582F03225C23A5C00302CBF /* NewsDetailTableViewController.swift in Sources */, + F5E319A925BDF548002D1034 /* AppCoordinator.swift in Sources */, + F582EF8425BFA25D00302CBF /* Date+Extension.swift in Sources */, + F582EFE925C1356600302CBF /* NewsTableViewCell.swift in Sources */, + F582EFDA25C11FF500302CBF /* Codable+Extension.swift in Sources */, + F5E31A1E25BEAEF4002D1034 /* NewsButton.swift in Sources */, + F582EFE425C1352100302CBF /* NewsListTableViewController.swift in Sources */, + F582EF6A25BF912900302CBF /* NewsProvider.swift in Sources */, + F5E3197325BDEF50002D1034 /* AppDelegate.swift in Sources */, + F5E319BA25BE4E2E002D1034 /* ViewModel.swift in Sources */, + F5E319D125BE5651002D1034 /* UserLogin.swift in Sources */, + F582EFFB25C1463A00302CBF /* LoggedCoordinator.swift in Sources */, + F582F01125C1495500302CBF /* NewsListViewModel.swift in Sources */, + F5E3197525BDEF50002D1034 /* SceneDelegate.swift in Sources */, + F5E31A2725BEB2F9002D1034 /* RoundedImage.swift in Sources */, + F5E319F125BE6DF9002D1034 /* TokenJwt.swift in Sources */, + F582EFC825C080BE00302CBF /* DefaultServerError.swift in Sources */, + F582EFDF25C12AC300302CBF /* UIViewController+Extension.swift in Sources */, + F582EFC325C07F2C00302CBF /* ServiceError+Extension.swift in Sources */, + F5E319EC25BE5A71002D1034 /* LoginViewModel.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F5E3198125BDEF53002D1034 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F5E3198A25BDEF53002D1034 /* NewsAppTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F5E3198C25BDEF53002D1034 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F5E3199525BDEF54002D1034 /* NewsAppUITests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + F5E3198725BDEF53002D1034 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = F5E3196E25BDEF50002D1034 /* NewsApp */; + targetProxy = F5E3198625BDEF53002D1034 /* PBXContainerItemProxy */; + }; + F5E3199225BDEF54002D1034 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = F5E3196E25BDEF50002D1034 /* NewsApp */; + targetProxy = F5E3199125BDEF54002D1034 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + F5E3197825BDEF50002D1034 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + F5E3197925BDEF50002D1034 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + F5E3197D25BDEF53002D1034 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + F5E3197E25BDEF53002D1034 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + F5E3199725BDEF54002D1034 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + F5E3199825BDEF54002D1034 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + F5E3199A25BDEF54002D1034 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 47EF3A55144D896DB8A8209C /* Pods-NewsApp.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 65RHQD8QXH; + INFOPLIST_FILE = NewsApp/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.renefx.NewsApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + F5E3199B25BDEF54002D1034 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 065E37E010A3687594509B07 /* Pods-NewsApp.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 65RHQD8QXH; + INFOPLIST_FILE = NewsApp/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.renefx.NewsApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + F5E3199D25BDEF54002D1034 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = FF4FB029991FAF7116D35CAA /* Pods-NewsAppTests.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 65RHQD8QXH; + INFOPLIST_FILE = NewsAppTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.3; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.renefx.NewsAppTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/NewsApp.app/NewsApp"; + }; + name = Debug; + }; + F5E3199E25BDEF54002D1034 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 78513634FB1CA7FC7ADBA430 /* Pods-NewsAppTests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 65RHQD8QXH; + INFOPLIST_FILE = NewsAppTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.3; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.renefx.NewsAppTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/NewsApp.app/NewsApp"; + }; + name = Release; + }; + F5E319A025BDEF54002D1034 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 8AF3838C628881C64578C1FE /* Pods-NewsApp-NewsAppUITests.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 65RHQD8QXH; + INFOPLIST_FILE = NewsAppUITests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.renefx.NewsAppUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = NewsApp; + }; + name = Debug; + }; + F5E319A125BDEF54002D1034 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 53DB52C778CF4FC908FCD8AF /* Pods-NewsApp-NewsAppUITests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 65RHQD8QXH; + INFOPLIST_FILE = NewsAppUITests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.renefx.NewsAppUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = NewsApp; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + F5E3196A25BDEF50002D1034 /* Build configuration list for PBXProject "NewsApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F5E3199725BDEF54002D1034 /* Debug */, + F5E3199825BDEF54002D1034 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F5E3199925BDEF54002D1034 /* Build configuration list for PBXNativeTarget "NewsApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F5E3199A25BDEF54002D1034 /* Debug */, + F5E3199B25BDEF54002D1034 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F5E3199C25BDEF54002D1034 /* Build configuration list for PBXNativeTarget "NewsAppTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F5E3199D25BDEF54002D1034 /* Debug */, + F5E3199E25BDEF54002D1034 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F5E3199F25BDEF54002D1034 /* Build configuration list for PBXNativeTarget "NewsAppUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F5E319A025BDEF54002D1034 /* Debug */, + F5E319A125BDEF54002D1034 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = F5E3196725BDEF50002D1034 /* Project object */; +} diff --git a/NewsApp.xcworkspace/contents.xcworkspacedata b/NewsApp.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..e82daea --- /dev/null +++ b/NewsApp.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/NewsApp.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/NewsApp.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/NewsApp.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/NewsApp/AppDelegate.swift b/NewsApp/AppDelegate.swift new file mode 100644 index 0000000..7f89266 --- /dev/null +++ b/NewsApp/AppDelegate.swift @@ -0,0 +1,36 @@ +// +// AppDelegate.swift +// NewsApp +// +// Created by Renê Xavier on 24/01/21. +// + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } + + +} + diff --git a/NewsApp/Assets.xcassets/AppIcon.appiconset/100.png b/NewsApp/Assets.xcassets/AppIcon.appiconset/100.png new file mode 100644 index 0000000..9786cfc Binary files /dev/null and b/NewsApp/Assets.xcassets/AppIcon.appiconset/100.png differ diff --git a/NewsApp/Assets.xcassets/AppIcon.appiconset/1024.png b/NewsApp/Assets.xcassets/AppIcon.appiconset/1024.png new file mode 100644 index 0000000..ef4471a Binary files /dev/null and b/NewsApp/Assets.xcassets/AppIcon.appiconset/1024.png differ diff --git a/NewsApp/Assets.xcassets/AppIcon.appiconset/114.png b/NewsApp/Assets.xcassets/AppIcon.appiconset/114.png new file mode 100644 index 0000000..0c68007 Binary files /dev/null and b/NewsApp/Assets.xcassets/AppIcon.appiconset/114.png differ diff --git a/NewsApp/Assets.xcassets/AppIcon.appiconset/120.png b/NewsApp/Assets.xcassets/AppIcon.appiconset/120.png new file mode 100644 index 0000000..39b43f1 Binary files /dev/null and b/NewsApp/Assets.xcassets/AppIcon.appiconset/120.png differ diff --git a/NewsApp/Assets.xcassets/AppIcon.appiconset/128.png b/NewsApp/Assets.xcassets/AppIcon.appiconset/128.png new file mode 100644 index 0000000..151faf9 Binary files /dev/null and b/NewsApp/Assets.xcassets/AppIcon.appiconset/128.png differ diff --git a/NewsApp/Assets.xcassets/AppIcon.appiconset/144.png b/NewsApp/Assets.xcassets/AppIcon.appiconset/144.png new file mode 100644 index 0000000..49f5e75 Binary files /dev/null and b/NewsApp/Assets.xcassets/AppIcon.appiconset/144.png differ diff --git a/NewsApp/Assets.xcassets/AppIcon.appiconset/152.png b/NewsApp/Assets.xcassets/AppIcon.appiconset/152.png new file mode 100644 index 0000000..86a7937 Binary files /dev/null and b/NewsApp/Assets.xcassets/AppIcon.appiconset/152.png differ diff --git a/NewsApp/Assets.xcassets/AppIcon.appiconset/16.png b/NewsApp/Assets.xcassets/AppIcon.appiconset/16.png new file mode 100644 index 0000000..79344aa Binary files /dev/null and b/NewsApp/Assets.xcassets/AppIcon.appiconset/16.png differ diff --git a/NewsApp/Assets.xcassets/AppIcon.appiconset/167.png b/NewsApp/Assets.xcassets/AppIcon.appiconset/167.png new file mode 100644 index 0000000..c742e00 Binary files /dev/null and b/NewsApp/Assets.xcassets/AppIcon.appiconset/167.png differ diff --git a/NewsApp/Assets.xcassets/AppIcon.appiconset/172.png b/NewsApp/Assets.xcassets/AppIcon.appiconset/172.png new file mode 100644 index 0000000..801f750 Binary files /dev/null and b/NewsApp/Assets.xcassets/AppIcon.appiconset/172.png differ diff --git a/NewsApp/Assets.xcassets/AppIcon.appiconset/180.png b/NewsApp/Assets.xcassets/AppIcon.appiconset/180.png new file mode 100644 index 0000000..c541d07 Binary files /dev/null and b/NewsApp/Assets.xcassets/AppIcon.appiconset/180.png differ diff --git a/NewsApp/Assets.xcassets/AppIcon.appiconset/196.png b/NewsApp/Assets.xcassets/AppIcon.appiconset/196.png new file mode 100644 index 0000000..d078703 Binary files /dev/null and b/NewsApp/Assets.xcassets/AppIcon.appiconset/196.png differ diff --git a/NewsApp/Assets.xcassets/AppIcon.appiconset/20.png b/NewsApp/Assets.xcassets/AppIcon.appiconset/20.png new file mode 100644 index 0000000..f0565bd Binary files /dev/null and b/NewsApp/Assets.xcassets/AppIcon.appiconset/20.png differ diff --git a/NewsApp/Assets.xcassets/AppIcon.appiconset/216.png b/NewsApp/Assets.xcassets/AppIcon.appiconset/216.png new file mode 100644 index 0000000..76b9805 Binary files /dev/null and b/NewsApp/Assets.xcassets/AppIcon.appiconset/216.png differ diff --git a/NewsApp/Assets.xcassets/AppIcon.appiconset/256.png b/NewsApp/Assets.xcassets/AppIcon.appiconset/256.png new file mode 100644 index 0000000..53315fc Binary files /dev/null and b/NewsApp/Assets.xcassets/AppIcon.appiconset/256.png differ diff --git a/NewsApp/Assets.xcassets/AppIcon.appiconset/29.png b/NewsApp/Assets.xcassets/AppIcon.appiconset/29.png new file mode 100644 index 0000000..460d769 Binary files /dev/null and b/NewsApp/Assets.xcassets/AppIcon.appiconset/29.png differ diff --git a/NewsApp/Assets.xcassets/AppIcon.appiconset/32.png b/NewsApp/Assets.xcassets/AppIcon.appiconset/32.png new file mode 100644 index 0000000..2f2a641 Binary files /dev/null and b/NewsApp/Assets.xcassets/AppIcon.appiconset/32.png differ diff --git a/NewsApp/Assets.xcassets/AppIcon.appiconset/40.png b/NewsApp/Assets.xcassets/AppIcon.appiconset/40.png new file mode 100644 index 0000000..01b3877 Binary files /dev/null and b/NewsApp/Assets.xcassets/AppIcon.appiconset/40.png differ diff --git a/NewsApp/Assets.xcassets/AppIcon.appiconset/48.png b/NewsApp/Assets.xcassets/AppIcon.appiconset/48.png new file mode 100644 index 0000000..e4a43bd Binary files /dev/null and b/NewsApp/Assets.xcassets/AppIcon.appiconset/48.png differ diff --git a/NewsApp/Assets.xcassets/AppIcon.appiconset/50.png b/NewsApp/Assets.xcassets/AppIcon.appiconset/50.png new file mode 100644 index 0000000..36f5124 Binary files /dev/null and b/NewsApp/Assets.xcassets/AppIcon.appiconset/50.png differ diff --git a/NewsApp/Assets.xcassets/AppIcon.appiconset/512.png b/NewsApp/Assets.xcassets/AppIcon.appiconset/512.png new file mode 100644 index 0000000..8a05b6a Binary files /dev/null and b/NewsApp/Assets.xcassets/AppIcon.appiconset/512.png differ diff --git a/NewsApp/Assets.xcassets/AppIcon.appiconset/55.png b/NewsApp/Assets.xcassets/AppIcon.appiconset/55.png new file mode 100644 index 0000000..8f38808 Binary files /dev/null and b/NewsApp/Assets.xcassets/AppIcon.appiconset/55.png differ diff --git a/NewsApp/Assets.xcassets/AppIcon.appiconset/57.png b/NewsApp/Assets.xcassets/AppIcon.appiconset/57.png new file mode 100644 index 0000000..544a018 Binary files /dev/null and b/NewsApp/Assets.xcassets/AppIcon.appiconset/57.png differ diff --git a/NewsApp/Assets.xcassets/AppIcon.appiconset/58.png b/NewsApp/Assets.xcassets/AppIcon.appiconset/58.png new file mode 100644 index 0000000..e140299 Binary files /dev/null and b/NewsApp/Assets.xcassets/AppIcon.appiconset/58.png differ diff --git a/NewsApp/Assets.xcassets/AppIcon.appiconset/60.png b/NewsApp/Assets.xcassets/AppIcon.appiconset/60.png new file mode 100644 index 0000000..f9c9438 Binary files /dev/null and b/NewsApp/Assets.xcassets/AppIcon.appiconset/60.png differ diff --git a/NewsApp/Assets.xcassets/AppIcon.appiconset/64.png b/NewsApp/Assets.xcassets/AppIcon.appiconset/64.png new file mode 100644 index 0000000..69a8a71 Binary files /dev/null and b/NewsApp/Assets.xcassets/AppIcon.appiconset/64.png differ diff --git a/NewsApp/Assets.xcassets/AppIcon.appiconset/72.png b/NewsApp/Assets.xcassets/AppIcon.appiconset/72.png new file mode 100644 index 0000000..257948a Binary files /dev/null and b/NewsApp/Assets.xcassets/AppIcon.appiconset/72.png differ diff --git a/NewsApp/Assets.xcassets/AppIcon.appiconset/76.png b/NewsApp/Assets.xcassets/AppIcon.appiconset/76.png new file mode 100644 index 0000000..e393b16 Binary files /dev/null and b/NewsApp/Assets.xcassets/AppIcon.appiconset/76.png differ diff --git a/NewsApp/Assets.xcassets/AppIcon.appiconset/80.png b/NewsApp/Assets.xcassets/AppIcon.appiconset/80.png new file mode 100644 index 0000000..7e45296 Binary files /dev/null and b/NewsApp/Assets.xcassets/AppIcon.appiconset/80.png differ diff --git a/NewsApp/Assets.xcassets/AppIcon.appiconset/87.png b/NewsApp/Assets.xcassets/AppIcon.appiconset/87.png new file mode 100644 index 0000000..175e057 Binary files /dev/null and b/NewsApp/Assets.xcassets/AppIcon.appiconset/87.png differ diff --git a/NewsApp/Assets.xcassets/AppIcon.appiconset/88.png b/NewsApp/Assets.xcassets/AppIcon.appiconset/88.png new file mode 100644 index 0000000..1724858 Binary files /dev/null and b/NewsApp/Assets.xcassets/AppIcon.appiconset/88.png differ diff --git a/NewsApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/NewsApp/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..e138c0b --- /dev/null +++ b/NewsApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1 @@ +{"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"72x72","expected-size":"72","filename":"72.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"76x76","expected-size":"152","filename":"152.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"50x50","expected-size":"100","filename":"100.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"76x76","expected-size":"76","filename":"76.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"50x50","expected-size":"50","filename":"50.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"72x72","expected-size":"144","filename":"144.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"40x40","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"83.5x83.5","expected-size":"167","filename":"167.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"20x20","expected-size":"20","filename":"20.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"idiom":"watch","filename":"172.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"38mm","scale":"2x","size":"86x86","expected-size":"172","role":"quickLook"},{"idiom":"watch","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"38mm","scale":"2x","size":"40x40","expected-size":"80","role":"appLauncher"},{"idiom":"watch","filename":"88.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"40mm","scale":"2x","size":"44x44","expected-size":"88","role":"appLauncher"},{"idiom":"watch","filename":"100.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"44mm","scale":"2x","size":"50x50","expected-size":"100","role":"appLauncher"},{"idiom":"watch","filename":"196.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"42mm","scale":"2x","size":"98x98","expected-size":"196","role":"quickLook"},{"idiom":"watch","filename":"216.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"44mm","scale":"2x","size":"108x108","expected-size":"216","role":"quickLook"},{"idiom":"watch","filename":"48.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"38mm","scale":"2x","size":"24x24","expected-size":"48","role":"notificationCenter"},{"idiom":"watch","filename":"55.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"42mm","scale":"2x","size":"27.5x27.5","expected-size":"55","role":"notificationCenter"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"watch","role":"companionSettings","scale":"3x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"watch","role":"companionSettings","scale":"2x"},{"size":"1024x1024","expected-size":"1024","filename":"1024.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"watch-marketing","scale":"1x"},{"size":"128x128","expected-size":"128","filename":"128.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"256x256","expected-size":"256","filename":"256.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"128x128","expected-size":"256","filename":"256.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"256x256","expected-size":"512","filename":"512.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"32","filename":"32.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"512x512","expected-size":"512","filename":"512.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"16","filename":"16.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"32","filename":"32.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"64","filename":"64.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"512x512","expected-size":"1024","filename":"1024.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"}]} \ No newline at end of file diff --git a/NewsApp/Assets.xcassets/Contents.json b/NewsApp/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/NewsApp/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NewsApp/Assets.xcassets/logo.imageset/Contents.json b/NewsApp/Assets.xcassets/logo.imageset/Contents.json new file mode 100644 index 0000000..5f670ca --- /dev/null +++ b/NewsApp/Assets.xcassets/logo.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "logo.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NewsApp/Assets.xcassets/logo.imageset/logo.png b/NewsApp/Assets.xcassets/logo.imageset/logo.png new file mode 100644 index 0000000..12528d3 Binary files /dev/null and b/NewsApp/Assets.xcassets/logo.imageset/logo.png differ diff --git a/NewsApp/Assets.xcassets/logoRounded.imageset/Contents.json b/NewsApp/Assets.xcassets/logoRounded.imageset/Contents.json new file mode 100644 index 0000000..1a80528 --- /dev/null +++ b/NewsApp/Assets.xcassets/logoRounded.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "logoRounded.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NewsApp/Assets.xcassets/logoRounded.imageset/logoRounded.png b/NewsApp/Assets.xcassets/logoRounded.imageset/logoRounded.png new file mode 100644 index 0000000..c1cdeac Binary files /dev/null and b/NewsApp/Assets.xcassets/logoRounded.imageset/logoRounded.png differ diff --git a/NewsApp/Assets.xcassets/user.imageset/Contents.json b/NewsApp/Assets.xcassets/user.imageset/Contents.json new file mode 100644 index 0000000..21d8882 --- /dev/null +++ b/NewsApp/Assets.xcassets/user.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "user.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NewsApp/Assets.xcassets/user.imageset/user.png b/NewsApp/Assets.xcassets/user.imageset/user.png new file mode 100644 index 0000000..b520519 Binary files /dev/null and b/NewsApp/Assets.xcassets/user.imageset/user.png differ diff --git a/NewsApp/Base.lproj/LaunchScreen.storyboard b/NewsApp/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..3e5c209 --- /dev/null +++ b/NewsApp/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/NewsApp/Color.xcassets/Accent.colorset/Contents.json b/NewsApp/Color.xcassets/Accent.colorset/Contents.json new file mode 100644 index 0000000..1ceab1c --- /dev/null +++ b/NewsApp/Color.xcassets/Accent.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.894", + "green" : "0.420", + "red" : "0.016" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NewsApp/Color.xcassets/Contents.json b/NewsApp/Color.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/NewsApp/Color.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NewsApp/Color.xcassets/Primary Dark.colorset/Contents.json b/NewsApp/Color.xcassets/Primary Dark.colorset/Contents.json new file mode 100644 index 0000000..52ed902 --- /dev/null +++ b/NewsApp/Color.xcassets/Primary Dark.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "162", + "green" : "30", + "red" : "15" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.584", + "green" : "0.500", + "red" : "0.059" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NewsApp/Color.xcassets/Primary.colorset/Contents.json b/NewsApp/Color.xcassets/Primary.colorset/Contents.json new file mode 100644 index 0000000..b747911 --- /dev/null +++ b/NewsApp/Color.xcassets/Primary.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xE4", + "green" : "0x6B", + "red" : "0x04" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x95", + "green" : "0x0F", + "red" : "0x0F" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NewsApp/Color.xcassets/PrimaryWhite.colorset/Contents.json b/NewsApp/Color.xcassets/PrimaryWhite.colorset/Contents.json new file mode 100644 index 0000000..b5224b1 --- /dev/null +++ b/NewsApp/Color.xcassets/PrimaryWhite.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xE4", + "green" : "0x6B", + "red" : "0x04" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NewsApp/Color.xcassets/Secondary.colorset/Contents.json b/NewsApp/Color.xcassets/Secondary.colorset/Contents.json new file mode 100644 index 0000000..6ebd2c3 --- /dev/null +++ b/NewsApp/Color.xcassets/Secondary.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x0F", + "green" : "0x0F", + "red" : "0x0F" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NewsApp/Color.xcassets/WhiteBlack.colorset/Contents.json b/NewsApp/Color.xcassets/WhiteBlack.colorset/Contents.json new file mode 100644 index 0000000..2960d72 --- /dev/null +++ b/NewsApp/Color.xcassets/WhiteBlack.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x00", + "green" : "0x00", + "red" : "0x00" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NewsApp/Color.xcassets/WhiteWhite.colorset/Contents.json b/NewsApp/Color.xcassets/WhiteWhite.colorset/Contents.json new file mode 100644 index 0000000..22c4bb0 --- /dev/null +++ b/NewsApp/Color.xcassets/WhiteWhite.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NewsApp/Components/ImageCornerRadius.swift b/NewsApp/Components/ImageCornerRadius.swift new file mode 100644 index 0000000..ece8008 --- /dev/null +++ b/NewsApp/Components/ImageCornerRadius.swift @@ -0,0 +1,22 @@ +// +// ImageCornerRadius.swift +// NewsApp +// +// Created by Renê Xavier on 27/01/21. +// + +import Foundation +import UIKit + +class ImageCornerRadius: UIImageView { + + override func awakeFromNib() { + super.awakeFromNib() + } + + override func layoutSubviews(){ + super.layoutSubviews() + layer.cornerRadius = 6 + clipsToBounds = true + } +} diff --git a/NewsApp/Components/NewsButton.swift b/NewsApp/Components/NewsButton.swift new file mode 100644 index 0000000..8233dee --- /dev/null +++ b/NewsApp/Components/NewsButton.swift @@ -0,0 +1,24 @@ +// +// NewsButton.swift +// NewsApp +// +// Created by Renê Xavier on 25/01/21. +// + +import UIKit + +class NewsButton: UIButton { + + override func awakeFromNib() { + super.awakeFromNib() + } + + + override func layoutSubviews(){ + super.layoutSubviews() + layer.cornerRadius = 8 + backgroundColor = UIColor(named: "Primary") + titleLabel?.font.withSize(20.0) + setTitleColor(UIColor(named: "Accent"), for: .normal) + } +} diff --git a/NewsApp/Components/RoundedImage.swift b/NewsApp/Components/RoundedImage.swift new file mode 100644 index 0000000..4a68655 --- /dev/null +++ b/NewsApp/Components/RoundedImage.swift @@ -0,0 +1,21 @@ +// +// RoundedImage.swift +// NewsApp +// +// Created by Renê Xavier on 25/01/21. +// + +import UIKit + +class RoundedImage: UIImageView { + + override func awakeFromNib() { + super.awakeFromNib() + } + + override func layoutSubviews(){ + super.layoutSubviews() + layer.cornerRadius = self.frame.height / 2 + clipsToBounds = true + } +} diff --git a/NewsApp/Info.plist b/NewsApp/Info.plist new file mode 100644 index 0000000..320db73 --- /dev/null +++ b/NewsApp/Info.plist @@ -0,0 +1,64 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + News App + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + + + + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/NewsApp/SceneDelegate.swift b/NewsApp/SceneDelegate.swift new file mode 100644 index 0000000..1e8b569 --- /dev/null +++ b/NewsApp/SceneDelegate.swift @@ -0,0 +1,24 @@ +// +// SceneDelegate.swift +// NewsApp +// +// Created by Renê Xavier on 24/01/21. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + // created so it won`t loose the refence of the coordinator + var appCoordinator: AppCoordinator? + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + guard let scene = (scene as? UIWindowScene) else { return } + + let window = UIWindow(windowScene: scene) + appCoordinator = AppCoordinator(window: window) + self.window = window + appCoordinator?.start() + } +} diff --git a/NewsApp/Scenes/AppCoordinator.swift b/NewsApp/Scenes/AppCoordinator.swift new file mode 100644 index 0000000..2c3e53b --- /dev/null +++ b/NewsApp/Scenes/AppCoordinator.swift @@ -0,0 +1,111 @@ +// +// AppCoordinator.swift +// NewsApp +// +// Created by Renê Xavier on 24/01/21. +// + +import RxSwift +import ProgressHUD + +protocol Coordinator { + var childCoordinators: [Coordinator] { get } + func start() +} + +final class AppCoordinator: Coordinator { + private(set) var childCoordinators: [Coordinator] = [] + private let window: UIWindow + var navigation: UINavigationController? + + let disposeBagViewModel = DisposeBag() + var loadingView: UIView? + var currentViewController = UIViewController() + + private let controlLoading = PublishSubject() + + init(window: UIWindow) { + self.window = window + } + + func start () { + currentViewController = prepareLoginViewController() + navigation = UINavigationController(rootViewController: currentViewController) + + navigation?.navigationBar.barTintColor = UIColor(named: "Primary") + navigation?.navigationBar.titleTextAttributes = [.foregroundColor: UIColor(named: "WhiteWhite")] + UIBarButtonItem.appearance().tintColor = UIColor(named: "WhiteWhite") + + window.rootViewController = navigation + window.makeKeyAndVisible() + + setLoadingAnimation() + } + + func prepareLoginViewController() -> LoginViewController { + var rootViewController = LoginViewController.instantiate() + let newsService = NewsService() + var viewModel = LoginViewModel(newsService, disposeBag: disposeBagViewModel) + rootViewController.disposeBag = disposeBagViewModel + rootViewController.bind(to: &viewModel) + self.setLoginActions(viewModel) + + return rootViewController + } + + func setLoadingAnimation() { + let frame = currentViewController.view.frame + loadingView = UIView(frame: frame) + loadingView?.backgroundColor = Constants.loadingBackgroundColor + + ProgressHUD.animationType = .singleCirclePulse + ProgressHUD.colorBackground = Constants.loadingBackgroundColor + ProgressHUD.colorAnimation = Constants.loadingColor + } + + func setLoginActions(_ viewModel: LoginViewModel) { + viewModel.actions.loginExecution + .subscribe(onNext: { [weak self] (object) in + guard let selfNotOptional = self, + let objectNotNil = object, + let token = (objectNotNil as? TokenJwt)?.token else { + self?.currentViewController.presentDefaultAlert(object as? DefaultServerError) + return + } + selfNotOptional.navigateLoggedArea(token) + }).disposed(by: disposeBagViewModel) + + viewModel.actions.loginFailFillField + .drive(onNext: { [weak self] (_) in + self?.window.rootViewController?.presentedViewController?.presentDefaultAlert(message: "Preencha os campos de login") + }).disposed(by: disposeBagViewModel) + + viewModel.actions.loading + .drive(onNext: { [weak self] (value) in + if let loadingView = self?.loadingView { + if value { + ProgressHUD.show() + self?.currentViewController.view.addSubview(loadingView) + } else { + ProgressHUD.dismiss() + loadingView.removeFromSuperview() + } + } + }).disposed(by: disposeBagViewModel) + } + + func navigateLoggedArea(_ token: String) { + NewsProvider.tokenJwt = token + let loggedCoordinator = LoggedCoordinator(navigation: self.navigation) + self.childCoordinators.append(loggedCoordinator) + loggedCoordinator.logoutCoordinatorAction + .subscribe(onNext: { [weak self] (_) in + guard let self = self else { + return + } + self.currentViewController = self.prepareLoginViewController() + self.navigation?.setViewControllers([self.currentViewController], animated: true) + }).disposed(by: disposeBagViewModel) + loggedCoordinator.start() + } +} diff --git a/NewsApp/Scenes/Base.lproj/Main.storyboard b/NewsApp/Scenes/Base.lproj/Main.storyboard new file mode 100644 index 0000000..18e7065 --- /dev/null +++ b/NewsApp/Scenes/Base.lproj/Main.storyboard @@ -0,0 +1,514 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/NewsApp/Scenes/Logged Area/LoggedCoordinator.swift b/NewsApp/Scenes/Logged Area/LoggedCoordinator.swift new file mode 100644 index 0000000..0bd84b9 --- /dev/null +++ b/NewsApp/Scenes/Logged Area/LoggedCoordinator.swift @@ -0,0 +1,102 @@ +// +// LoggedCoordinator.swift +// NewsApp +// +// Created by Renê Xavier on 27/01/21. +// + +import Foundation +import RxSwift +import Kingfisher + +final class LoggedCoordinator: Coordinator { + private(set) var childCoordinators: [Coordinator] = [] + private let navigation: UINavigationController? + + let disposeBagViewModel = DisposeBag() + let newsService = NewsService() + let logoutCoordinatorAction = PublishSubject() + + init(navigation: UINavigationController?) { + self.navigation = navigation + } + + func start () { + var newsListViewController = NewsListTableViewController.instantiate() + var viewModel = NewsListViewModel(newsService) + newsListViewController.disposeBag = disposeBagViewModel + newsListViewController.bind(to: &viewModel) + self.setListNewsActions(viewModel) + + navigation?.setViewControllers([newsListViewController], animated: true) + } + + func setListNewsActions(_ viewModel: NewsListViewModel) { + viewModel.actions.logout + .bind { [weak self] (_) in + self?.logoutCoordinatorAction.onNext(()) + }.disposed(by: disposeBagViewModel) + + viewModel.actions.navigateScreenDetail + .subscribe { [weak self] (idNews) in + self?.navigateDetail(idNews) + }.disposed(by: disposeBagViewModel) + } + + func navigateDetail(_ idNews: String?) { + guard let idNews = idNews, let navigationController = navigation else { + navigation?.viewControllers.first?.presentDefaultError(Constants.unknownError) + return + } + var newsDetailViewController = NewsDetailTableViewController.instantiate() + var viewModel = NewsDetailViewModel(newsService, idNews) + newsDetailViewController.disposeBag = disposeBagViewModel + newsDetailViewController.bind(to: &viewModel) + self.setDetailNewsActions(viewModel) + + navigationController.pushViewController(newsDetailViewController, animated: true) + } + + func setDetailNewsActions(_ viewModel: NewsDetailViewModel) { + viewModel.actions.shareNews + .subscribe(onNext: { [weak self] (elementNews) in + guard let elementNews = elementNews else { + self?.navigation?.presentDefaultAlert(message: Constants.notFound) + return + } + let resource = ImageResource(downloadURL: elementNews.urlFormatted!) + KingfisherManager.shared + .retrieveImage(with: resource, completionHandler: { (result) in + let image = try? result.get().image + self?.shareNewsAction(elementNews, image) + }) + }).disposed(by: disposeBagViewModel) + } + + func shareNewsAction(_ document: Documento?, _ image: UIImage?) { + let firstActivityItem = "Veja essa notícia: \n\(document?.titulo ?? "")" + let secondActivityItem : NSURL = NSURL(string: document?.url ?? "")! + + let activityViewController : UIActivityViewController = UIActivityViewController( + activityItems: [firstActivityItem, secondActivityItem, image], applicationActivities: nil) + + activityViewController.activityItemsConfiguration = [ + UIActivity.ActivityType.message + ] as? UIActivityItemsConfigurationReading + + activityViewController.excludedActivityTypes = [ + UIActivity.ActivityType.postToWeibo, + UIActivity.ActivityType.print, + UIActivity.ActivityType.assignToContact, + UIActivity.ActivityType.saveToCameraRoll, + UIActivity.ActivityType.addToReadingList, + UIActivity.ActivityType.postToFlickr, + UIActivity.ActivityType.postToVimeo, + UIActivity.ActivityType.postToTencentWeibo, + UIActivity.ActivityType.postToFacebook + ] + + activityViewController.isModalInPresentation = true + self.navigation?.present(activityViewController, animated: true, completion: nil) + } +} diff --git a/NewsApp/Scenes/Logged Area/News Detail/Model/NewsDetailWrapper.swift b/NewsApp/Scenes/Logged Area/News Detail/Model/NewsDetailWrapper.swift new file mode 100644 index 0000000..200844c --- /dev/null +++ b/NewsApp/Scenes/Logged Area/News Detail/Model/NewsDetailWrapper.swift @@ -0,0 +1,40 @@ +// +// NewsDetailWrapper.swift +// NewsApp +// +// Created by Renê Xavier on 27/01/21. +// + +import Foundation + +// MARK: - NewsDetailWrapperElement +struct NewsDetailWrapperElement: Codable { + var documento: Documento? +} + +// MARK: - Documento +struct Documento: Codable { + var url: String? + var source, produto, editoria, subeditoria: String? + var titulo, credito, datapub, horapub: String? + var linhafina: String? + var imagem, thumbnail: String? + var creditoImagem, legendaImagem, origem, id: String? + var corpoformatado: String? + + var urlFormatted: URL? { + return URL(string: imagem ?? "") + } + + var creditoImagemFormatted: String { + return "(Imagem: \(creditoImagem ?? ""))" + } + + var creditoFormatted: String { + return "Por: \(credito ?? "")" + } + + var dataFormatted: String { + return "\(datapub ?? "") \(horapub ?? "")" + } +} diff --git a/NewsApp/Scenes/Logged Area/News Detail/NewsDetailTableViewController.swift b/NewsApp/Scenes/Logged Area/News Detail/NewsDetailTableViewController.swift new file mode 100644 index 0000000..a8930c3 --- /dev/null +++ b/NewsApp/Scenes/Logged Area/News Detail/NewsDetailTableViewController.swift @@ -0,0 +1,104 @@ +// +// NewsDetailTableViewController.swift +// NewsApp +// +// Created by Renê Xavier on 27/01/21. +// + +import UIKit +import RxSwift +import RxCocoa +import markymark +import SkeletonView + +class NewsDetailTableViewController: UITableViewController, ViewModelBased { + @IBOutlet weak var imageBig: UIImageView! + @IBOutlet weak var authorImage: UILabel! + @IBOutlet weak var titleNews: UILabel! + @IBOutlet weak var authorText: UILabel! + @IBOutlet weak var originText: UILabel! + @IBOutlet weak var dateTimeNews: UILabel! + @IBOutlet weak var imageSubtitleMarkdown: MarkDownTextView! + @IBOutlet weak var bodyMarkdown: MarkDownTextView! + + + var viewModel: NewsDetailViewModel! + let barButton = UIBarButtonItem(barButtonSystemItem: .action, target: nil, action: nil) + + weak var disposeBag: DisposeBag! + + override func viewDidLoad() { + super.viewDidLoad() + self.title = "News App" + self.navigationItem.rightBarButtonItem = barButton + } + + func getInputsViewModel() -> NewsDetailInputs { + return NewsDetailInputs(tapShareNews: + barButton.rx + .tap.asObservable(), + loadServerData: .just(()), + reloadServerData: + refreshControl!.rx + .controlEvent(.valueChanged) + .asObservable()) + } + + func bindUI() { + viewModel.output.loading + .drive (onNext: { [weak self] (showLoading) in + if showLoading { + self?.tableView.refreshControl?.beginRefreshing() + self?.showSkeleton() + } else { + self?.tableView.refreshControl?.endRefreshing() + self?.hideSkeleton() + } + }).disposed(by: disposeBag) + + viewModel.output.apiCallResult + .drive(onNext: { [weak self] (document) in + self?.fillElements(document) + }).disposed(by: disposeBag) + } + + func fillElements(_ documento: Documento?) { + guard let document = documento else { + self.presentDefaultError(Constants.notFound) { [weak self] (_) in + self?.navigationController?.popViewController(animated: true) + } + return + } + self.title = document.editoria + self.imageBig.kf.setImage(with: document.urlFormatted) + self.authorImage.text = document.creditoImagemFormatted + self.titleNews.text = document.titulo + self.authorText.text = document.creditoFormatted + self.originText.text = document.source + self.dateTimeNews.text = document.dataFormatted + self.imageSubtitleMarkdown.text = document.legendaImagem + self.bodyMarkdown.text = document.corpoformatado + + } + + func showSkeleton() { + self.imageBig.showAnimatedGradientSkeleton() + self.authorImage.showAnimatedGradientSkeleton() + self.titleNews.showAnimatedGradientSkeleton() + self.authorText.showAnimatedGradientSkeleton() + self.originText.showAnimatedGradientSkeleton() + self.dateTimeNews.showAnimatedGradientSkeleton() + self.imageSubtitleMarkdown.showAnimatedGradientSkeleton() + + } + + func hideSkeleton() { + self.imageBig.hideSkeleton() + self.authorImage.hideSkeleton() + self.titleNews.hideSkeleton() + self.authorText.hideSkeleton() + self.originText.hideSkeleton() + self.dateTimeNews.hideSkeleton() + self.imageSubtitleMarkdown.hideSkeleton() + } +} diff --git a/NewsApp/Scenes/Logged Area/News Detail/NewsDetailViewModel.swift b/NewsApp/Scenes/Logged Area/News Detail/NewsDetailViewModel.swift new file mode 100644 index 0000000..6faa186 --- /dev/null +++ b/NewsApp/Scenes/Logged Area/News Detail/NewsDetailViewModel.swift @@ -0,0 +1,83 @@ +// +// NewsDetailViewModel.swift +// NewsApp +// +// Created by Renê Xavier on 27/01/21. +// + +import Foundation +import RxSwift +import RxCocoa +import Moya + +struct NewsDetailInputs: Inputs { + var tapShareNews: Observable + var loadServerData: Observable + var reloadServerData: Observable +} + +struct NewsDetailViewModelOutputs: Outputs { + var loading: Driver + var apiCallResult: Driver +} + +struct NewsDetailActions: Actions { + var shareNews: Observable +} + +class NewsDetailViewModel: ViewModel { + public var output: NewsDetailViewModelOutputs! + public var actions: NewsDetailActions! + + private let disposeBag = DisposeBag() + private let apiCallResult = PublishSubject() + private let isLoading = BehaviorSubject(value: false) + private let service: NewsService + private let idNews: String + + init(_ service: NewsService, _ idNews: String) { + self.service = service + self.idNews = idNews + } + + func setUp(with input: NewsDetailInputs) { + + let shareNews = input.tapShareNews + .withLatestFrom(apiCallResult) + + input.loadServerData + .bind (onNext: { [weak self] (_) in + self?.fetchNewsDetail() + }).disposed(by: disposeBag) + + input.reloadServerData + .subscribe(onNext: { [weak self] _ in + self?.fetchNewsDetail() + }) + .disposed(by: disposeBag) + + actions = NewsDetailActions(shareNews: shareNews) + + output = NewsDetailViewModelOutputs(loading: isLoading.asDriver(onErrorJustReturn: false), + apiCallResult: apiCallResult.asDriver(onErrorJustReturn: nil)) + } + + func fetchNewsDetail() { + self.isLoading.onNext(true) + self.service.fetchNewsDetail(newsId: self.idNews) + .subscribe(onSuccess: { [weak self] (response) in + print("aliiii") + self?.isLoading.onNext(false) + do { + let object = try JSONDecoder.decode(data: response.data, to: [NewsDetailWrapperElement].self) + self?.apiCallResult.onNext(object.first?.documento) + } catch (let erro) { + self?.apiCallResult.onError(erro) + } + }, onError: { [weak self] (erro) in + self?.isLoading.onNext(false) + self?.apiCallResult.onError(erro) + }).disposed(by: self.disposeBag) + } + +} diff --git a/NewsApp/Scenes/Logged Area/NewsList/Model/NewsListElement.swift b/NewsApp/Scenes/Logged Area/NewsList/Model/NewsListElement.swift new file mode 100644 index 0000000..12b34cd --- /dev/null +++ b/NewsApp/Scenes/Logged Area/NewsList/Model/NewsListElement.swift @@ -0,0 +1,66 @@ +// +// NewsListElement.swift +// NewsApp +// +// Created by Renê Xavier on 25/01/21. +// + +import Foundation + +// MARK: - NewsListElement +struct NewsListElement: Codable { + var idDocumento, area, title, subtitle: String? + var date, time: String? + var url: URL? + var image: URL? + var source, author, authorImage: String? + var mock:Bool = false + + + enum CodingKeys: String, CodingKey { + case idDocumento = "id_documento" + case area = "chapeu" + case title = "titulo" + case subtitle = "linha_fina" + case date = "data_hora_publicacao" + case time, url, source + case author = "credito" + case image = "imagem" + case authorImage = "imagem_credito" + } + + init(mock: Bool) { + self.mock = mock + } + + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + idDocumento = try values.decodeIfPresent(String.self, forKey: .idDocumento) + area = try values.decodeIfPresent(String.self, forKey: .area) + title = try values.decodeIfPresent(String.self, forKey: .title) + subtitle = try values.decodeIfPresent(String.self, forKey: .subtitle) + if let dateTime = try values.decodeIfPresent(String.self, forKey: .date) { + date = Date.dateFormatter(date: dateTime) + time = Date.timeFormatter(date: dateTime) + } else { + date = nil + time = nil + } + source = try values.decodeIfPresent(String.self, forKey: .source) + author = try values.decodeIfPresent(String.self, forKey: .author) + authorImage = try values.decodeIfPresent(String.self, forKey: .authorImage) + + if let urlNews = try values.decodeIfPresent(String.self, forKey: .url) { + url = URL(string: urlNews) + } else { + url = nil + } + + if let urlImage = try values.decodeIfPresent(String.self, forKey: .image) { + image = URL(string: urlImage) + } else { + image = nil + } + + } +} diff --git a/NewsApp/Scenes/Logged Area/NewsList/NewsListTableViewController.swift b/NewsApp/Scenes/Logged Area/NewsList/NewsListTableViewController.swift new file mode 100644 index 0000000..36c58cf --- /dev/null +++ b/NewsApp/Scenes/Logged Area/NewsList/NewsListTableViewController.swift @@ -0,0 +1,90 @@ +// +// NewsListTableViewController.swift +// NewsApp +// +// Created by Renê Xavier on 27/01/21. +// + +import UIKit +import RxSwift +import RxCocoa +import SkeletonView + +class NewsListTableViewController: UITableViewController, ViewModelBased { + var viewModel: NewsListViewModel! + let loadServerData = PublishSubject() + let barButton = UIBarButtonItem(title: "Sair", style: .plain, target: nil, action: nil) + + weak var disposeBag: DisposeBag! + + override func viewDidLoad() { + super.viewDidLoad() + tableView.delegate = self + tableView.estimatedRowHeight = 200 + self.title = "News App" + self.navigationItem.rightBarButtonItem = barButton + viewModel.fetchNews() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationController?.setNavigationBarHidden(false, animated: true) + } + + func getInputsViewModel() -> NewsListInputs { + self.tableView.delegate = nil + self.tableView.dataSource = nil + + return NewsListInputs(tapLogout: + barButton.rx + .tap.asObservable(), + tapNavigateScreenDetail: + tableView + .rx + .modelSelected(NewsListElement.self) + .asObservable(), + loadServerData: .just(()), + reloadServerData: + refreshControl! + .rx + .controlEvent(.valueChanged) + .asObservable()) + } + + func bindUI() { + viewModel.output.loading + .drive (onNext: { [weak self] (showLoading) in + if showLoading { + self?.tableView.refreshControl?.beginRefreshing() + } else { + self?.tableView.refreshControl?.endRefreshing() + } + }).disposed(by: disposeBag) + + viewModel.output.reloadTableViewWith + .drive(tableView.rx.items(cellIdentifier: "newsTableViewCell", + cellType: NewsTableViewCell.self)) + { (index, cellViewModel, cell) in + if cellViewModel.mock { + cell.showSkeleton() + return + } + cell.hideSkeleton() + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250)) { + cell.configure(cellViewModel) + } + }.disposed(by: disposeBag) + + } + + override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return UITableView.automaticDimension + } + +} + +extension NewsListTableViewController: SkeletonTableViewDataSource { + func collectionSkeletonView(_ skeletonView: UITableView, cellIdentifierForRowAt indexPath: IndexPath) -> ReusableCellIdentifier { + return "newsTableViewCell" + } +} diff --git a/NewsApp/Scenes/Logged Area/NewsList/NewsListViewModel.swift b/NewsApp/Scenes/Logged Area/NewsList/NewsListViewModel.swift new file mode 100644 index 0000000..bb38ff6 --- /dev/null +++ b/NewsApp/Scenes/Logged Area/NewsList/NewsListViewModel.swift @@ -0,0 +1,99 @@ +// +// NewsListViewModel.swift +// NewsApp +// +// Created by Renê Xavier on 27/01/21. +// + +import Foundation +import RxSwift +import RxCocoa +import Moya + +struct NewsListInputs: Inputs { + var tapLogout: Observable + var tapNavigateScreenDetail: Observable + var loadServerData: Observable + var reloadServerData: Observable +} + +struct NewsListViewModelOutputs: Outputs { + var loading: Driver + var reloadTableViewWith: Driver<[NewsListElement]> +} + +struct NewsListActions: Actions { + var logout: Observable + var navigateScreenDetail: Observable +} + +class NewsListViewModel: ViewModel { + public var output: NewsListViewModelOutputs! + public var actions: NewsListActions! + + private let disposeBag = DisposeBag() + private let apiCallResult = BehaviorSubject<[NewsListElement]>(value: []) + private let isLoading = BehaviorSubject(value: false) + private let service: NewsService + + init(_ service: NewsService) { + self.service = service + } + + func setUp(with input: NewsListInputs) { + + let idScreendDetail = input.tapNavigateScreenDetail + .map { (element) -> String? in + element.idDocumento + } + + let mockLoadingResult = Observable.from(optional: isLoading) + .filter({ (isLoading) -> Bool in + (try? isLoading.value()) ?? false + }) + .flatMapFirst({ (value) -> Observable<[NewsListElement]> in + var arrayOfMocks = [NewsListElement]() + arrayOfMocks.append(NewsListElement(mock: true)) + arrayOfMocks.append(NewsListElement(mock: true)) + arrayOfMocks.append(NewsListElement(mock: true)) + return Observable.of(arrayOfMocks) + }) + .share() + + input.reloadServerData + .subscribe(onNext: { [weak self] _ in + self?.fetchNews() + }) + .disposed(by: disposeBag) + + let reloadTableViewWith = Observable.combineLatest(apiCallResult, mockLoadingResult, isLoading) + .flatMapLatest { (api, mock, isLoading) -> Observable<[NewsListElement]> in + if isLoading { + return Observable.of(mock) + } else { + return Observable.of(api) + } + } + actions = NewsListActions(logout: input.tapLogout, navigateScreenDetail: idScreendDetail) + + output = NewsListViewModelOutputs(loading: isLoading.asDriver(onErrorJustReturn: false), + reloadTableViewWith: reloadTableViewWith.asDriver(onErrorJustReturn: [])) + } + + func fetchNews() { + self.isLoading.onNext(true) + self.service.fetchNews().subscribe { [weak self] (response) in + self?.isLoading.onNext(false) + do { + let object = try JSONDecoder.decode(data: response.data, to: [NewsListElement].self) + self?.apiCallResult.onNext(object) + } catch (let erro) { + self?.apiCallResult.onError(erro) + } + } onError: { [weak self] (erro) in + self?.isLoading.onNext(false) + self?.apiCallResult.onError(erro) + }.disposed(by: self.disposeBag) + } + +} diff --git a/NewsApp/Scenes/Logged Area/NewsList/NewsTableViewCell.swift b/NewsApp/Scenes/Logged Area/NewsList/NewsTableViewCell.swift new file mode 100644 index 0000000..49c13e2 --- /dev/null +++ b/NewsApp/Scenes/Logged Area/NewsList/NewsTableViewCell.swift @@ -0,0 +1,36 @@ +// +// NewsTableViewCell.swift +// NewsApp +// +// Created by Renê Xavier on 27/01/21. +// + +import UIKit +import Kingfisher +import SkeletonView + +class NewsTableViewCell: UITableViewCell { + + @IBOutlet weak var newsImage: UIImageView! + @IBOutlet weak var title: UILabel! + @IBOutlet weak var subtitle: UILabel! + + func configure (_ viewModel: NewsListElement) { + self.newsImage.kf.setImage(with: viewModel.image) + self.title.text = viewModel.title + self.subtitle.text = viewModel.subtitle + } + + func showSkeleton() { + newsImage.showAnimatedGradientSkeleton() + title.showAnimatedGradientSkeleton() + subtitle.showAnimatedGradientSkeleton() + } + + func hideSkeleton() { + newsImage.hideSkeleton() + title.hideSkeleton() + subtitle.hideSkeleton() + } + +} diff --git a/NewsApp/Scenes/Login/LoginViewController.swift b/NewsApp/Scenes/Login/LoginViewController.swift new file mode 100644 index 0000000..299584b --- /dev/null +++ b/NewsApp/Scenes/Login/LoginViewController.swift @@ -0,0 +1,53 @@ +// +// LoginViewController.swift +// NewsApp +// +// Created by Renê Xavier on 24/01/21. +// + +import UIKit +import RxSwift +import RxCocoa + +class LoginViewController: UIViewController, ViewModelBased { + weak var viewModel: LoginViewModel! + + weak var disposeBag: DisposeBag! + + @IBOutlet weak var usernameLabel: UITextField! + @IBOutlet weak var passwordLabel: UITextField! + @IBOutlet weak var loginButton: UIButton! + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationController?.setNavigationBarHidden(true, animated: false) + } + + func getInputsViewModel() -> LoginInputs { + return LoginInputs(username: usernameLabel.rx.text.orEmpty.asObservable(), + password: passwordLabel.rx.text.orEmpty.asObservable(), + tapExecuteLogin: loginButton.rx.tap.asObservable()) + } + + func bindUI() { + + viewModel.output.isLoginValid + .drive(loginButton.rx.isEnabled) + .disposed(by: disposeBag) + + viewModel.output.alphaButton + .asDriver() + .drive(loginButton.rx.alpha) + .disposed(by: disposeBag) + + viewModel.output.animateButtonEvent + .drive(onNext: { [weak self] (_) in + self?.loginButton.transform = CGAffineTransform(scaleX: 1.2, y: 1.2) + UIView.animate(withDuration: 0.2) { + self?.loginButton.transform = .identity + } + }) + .disposed(by: disposeBag) + } + +} diff --git a/NewsApp/Scenes/Login/LoginViewModel.swift b/NewsApp/Scenes/Login/LoginViewModel.swift new file mode 100644 index 0000000..94fd5b0 --- /dev/null +++ b/NewsApp/Scenes/Login/LoginViewModel.swift @@ -0,0 +1,119 @@ +// +// LoginViewModel.swift +// NewsApp +// +// Created by Renê Xavier on 24/01/21. +// + +import Foundation +import RxSwift +import RxCocoa +import Moya + +struct LoginInputs: Inputs { + var username: Observable + var password: Observable + var tapExecuteLogin: Observable +} + +struct LoginViewModelOutputs: Outputs { + var isLoginValid: Driver + var alphaButton: Driver + var animateButtonEvent: Driver +} + +struct LoginActions: Actions { + var loginExecution: Observable + var loginFailFillField: Driver + var loading: Driver +} + +class LoginViewModel: ViewModel { + public var output: LoginViewModelOutputs! + public var actions: LoginActions! + + var disposeBag: DisposeBag + let apiCallResult = PublishSubject() + let isLoading = BehaviorSubject(value: false) + let service: NewsService + + init(_ service: NewsService, disposeBag: DisposeBag) { + self.service = service + self.disposeBag = disposeBag + } + + func setUp(with input: LoginInputs) { + let usernameValid = input.username + .map { (username) in + return username.count > 0 + } + .startWith(false) + .share() + + let passwordValid = input.password + .map { (username) in + return username.count > 0 + } + .startWith(false) + .share() + + let isLoginValid = Observable + .combineLatest(usernameValid, passwordValid, isLoading) + .map { (usernameValid, passwordValid, isLoading) in + return usernameValid && passwordValid && !isLoading + }.startWith(false) + .share() + + let alphaButton = isLoginValid + .map({ $0 ? CGFloat(1) : CGFloat(0.1)}) + .startWith(CGFloat(0.1)) + .asDriver(onErrorJustReturn: 0.1) + + let animateButtonEvent = isLoginValid + .distinctUntilChanged() + .filter({ $0 }) + .asDriver(onErrorJustReturn: true) + + let loginExecution = input.tapExecuteLogin + .throttle(.milliseconds(500), scheduler: MainScheduler.instance) + .withLatestFrom(isLoginValid) + .share() + + let loginFailFillField = loginExecution + .filter { !$0 } + .map({ _ in Void() }) + .asDriver(onErrorJustReturn: Void()) + + + _ = loginExecution + .withLatestFrom(isLoginValid) + .filter { $0 } + .withLatestFrom(input.username) + .withLatestFrom(input.password, resultSelector: { (username, password) -> UserLogin in +// UserLogin(username: username, password: password) + UserLogin(username: "devmobile", password: "uC&+}H4wg?rYbF:") + }) + .subscribe(onNext: { (user) in + self.isLoading.onNext(true) + self.service.postNewsLogin(user: user).filterSuccess().subscribe { [weak self] (response) in + self?.isLoading.onNext(false) + let object = try? JSONDecoder.decode(data: response.data, to: TokenJwt.self) + self?.apiCallResult.onNext(object) + } onError: { [weak self] (erro) in + self?.isLoading.onNext(false) + self?.apiCallResult.onNext(erro) + }.disposed(by: self.disposeBag) + }).disposed(by: disposeBag) + + actions = LoginActions(loginExecution: apiCallResult, + loginFailFillField: loginFailFillField, + loading: isLoading.asDriver(onErrorJustReturn: false)) + + output = LoginViewModelOutputs( + isLoginValid: isLoginValid.asDriver(onErrorJustReturn: false), + alphaButton: alphaButton, + animateButtonEvent: animateButtonEvent) + } + + +} diff --git a/NewsApp/Scenes/Login/Model/TokenJwt.swift b/NewsApp/Scenes/Login/Model/TokenJwt.swift new file mode 100644 index 0000000..9fc9dda --- /dev/null +++ b/NewsApp/Scenes/Login/Model/TokenJwt.swift @@ -0,0 +1,13 @@ +// +// TokenJwt.swift +// NewsApp +// +// Created by Renê Xavier on 25/01/21. +// + +import Foundation + +struct TokenJwt: Codable { + static var currentToken: String? + var token: String? +} diff --git a/NewsApp/Scenes/Login/Model/UserLogin.swift b/NewsApp/Scenes/Login/Model/UserLogin.swift new file mode 100644 index 0000000..308eee3 --- /dev/null +++ b/NewsApp/Scenes/Login/Model/UserLogin.swift @@ -0,0 +1,17 @@ +// +// UserLogin.swift +// NewsApp +// +// Created by Renê Xavier on 24/01/21. +// + +import Foundation + +struct UserLogin: Codable { + var username: String + var password: String + + private enum CodingKeys : String, CodingKey { + case username = "user", password = "pass" + } +} diff --git a/NewsApp/Service/DefaultServerError.swift b/NewsApp/Service/DefaultServerError.swift new file mode 100644 index 0000000..7829afc --- /dev/null +++ b/NewsApp/Service/DefaultServerError.swift @@ -0,0 +1,24 @@ +// +// DefaultServerError.swift +// NewsApp +// +// Created by Renê Xavier on 26/01/21. +// + +import Foundation + +struct DefaultServerError: Error, Codable { + var message: String + var statusCode: Int? + + private enum CodingKeys : String, CodingKey { + case message = "erro" + } + + var messageUser: String { + guard let statusCode = statusCode else { + return message + } + return "\(message) (\(statusCode))" + } +} diff --git a/NewsApp/Service/Mocks/EmptyTokenJwt.json b/NewsApp/Service/Mocks/EmptyTokenJwt.json new file mode 100644 index 0000000..69a88e3 --- /dev/null +++ b/NewsApp/Service/Mocks/EmptyTokenJwt.json @@ -0,0 +1 @@ +{} diff --git a/NewsApp/Service/Mocks/NewsDetail.json b/NewsApp/Service/Mocks/NewsDetail.json new file mode 100644 index 0000000..71b6115 --- /dev/null +++ b/NewsApp/Service/Mocks/NewsDetail.json @@ -0,0 +1,23 @@ +[ + { + "documento": { + "url": "https://politica.estadao.com.br/noticias/geral,antes-de-demissao-secretario-de-covas-enfrentou-pressao-por-segurar-gastos-de-secretarias,70003280555", + "source": "O Estado de S.Paulo", + "produto": "Estadão", + "editoria": "Política", + "subeditoria": "Geral", + "titulo": "Antes de demissão, secretário de Covas enfrentou pressão por segurar gastos de secretarias", + "credito": "Bruno Ribeiro", + "datapub": "23/04/20", + "horapub": "16:57", + "linhafina": "Mauro Ricardo fez pastas segurarem orçamento para ano eleitoral, mas, com pandemia de covid-19, investimentos não foram feitos", + "imagem": "https://img.estadao.com.br/resources/jpg/2/4/1545959783142.jpg", + "thumbnail": "https://img.estadao.com.br/resources/jpg/1/7/1545959782771.jpg", + "creditoImagem": "JF Diorio/Estadão", + "legendaImagem": "

Mauro Ricardo é secretário de Governo da Prefeitura de São Paulo

", + "origem": "O Estado de S.Paulo", + "id": "1", + "corpoformatado": "

O secretário de Governo da gestão Bruno Covas (PSDB), Mauro Ricardo, que anunciou sua saída nesta quarta-feira, 22, vinha enfrentando desgastes na Prefeitura de São Paulo que contribuíram para sua saída.

Desde que chegou à Prefeitura, em outubro de 2018, Ricardo buscou segurar o repasse de recursos para as demais secretarias, de modo a criar uma reserva para investimentos mais vultosos em 2020, ano eleitoral. Neste ano, a cidade alcançou um orçamento para investimentos na ordem de R$ 5 bilhões. 

Essa atitude gerou desgaste interno, uma vez que os recursos para diversas ações não sairam, e secretários ouvidos pelo Estado avaliavam a situação como irreversível. Com a crise do coronavírus e a inversão da lógica de austeridade adotada até o momento, Ricardo deixou o posto.

\n \n

Mauro Ricardo

\n

Mauro Ricardo é secretário de Governo da Prefeitura de São Paulo

\n

\n

JF Diorio/Estadão

\n

Na Câmara Municipal, aliados de Covas avaliam haver espaço para o agora ex-secretário participar da campanha do prefeito à reeleição.

Em nota, a Prefeitura informou que Mauro Ricardo decidiu voltar à Prefeitura de São Paulo com compromisso de trabalhar até dezembro de 2019 e que, passados os cinco meses “extras”, deixou o posto de forma combinada com o prefeito. A assessoria de imprensa de Ricardo não comentou.

Nos últimos dias, alguns vereadores vinham promovendo uma fritura de Ricardo por outro motivo. O ex-secretário foi alvo de uma representação ao Ministério Público de São Paulo (MP-SP) durante a crise do coronavírus por supostamente ter mantido no trabalho presencial funcionários do grupo de risco para o novo coronavírus. Entre os servidores, segundo a denúncia, havia copeiros que tiveram seu trabalho descrito como “essencial” e que tiveram de trabalhar.

Segundo decreto do prefeito publicado no dia 20 de março, servidores com doenças crônicas e os idosos não seriam mais obrigados a comparecer ao trabalho, com exceção daqueles que ocupam postos em áreas essenciais, como nas secretarias da Saúde e da Segurança Urbana.

No caso da Secretaria de Governo, além de copeiro, haveria ao menos outros seis servidores que deveriam permanecer em casa, mas se viram obrigados a continuar o trabalho presencial, no Edifício Matarazzo, no Viaduto do Chá. 

Os nomes dessas pessoas foram repassados à Promotoria de Direitos Humanos do MP-SP, que pediu informações à Prefeitura. A resposta enviada à promotoria negou que houvesse servidores nessa situação e a promotora Anna Trotta Yaryd determinou o arquivamento do caso, sem instauração de inquérito.

O caso, entretanto, chegou à Câmara Municipal e foi explorado por parlamentares como Caio Miranda (DEM), que citou a situação em suas redes sociais.

A assessoria de Ricardo ressalta que o procedimento no MP foi arquivado e afirma que a saída do secretário não tem relação com o caso. A Secretaria Municipal de Comunicação foi questionada, mas não comentou a apuração feita pelo MP, destacando que o decreto que estabeleceu o isolamento dos servidores em grupos de risco vinha sendo cumprido.

" + } + } +] diff --git a/NewsApp/Service/Mocks/NewsList.json b/NewsApp/Service/Mocks/NewsList.json new file mode 100644 index 0000000..2504d33 --- /dev/null +++ b/NewsApp/Service/Mocks/NewsList.json @@ -0,0 +1,34 @@ +[ + { + "id_documento": "1", + "chapeu": "Política", + "titulo": "Antes de demissão, secretário de Covas enfrentou pressão por segurar gastos de secretarias", + "linha_fina": "Mauro Ricardo fez pastas segurarem orçamento para ano eleitoral, mas, com pandemia de covid-19, investimentos não foram feitos", + "data_hora_publicacao": "2020-04-23 16:57:00", + "url": "https://politica.estadao.com.br/noticias/geral,antes-de-demissao-secretario-de-covas-enfrentou-pressao-por-segurar-gastos-de-secretarias,70003280555", + "imagem": "https://img.estadao.com.br/resources/jpg/2/4/1545959783142.jpg", + "source": "O Estado de S.Paulo" + }, + { + "id_documento": "2", + "chapeu": "Política", + "titulo": "Sem acordo, Câmara Municipal adia votação para reduzir salários de vereadores", + "linha_fina": "Projeto que transfere recursos para o combate ao coronavírus é motivo de bate-boca até em grupos de WhatsApp", + "data_hora_publicacao": "2020-04-23 18:23:00", + "url": "https://politica.estadao.com.br/noticias/geral,sem-acordo-camara-municipal-adia-votacao-para-reduzir-salarios-de-vereadores,70003280728", + "imagem": "https://img.estadao.com.br/resources/jpg/9/2/1419025977029.jpg", + "source": "O Estado de S.Paulo" + }, + { + "id_documento": "3", + "chapeu": "Política", + "titulo": "Quem é Maurício Valeixo, delegado da Polícia Federal pivô da ameaça de demissão de Sérgio Moro", + "linha_fina": "Especialista em inteligência e combate ao narcotráfico, Maurício Leite Valeixo, diretor-geral da Polícia Federal, mantém relação de longa data com ministro Sergio Moro", + "data_hora_publicacao": "2020-04-23 17:23:00", + "credito": "Ricardo Brandt, Fausto Macedo e Julia Affonso", + "url": "https://politica.estadao.com.br/noticias/geral,quem-e-mauricio-valeixo-delegado-da-policia-federal-pivo-da-ameaca-de-demissao-de-sergio-moro,70003280612", + "imagem": "https://img.estadao.com.br/resources/jpg/0/8/1568342577880.jpg", + "imagem_credito": "Denis Ferreira Netto/ESTADÃO", + "source": "O Estado de S.Paulo" + } +] diff --git a/NewsApp/Service/Mocks/TokenJwt.json b/NewsApp/Service/Mocks/TokenJwt.json new file mode 100644 index 0000000..e8bb106 --- /dev/null +++ b/NewsApp/Service/Mocks/TokenJwt.json @@ -0,0 +1,3 @@ +{ + "token": "invalidToken" +} diff --git a/NewsApp/Service/NewsProvider.swift b/NewsApp/Service/NewsProvider.swift new file mode 100644 index 0000000..4db11ea --- /dev/null +++ b/NewsApp/Service/NewsProvider.swift @@ -0,0 +1,77 @@ +// +// NewsProvider.swift +// NewsApp +// +// Created by Renê Xavier on 25/01/21. +// + +import Foundation +import Moya + +enum NewsProvider { + case getNewsToken(user: UserLogin) + case getNews + case getNewsDetail(newsId: String) +} + +extension NewsProvider: TargetType { + var baseURL: URL { + if let baseUrl = URL(string: "https://teste-dev-mobile-api.herokuapp.com/") { + return baseUrl + } + return URL(fileURLWithPath: "") + } + + var path: String { + switch self { + case .getNewsToken: + return "login" + case .getNews: + return "news" + case .getNewsDetail(newsId: let newsId): + return "news/\(newsId)" + } + } + + var method: Moya.Method { + switch self { + case .getNewsToken: + return .post + case .getNews: + return .get + case .getNewsDetail: + return .get + } + } + + var sampleData: Data { + switch self { + case .getNews: + return Bundle.loadJSONFromBundle(resourceName: "TokenJwt") + default: + return Data() + } + } + + var task: Task { + switch self { + case .getNewsToken(user: let user): + return .requestJSONEncodable(user) + case .getNews: + return .requestPlain + case .getNewsDetail(newsId: let newsId): +// let parameters = ["": newsId] +// return .requestParameters(parameters: parameters, encoding: URLEncoding.default) + return .requestPlain + } + } + + static var tokenJwt: String = "" + + var headers: [String : String]? { + return ["Content-Type": "application/json", + "Authorization": "Bearer \(Self.tokenJwt)"] + } + + +} diff --git a/NewsApp/Service/NewsService.swift b/NewsApp/Service/NewsService.swift new file mode 100644 index 0000000..0baac93 --- /dev/null +++ b/NewsApp/Service/NewsService.swift @@ -0,0 +1,32 @@ +// +// NewsService.swift +// NewsApp +// +// Created by Renê Xavier on 25/01/21. +// + +import Foundation +import RxSwift +import Moya +import RxCocoa + +class NewsService { + var newsProvider = MoyaProvider() + + init(stub: Bool = false ) { + newsProvider = stub ? MoyaProvider(stubClosure: MoyaProvider.immediatelyStub) : MoyaProvider() + } + + func postNewsLogin(user: UserLogin) -> Single { + return newsProvider.rx.request(.getNewsToken(user: user)).filterSuccess() + } + + func fetchNews() -> Single { + return newsProvider.rx.request(.getNews).filterSuccess() + } + + func fetchNewsDetail(newsId: String) -> Single { + return newsProvider.rx.request(.getNewsDetail(newsId: newsId)).filterSuccess() + } +} + diff --git a/NewsApp/Service/ServiceError+Extension.swift b/NewsApp/Service/ServiceError+Extension.swift new file mode 100644 index 0000000..576d13e --- /dev/null +++ b/NewsApp/Service/ServiceError+Extension.swift @@ -0,0 +1,40 @@ +// +// ServiceError+Extension.swift +// NewsApp +// +// Created by Renê Xavier on 26/01/21. +// + +import Foundation +import RxSwift +import Moya + +extension PrimitiveSequence where Trait == SingleTrait, Element == Response { + func filterSuccess() -> Single { + return flatMap { (response) -> Single in + if 200 ... 299 ~= response.statusCode { + return .just(response) + } + + switch response.statusCode { + case 501: + let commonError = DefaultServerError(message: Constants.serviceUnavailableError, statusCode: nil) + return .error(commonError) + case -1009: + let commonError = DefaultServerError(message: Constants.notConnectedToInternet, statusCode: nil) + return .error(commonError) + default: break + } + + let decoder = JSONDecoder() + do { + var serverError = try decoder.decode(DefaultServerError.self, from: response.data) + serverError.statusCode = response.statusCode + return .error(serverError) + } catch { + let genericError = DefaultServerError(message: Constants.unknownError, statusCode: response.statusCode) + return .error(genericError) + } + } + } +} diff --git a/NewsApp/Utils/Constants.swift b/NewsApp/Utils/Constants.swift new file mode 100644 index 0000000..ab71007 --- /dev/null +++ b/NewsApp/Utils/Constants.swift @@ -0,0 +1,22 @@ +// +// Constants.swift +// NewsApp +// +// Created by Renê Xavier on 26/01/21. +// + +import Foundation +import UIKit + +struct Constants { + static let ok = "OK" + static let erro = "Erro" + static let atencao = "Atenção" + static let unknownError = "Erro inesperado" + static let serviceUnavailableError = "Serviço não disponível. 😅 Tente novamente mais tarde." + static let notConnectedToInternet = "Você não está conectado a internet. Conecte-se e tente novamente." + static let notFound = "Essa notícia não foi encontrada 🧐" + + static let loadingColor = UIColor(named: "Primary") ?? .black + static let loadingBackgroundColor = UIColor(named: "Accent")?.withAlphaComponent(0.5) ?? .white +} diff --git a/NewsApp/Utils/Extensions/Bundle+Extension.swift b/NewsApp/Utils/Extensions/Bundle+Extension.swift new file mode 100644 index 0000000..bd50b25 --- /dev/null +++ b/NewsApp/Utils/Extensions/Bundle+Extension.swift @@ -0,0 +1,18 @@ +// +// Bundle+Extension.swift +// NewsApp +// +// Created by Renê Xavier on 26/01/21. +// + +import Foundation + +extension Bundle { + static func loadJSONFromBundle(bundle: Bundle = Bundle.main, resourceName: String) -> Data { + guard let url = bundle.url(forResource: resourceName, withExtension: "json"), + let data = try? Data(contentsOf: url) else { + return Data() + } + return data + } +} diff --git a/NewsApp/Utils/Extensions/Codable+Extension.swift b/NewsApp/Utils/Extensions/Codable+Extension.swift new file mode 100644 index 0000000..6468bd5 --- /dev/null +++ b/NewsApp/Utils/Extensions/Codable+Extension.swift @@ -0,0 +1,19 @@ +// +// Codable+Extension.swift +// NewsApp +// +// Created by Renê Xavier on 27/01/21. +// + +import Foundation + +extension JSONDecoder { + static func decode(data: Data, to type: T.Type) throws -> T{ + let decoder = JSONDecoder() + do { + return try decoder.decode(type, from: data) + } catch { + throw DefaultServerError(message: Constants.unknownError, statusCode: -9999) + } + } +} diff --git a/NewsApp/Utils/Extensions/Date+Extension.swift b/NewsApp/Utils/Extensions/Date+Extension.swift new file mode 100644 index 0000000..dbdf949 --- /dev/null +++ b/NewsApp/Utils/Extensions/Date+Extension.swift @@ -0,0 +1,26 @@ +// +// Date+Extension.swift +// NewsApp +// +// Created by Renê Xavier on 25/01/21. +// + +import Foundation + +extension Date { + static func timeFormatter(date: String) -> String{ + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + let myDate = dateFormatter.date(from: date) ?? Date() + dateFormatter.dateFormat = "HH:mm" + return dateFormatter.string(from: myDate) + } + + static func dateFormatter(date: String) -> String{ + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + let myDate = dateFormatter.date(from: date) ?? Date() + dateFormatter.dateFormat = "dd/MM/yyyy" + return dateFormatter.string(from: myDate) + } +} diff --git a/NewsApp/Utils/Extensions/UIViewController+Extension.swift b/NewsApp/Utils/Extensions/UIViewController+Extension.swift new file mode 100644 index 0000000..834ce97 --- /dev/null +++ b/NewsApp/Utils/Extensions/UIViewController+Extension.swift @@ -0,0 +1,50 @@ +// +// UIViewController+Extension.swift +// NewsApp +// +// Created by Renê Xavier on 27/01/21. +// + +import Foundation +import UIKit + +extension UIViewController { + + //MARK: Present Error + func presentDefaultAlert(title: String = Constants.atencao, message: String?, _ handler: ((UIAlertAction) -> Void)? = nil) { + + let alertvc = UIAlertController(title: title, + message: message ?? Constants.unknownError, + preferredStyle: .alert) + let action = UIAlertAction(title: Constants.ok, + style: .default, + handler: handler) + alertvc.addAction(action) + self.present(alertvc, animated: true, completion: nil) + } + + func presentDefaultAlert(_ message: DefaultServerError?, _ handler: ((UIAlertAction) -> Void)? = nil) { + if message == nil { + let alert = DefaultServerError(message: Constants.unknownError, statusCode: -9999) + self.presentDefaultAlert(message: alert.messageUser, handler) + return + } + self.presentDefaultAlert(message: message?.messageUser, handler) + } + + func presentDefaultError(_ message: String?, _ handler: ((UIAlertAction) -> Void)? = nil) { + + let alertvc = UIAlertController(title: Constants.erro, + message: message ?? Constants.unknownError, + preferredStyle: .alert) + let action = UIAlertAction(title: Constants.ok, + style: .default, + handler: handler) + alertvc.addAction(action) + self.present(alertvc, animated: true, completion: nil) + } + + func presentDefaultError(_ message: DefaultServerError?, _ handler: ((UIAlertAction) -> Void)? = nil) { + self.presentDefaultError(message?.messageUser, handler) + } +} diff --git a/NewsApp/Utils/Extensions/UIViewController+Storyboardable.swift b/NewsApp/Utils/Extensions/UIViewController+Storyboardable.swift new file mode 100644 index 0000000..452495d --- /dev/null +++ b/NewsApp/Utils/Extensions/UIViewController+Storyboardable.swift @@ -0,0 +1,58 @@ +// +// UIViewController+Storyboardable.swift +// NewsApp +// +// Created by Renê Xavier on 24/01/21. +// + +import Foundation +import UIKit + +public protocol Storyboardable { + static var storyboardBundle: Bundle { get } +} + +extension Storyboardable where Self: UIViewController { + + public static var storyboardBundle: Bundle { + return Bundle(for: self) + } + + public static var identifier: String { + return String(describing: self) + } + + public static func getBundle(forStoryboard storyboardName: String) -> Bundle? { + // for cases like UINavigationController.instantiateInitial() + if storyboardBundle.path(forResource: storyboardName, ofType: "storyboardc") == nil { + if Bundle.main.path(forResource: storyboardName, ofType: "storyboardc") == nil { + return nil + } + return Bundle.main + } + return storyboardBundle + } +} + +extension ViewModelBased where Self: UIViewController { + + static func instantiate(from storyboardName: String = "Main") -> Self { + let storyboard = UIStoryboard(name: storyboardName, bundle: storyboardBundle) + guard let viewcontroller = storyboard.instantiateViewController(withIdentifier: identifier) as? Self else { + fatalError("Could not instantiate viewcontroller \(identifier) in storyboard with name: \(storyboardName)") + } + return viewcontroller + } + + static func instantiateInitial() -> Self { + let storyboardName = self.identifier + guard let bundle = getBundle(forStoryboard: storyboardName), + let viewcontroller = UIStoryboard(name: storyboardName, bundle: bundle).instantiateInitialViewController() as? Self else { + fatalError("Could not instantiate initial storyboard for: \(storyboardName)") + } + return viewcontroller + } +} + +extension UIViewController: Storyboardable {} + diff --git a/NewsApp/Utils/Extensions/ViewModel.swift b/NewsApp/Utils/Extensions/ViewModel.swift new file mode 100644 index 0000000..058eabf --- /dev/null +++ b/NewsApp/Utils/Extensions/ViewModel.swift @@ -0,0 +1,53 @@ +// +// ViewModel.swift +// NewsApp +// +// Created by Renê Xavier on 24/01/21. +// + +import Foundation +import UIKit + +protocol Inputs {} + +struct EmptyInputs: Inputs {} + +protocol Outputs {} + +struct EmptyOutputs: Outputs {} + +protocol Actions {} + +protocol ViewModel { + associatedtype InputType: Inputs + mutating func setUp(with input: InputType) + + associatedtype OutputType: Outputs + var output: OutputType! { get set } +} + +extension ViewModel { + func setUp(with input: EmptyInputs) {} +} + +protocol ViewModelBased { + associatedtype ViewModelType: ViewModel + var viewModel: ViewModelType! { get set } + + associatedtype InputData: Inputs + func getInputsViewModel() -> InputData + + func bindUI() +} + +extension ViewModelBased where Self: UIViewController, InputData == ViewModelType.InputType { + + mutating func bind( to viewmodel: inout ViewModelType) { + self.viewModel = viewmodel + loadViewIfNeeded() + let inputs = getInputsViewModel() + viewmodel.setUp(with: inputs) + self.bindUI() + } + +} diff --git a/NewsAppTests/Info.plist b/NewsAppTests/Info.plist new file mode 100644 index 0000000..64d65ca --- /dev/null +++ b/NewsAppTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/NewsAppTests/NewsAppTests.swift b/NewsAppTests/NewsAppTests.swift new file mode 100644 index 0000000..f0d9d5b --- /dev/null +++ b/NewsAppTests/NewsAppTests.swift @@ -0,0 +1,33 @@ +// +// NewsAppTests.swift +// NewsAppTests +// +// Created by Renê Xavier on 24/01/21. +// + +import XCTest +@testable import NewsApp + +class NewsAppTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/NewsAppUITests/Info.plist b/NewsAppUITests/Info.plist new file mode 100644 index 0000000..64d65ca --- /dev/null +++ b/NewsAppUITests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/NewsAppUITests/NewsAppUITests.swift b/NewsAppUITests/NewsAppUITests.swift new file mode 100644 index 0000000..9bad839 --- /dev/null +++ b/NewsAppUITests/NewsAppUITests.swift @@ -0,0 +1,42 @@ +// +// NewsAppUITests.swift +// NewsAppUITests +// +// Created by Renê Xavier on 24/01/21. +// + +import XCTest + +class NewsAppUITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use recording to get started writing UI tests. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testLaunchPerformance() throws { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } + } +} diff --git a/Podfile b/Podfile new file mode 100644 index 0000000..82138f2 --- /dev/null +++ b/Podfile @@ -0,0 +1,26 @@ +# Uncomment the next line to define a global platform for your project +platform :ios, '13.0' + +target 'NewsApp' do + # Comment the next line if you don't want to use dynamic frameworks + use_frameworks! + + # Pods for NewsApp + pod 'RxSwift', '~> 5.1.1' + pod 'RxCocoa', '~> 5.1.1' + pod 'Kingfisher', '~> 6.0.1' + pod 'Moya/RxSwift', '~> 14.0' + pod 'ProgressHUD', '~> 13.4' + pod 'SkeletonView', '~> 1.11.0' + pod "markymark", '~> 10.1.1' + + target 'NewsAppTests' do + inherit! :search_paths + # Pods for testing + end + + target 'NewsAppUITests' do + # Pods for testing + end + +end diff --git a/Podfile.lock b/Podfile.lock new file mode 100644 index 0000000..80665f1 --- /dev/null +++ b/Podfile.lock @@ -0,0 +1,53 @@ +PODS: + - Alamofire (5.4.1) + - Kingfisher (6.0.1) + - markymark (10.1.1) + - Moya/Core (14.0.0): + - Alamofire (~> 5.0) + - Moya/RxSwift (14.0.0): + - Moya/Core + - RxSwift (~> 5.0) + - ProgressHUD (13.4) + - RxCocoa (5.1.1): + - RxRelay (~> 5) + - RxSwift (~> 5) + - RxRelay (5.1.1): + - RxSwift (~> 5) + - RxSwift (5.1.1) + - SkeletonView (1.11.0) + +DEPENDENCIES: + - Kingfisher + - markymark (~> 10.1.1) + - Moya/RxSwift (~> 14.0) + - ProgressHUD (~> 13.4) + - RxCocoa (~> 5.1.1) + - RxSwift (~> 5.1.1) + - SkeletonView (~> 1.11.0) + +SPEC REPOS: + trunk: + - Alamofire + - Kingfisher + - markymark + - Moya + - ProgressHUD + - RxCocoa + - RxRelay + - RxSwift + - SkeletonView + +SPEC CHECKSUMS: + Alamofire: 2291f7d21ca607c491dd17642e5d40fdcda0e65c + Kingfisher: adde87a4f74f6a3845395769354efff593581740 + markymark: 29adb8789fa6b3db97b04b7585d65f13931b5aa5 + Moya: 5b45dacb75adb009f97fde91c204c1e565d31916 + ProgressHUD: 137a7fc677487c5581668f4490563124fb35c232 + RxCocoa: 32065309a38d29b5b0db858819b5bf9ef038b601 + RxRelay: d77f7d771495f43c556cbc43eebd1bb54d01e8e9 + RxSwift: 81470a2074fa8780320ea5fe4102807cb7118178 + SkeletonView: cc84ce90a804dd0dd7198dd802833411c8a64eaf + +PODFILE CHECKSUM: 23e622010c67078a3d2ba096d7bb5866af32e65e + +COCOAPODS: 1.9.3