diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7f46e69 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +#Xcode +.DS_Store +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +*.xcworkspace +!default.xcworkspace +xcuserdata +profile +*.moved-aside +DerivedData +.idea/ +# Pods - for those of you who use CocoaPods +Pods +*.lock +# Fastlane +*.ipa +*.zip diff --git a/DesafioIOS/DesafioIOS.xcodeproj/project.pbxproj b/DesafioIOS/DesafioIOS.xcodeproj/project.pbxproj new file mode 100644 index 0000000..427d308 --- /dev/null +++ b/DesafioIOS/DesafioIOS.xcodeproj/project.pbxproj @@ -0,0 +1,943 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 48; + objects = { + +/* Begin PBXBuildFile section */ + 4A427B7B1FA4AF6700B0E95D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A427B7A1FA4AF6700B0E95D /* AppDelegate.swift */; }; + 4A427B801FA4AF6700B0E95D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4A427B7E1FA4AF6700B0E95D /* Main.storyboard */; }; + 4A427B851FA4AF6700B0E95D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4A427B841FA4AF6700B0E95D /* Assets.xcassets */; }; + 4A427B881FA4AF6700B0E95D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4A427B861FA4AF6700B0E95D /* LaunchScreen.storyboard */; }; + 4A5451DD1FB3754100B71E12 /* NotificationCenter+Custom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5451DC1FB3754100B71E12 /* NotificationCenter+Custom.swift */; }; + 4A54D6561FA507AD00569EB9 /* SharedProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A54D6551FA507AD00569EB9 /* SharedProtocols.swift */; }; + 4A54D6591FA51C8700569EB9 /* RepositoryServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A54D6581FA51C8700569EB9 /* RepositoryServiceTests.swift */; }; + 4A54D65B1FA51CA100569EB9 /* PullRequestServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A54D65A1FA51CA100569EB9 /* PullRequestServiceTests.swift */; }; + 4A54D6601FA51F2000569EB9 /* RepositoriesViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A54D65F1FA51F2000569EB9 /* RepositoriesViewModelTests.swift */; }; + 4A54D6621FA51F3200569EB9 /* PullRequestsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A54D6611FA51F3200569EB9 /* PullRequestsViewModelTests.swift */; }; + 4A9011A41FA6032E00494D14 /* MockWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A9011A31FA6032E00494D14 /* MockWebViewController.swift */; }; + 4A9011A61FA6040200494D14 /* MockPullRequestsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A9011A51FA6040200494D14 /* MockPullRequestsViewController.swift */; }; + 4A9011A81FA6043500494D14 /* MockPullRequestsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A9011A71FA6043500494D14 /* MockPullRequestsViewModel.swift */; }; + 4A9011AA1FA6048500494D14 /* MockRepositoriesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A9011A91FA6048500494D14 /* MockRepositoriesViewController.swift */; }; + 4A993A691FA4B32200A23610 /* ReachabilityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A993A681FA4B32200A23610 /* ReachabilityManager.swift */; }; + 4A993A6E1FA4B40C00A23610 /* UIKit+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A993A6D1FA4B40C00A23610 /* UIKit+Extensions.swift */; }; + 4A993A701FA4B41900A23610 /* Foundation+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A993A6F1FA4B41900A23610 /* Foundation+Extensions.swift */; }; + 4A993A751FA4B4CF00A23610 /* DropShadowNavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A993A741FA4B4CF00A23610 /* DropShadowNavigationBar.swift */; }; + 4A993A781FA4B7D600A23610 /* RestClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A993A771FA4B7D600A23610 /* RestClient.swift */; }; + 4A993A7A1FA4B85100A23610 /* PullRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A993A791FA4B85100A23610 /* PullRequest.swift */; }; + 4A993A7C1FA4B86200A23610 /* Repository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A993A7B1FA4B86200A23610 /* Repository.swift */; }; + 4A993A7E1FA4B86700A23610 /* Owner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A993A7D1FA4B86700A23610 /* Owner.swift */; }; + 4A993A801FA4B8B600A23610 /* RepositoryService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A993A7F1FA4B8B600A23610 /* RepositoryService.swift */; }; + 4A993A821FA4B8C200A23610 /* PullRequestService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A993A811FA4B8C200A23610 /* PullRequestService.swift */; }; + 4A993A841FA4B9DA00A23610 /* MainStoryboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A993A831FA4B9DA00A23610 /* MainStoryboard.swift */; }; + 4A993A861FA4BA0E00A23610 /* RepositoriesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A993A851FA4BA0E00A23610 /* RepositoriesViewController.swift */; }; + 4A993A881FA4BA1D00A23610 /* PullRequestsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A993A871FA4BA1D00A23610 /* PullRequestsViewController.swift */; }; + 4A993A8A1FA4BA3F00A23610 /* WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A993A891FA4BA3F00A23610 /* WebViewController.swift */; }; + 4A993A8F1FA4BAC100A23610 /* RepositoryTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A993A8E1FA4BAC100A23610 /* RepositoryTableViewCell.swift */; }; + 4A993A911FA4BACD00A23610 /* PullRequestTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A993A901FA4BACD00A23610 /* PullRequestTableViewCell.swift */; }; + 4A993A961FA4BB6300A23610 /* RepositoriesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A993A951FA4BB6300A23610 /* RepositoriesViewModel.swift */; }; + 4A993A981FA4BB7000A23610 /* PullRequestsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A993A971FA4BB7000A23610 /* PullRequestsViewModel.swift */; }; + 4A993A9A1FA4BB7B00A23610 /* WebViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A993A991FA4BB7B00A23610 /* WebViewModel.swift */; }; + 4A993A9F1FA4BC9300A23610 /* ReachabilityManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A993A9E1FA4BC9300A23610 /* ReachabilityManagerTests.swift */; }; + 4A993AA11FA4BCB200A23610 /* RestClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A993AA01FA4BCB200A23610 /* RestClientTests.swift */; }; + 4A993AA41FA4BCF700A23610 /* RepositoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A993AA31FA4BCF700A23610 /* RepositoryTests.swift */; }; + 4A993AA61FA4BD0600A23610 /* PullRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A993AA51FA4BD0600A23610 /* PullRequestTests.swift */; }; + 4A993AA81FA4BD1B00A23610 /* OwnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A993AA71FA4BD1B00A23610 /* OwnerTests.swift */; }; + 4A993AAA1FA4BD4000A23610 /* RepositoriesViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A993AA91FA4BD4000A23610 /* RepositoriesViewControllerTests.swift */; }; + 4A993AAC1FA4BD6A00A23610 /* PullRequestsViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A993AAB1FA4BD6A00A23610 /* PullRequestsViewControllerTests.swift */; }; + 4A993AAE1FA4BD7800A23610 /* WebViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A993AAD1FA4BD7800A23610 /* WebViewControllerTests.swift */; }; + 4AACED971FA67573007A02CF /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 4AACED991FA67573007A02CF /* Localizable.strings */; }; + 4DB7243BB4F6778FA4C27123 /* Pods_DesafioIOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D7C9DB27EC4D977B219219A4 /* Pods_DesafioIOS.framework */; }; + D443C24FAA1DB184CE1DC556 /* Pods_DesafioIOSTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 39C83602D0FE778852F685A5 /* Pods_DesafioIOSTests.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 4A427B8F1FA4AF6700B0E95D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 4A427B6F1FA4AF6700B0E95D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 4A427B761FA4AF6700B0E95D; + remoteInfo = DesafioIOS; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 181FA2BEBF8895A2D9637B8D /* Pods-DesafioIOSTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DesafioIOSTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-DesafioIOSTests/Pods-DesafioIOSTests.debug.xcconfig"; sourceTree = ""; }; + 39C83602D0FE778852F685A5 /* Pods_DesafioIOSTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_DesafioIOSTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 4A427B771FA4AF6700B0E95D /* DesafioIOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DesafioIOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 4A427B7A1FA4AF6700B0E95D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 4A427B7F1FA4AF6700B0E95D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 4A427B841FA4AF6700B0E95D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 4A427B871FA4AF6700B0E95D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 4A427B8E1FA4AF6700B0E95D /* DesafioIOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DesafioIOSTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 4A427B941FA4AF6700B0E95D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 4A5451DC1FB3754100B71E12 /* NotificationCenter+Custom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationCenter+Custom.swift"; sourceTree = ""; }; + 4A54D6551FA507AD00569EB9 /* SharedProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedProtocols.swift; sourceTree = ""; }; + 4A54D6581FA51C8700569EB9 /* RepositoryServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryServiceTests.swift; sourceTree = ""; }; + 4A54D65A1FA51CA100569EB9 /* PullRequestServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PullRequestServiceTests.swift; sourceTree = ""; }; + 4A54D65F1FA51F2000569EB9 /* RepositoriesViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoriesViewModelTests.swift; sourceTree = ""; }; + 4A54D6611FA51F3200569EB9 /* PullRequestsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PullRequestsViewModelTests.swift; sourceTree = ""; }; + 4A9011A31FA6032E00494D14 /* MockWebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockWebViewController.swift; sourceTree = ""; }; + 4A9011A51FA6040200494D14 /* MockPullRequestsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPullRequestsViewController.swift; sourceTree = ""; }; + 4A9011A71FA6043500494D14 /* MockPullRequestsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPullRequestsViewModel.swift; sourceTree = ""; }; + 4A9011A91FA6048500494D14 /* MockRepositoriesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRepositoriesViewController.swift; sourceTree = ""; }; + 4A993A611FA4B12E00A23610 /* Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Bridging-Header.h"; sourceTree = ""; }; + 4A993A621FA4B12E00A23610 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 4A993A681FA4B32200A23610 /* ReachabilityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReachabilityManager.swift; sourceTree = ""; }; + 4A993A6D1FA4B40C00A23610 /* UIKit+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIKit+Extensions.swift"; sourceTree = ""; }; + 4A993A6F1FA4B41900A23610 /* Foundation+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Foundation+Extensions.swift"; sourceTree = ""; }; + 4A993A741FA4B4CF00A23610 /* DropShadowNavigationBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropShadowNavigationBar.swift; sourceTree = ""; }; + 4A993A771FA4B7D600A23610 /* RestClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestClient.swift; sourceTree = ""; }; + 4A993A791FA4B85100A23610 /* PullRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PullRequest.swift; sourceTree = ""; }; + 4A993A7B1FA4B86200A23610 /* Repository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Repository.swift; sourceTree = ""; }; + 4A993A7D1FA4B86700A23610 /* Owner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Owner.swift; sourceTree = ""; }; + 4A993A7F1FA4B8B600A23610 /* RepositoryService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryService.swift; sourceTree = ""; }; + 4A993A811FA4B8C200A23610 /* PullRequestService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PullRequestService.swift; sourceTree = ""; }; + 4A993A831FA4B9DA00A23610 /* MainStoryboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainStoryboard.swift; sourceTree = ""; }; + 4A993A851FA4BA0E00A23610 /* RepositoriesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoriesViewController.swift; sourceTree = ""; }; + 4A993A871FA4BA1D00A23610 /* PullRequestsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PullRequestsViewController.swift; sourceTree = ""; }; + 4A993A891FA4BA3F00A23610 /* WebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewController.swift; sourceTree = ""; }; + 4A993A8E1FA4BAC100A23610 /* RepositoryTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryTableViewCell.swift; sourceTree = ""; }; + 4A993A901FA4BACD00A23610 /* PullRequestTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PullRequestTableViewCell.swift; sourceTree = ""; }; + 4A993A951FA4BB6300A23610 /* RepositoriesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoriesViewModel.swift; sourceTree = ""; }; + 4A993A971FA4BB7000A23610 /* PullRequestsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PullRequestsViewModel.swift; sourceTree = ""; }; + 4A993A991FA4BB7B00A23610 /* WebViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewModel.swift; sourceTree = ""; }; + 4A993A9E1FA4BC9300A23610 /* ReachabilityManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReachabilityManagerTests.swift; sourceTree = ""; }; + 4A993AA01FA4BCB200A23610 /* RestClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestClientTests.swift; sourceTree = ""; }; + 4A993AA31FA4BCF700A23610 /* RepositoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryTests.swift; sourceTree = ""; }; + 4A993AA51FA4BD0600A23610 /* PullRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PullRequestTests.swift; sourceTree = ""; }; + 4A993AA71FA4BD1B00A23610 /* OwnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OwnerTests.swift; sourceTree = ""; }; + 4A993AA91FA4BD4000A23610 /* RepositoriesViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoriesViewControllerTests.swift; sourceTree = ""; }; + 4A993AAB1FA4BD6A00A23610 /* PullRequestsViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PullRequestsViewControllerTests.swift; sourceTree = ""; }; + 4A993AAD1FA4BD7800A23610 /* WebViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewControllerTests.swift; sourceTree = ""; }; + 4AACED8D1FA674EC007A02CF /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Main.strings"; sourceTree = ""; }; + 4AACED8E1FA674EC007A02CF /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/LaunchScreen.strings"; sourceTree = ""; }; + 4AACED981FA67573007A02CF /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + 4AACED9A1FA67575007A02CF /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = ""; }; + 4BA7F1716C11BA8B1E0EB9CD /* Pods-DesafioIOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DesafioIOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-DesafioIOS/Pods-DesafioIOS.release.xcconfig"; sourceTree = ""; }; + 6B19B80C6E3CBF350F139E84 /* Pods-DesafioIOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DesafioIOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-DesafioIOS/Pods-DesafioIOS.debug.xcconfig"; sourceTree = ""; }; + D7C9DB27EC4D977B219219A4 /* Pods_DesafioIOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_DesafioIOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + E892893D796E7ECB915AC464 /* Pods-DesafioIOSTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DesafioIOSTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-DesafioIOSTests/Pods-DesafioIOSTests.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 4A427B741FA4AF6700B0E95D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4DB7243BB4F6778FA4C27123 /* Pods_DesafioIOS.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4A427B8B1FA4AF6700B0E95D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D443C24FAA1DB184CE1DC556 /* Pods_DesafioIOSTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 4A427B6E1FA4AF6700B0E95D = { + isa = PBXGroup; + children = ( + 4A427B791FA4AF6700B0E95D /* DesafioIOS */, + 4A427B911FA4AF6700B0E95D /* DesafioIOSTests */, + 4A427B781FA4AF6700B0E95D /* Products */, + 7FB0D43E8B06B528EC37013F /* Pods */, + C14EA81FB48DB572A80FE5DF /* Frameworks */, + ); + sourceTree = ""; + }; + 4A427B781FA4AF6700B0E95D /* Products */ = { + isa = PBXGroup; + children = ( + 4A427B771FA4AF6700B0E95D /* DesafioIOS.app */, + 4A427B8E1FA4AF6700B0E95D /* DesafioIOSTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 4A427B791FA4AF6700B0E95D /* DesafioIOS */ = { + isa = PBXGroup; + children = ( + 4A427B7A1FA4AF6700B0E95D /* AppDelegate.swift */, + 4AACED991FA67573007A02CF /* Localizable.strings */, + 4A427B841FA4AF6700B0E95D /* Assets.xcassets */, + 4A993A5F1FA4B12E00A23610 /* Core */, + 4A993A631FA4B12E00A23610 /* Models */, + 4A993A661FA4B14A00A23610 /* Views */, + 4A993A5E1FA4B12E00A23610 /* Controllers */, + ); + path = DesafioIOS; + sourceTree = ""; + }; + 4A427B911FA4AF6700B0E95D /* DesafioIOSTests */ = { + isa = PBXGroup; + children = ( + 4A993A9B1FA4BC5D00A23610 /* Core */, + 4A993A9C1FA4BC6300A23610 /* Models */, + 4A993A9D1FA4BC6800A23610 /* Controllers */, + ); + path = DesafioIOSTests; + sourceTree = ""; + }; + 4A54D6571FA51C6500569EB9 /* External Data */ = { + isa = PBXGroup; + children = ( + 4A993AA01FA4BCB200A23610 /* RestClientTests.swift */, + 4A54D6581FA51C8700569EB9 /* RepositoryServiceTests.swift */, + 4A54D65A1FA51CA100569EB9 /* PullRequestServiceTests.swift */, + ); + name = "External Data"; + sourceTree = ""; + }; + 4A54D65C1FA51EE500569EB9 /* Repositories */ = { + isa = PBXGroup; + children = ( + 4A9011A91FA6048500494D14 /* MockRepositoriesViewController.swift */, + 4A993AA91FA4BD4000A23610 /* RepositoriesViewControllerTests.swift */, + 4A54D65F1FA51F2000569EB9 /* RepositoriesViewModelTests.swift */, + ); + name = Repositories; + sourceTree = ""; + }; + 4A54D65D1FA51EF100569EB9 /* PullRequests */ = { + isa = PBXGroup; + children = ( + 4A9011A51FA6040200494D14 /* MockPullRequestsViewController.swift */, + 4A9011A71FA6043500494D14 /* MockPullRequestsViewModel.swift */, + 4A993AAB1FA4BD6A00A23610 /* PullRequestsViewControllerTests.swift */, + 4A54D6611FA51F3200569EB9 /* PullRequestsViewModelTests.swift */, + ); + name = PullRequests; + sourceTree = ""; + }; + 4A54D65E1FA51EFF00569EB9 /* Web */ = { + isa = PBXGroup; + children = ( + 4A9011A31FA6032E00494D14 /* MockWebViewController.swift */, + 4A993AAD1FA4BD7800A23610 /* WebViewControllerTests.swift */, + ); + name = Web; + sourceTree = ""; + }; + 4A9011AB1FA6050400494D14 /* Managers */ = { + isa = PBXGroup; + children = ( + 4A993A9E1FA4BC9300A23610 /* ReachabilityManagerTests.swift */, + ); + name = Managers; + sourceTree = ""; + }; + 4A993A5E1FA4B12E00A23610 /* Controllers */ = { + isa = PBXGroup; + children = ( + 4A993A831FA4B9DA00A23610 /* MainStoryboard.swift */, + 4A993A931FA4BB0E00A23610 /* Repositories */, + 4A993A921FA4BB0700A23610 /* PullRequests */, + 4A993A941FA4BB3900A23610 /* Web */, + ); + path = Controllers; + sourceTree = ""; + }; + 4A993A5F1FA4B12E00A23610 /* Core */ = { + isa = PBXGroup; + children = ( + 4A993A671FA4B31300A23610 /* Managers */, + 4A993A761FA4B7C700A23610 /* External Data */, + 4A993A6A1FA4B3E200A23610 /* Utils */, + 4A993A601FA4B12E00A23610 /* Supporting Files */, + ); + path = Core; + sourceTree = ""; + }; + 4A993A601FA4B12E00A23610 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 4A993A611FA4B12E00A23610 /* Bridging-Header.h */, + 4A993A621FA4B12E00A23610 /* Info.plist */, + ); + path = "Supporting Files"; + sourceTree = ""; + }; + 4A993A631FA4B12E00A23610 /* Models */ = { + isa = PBXGroup; + children = ( + 4A993A7B1FA4B86200A23610 /* Repository.swift */, + 4A993A791FA4B85100A23610 /* PullRequest.swift */, + 4A993A7D1FA4B86700A23610 /* Owner.swift */, + ); + path = Models; + sourceTree = ""; + }; + 4A993A661FA4B14A00A23610 /* Views */ = { + isa = PBXGroup; + children = ( + 4A427B7E1FA4AF6700B0E95D /* Main.storyboard */, + 4A427B861FA4AF6700B0E95D /* LaunchScreen.storyboard */, + ); + name = Views; + sourceTree = ""; + }; + 4A993A671FA4B31300A23610 /* Managers */ = { + isa = PBXGroup; + children = ( + 4A993A681FA4B32200A23610 /* ReachabilityManager.swift */, + ); + name = Managers; + sourceTree = ""; + }; + 4A993A6A1FA4B3E200A23610 /* Utils */ = { + isa = PBXGroup; + children = ( + 4A54D6551FA507AD00569EB9 /* SharedProtocols.swift */, + 4A993A741FA4B4CF00A23610 /* DropShadowNavigationBar.swift */, + 4A5451DC1FB3754100B71E12 /* NotificationCenter+Custom.swift */, + 4A993A6D1FA4B40C00A23610 /* UIKit+Extensions.swift */, + 4A993A6F1FA4B41900A23610 /* Foundation+Extensions.swift */, + ); + name = Utils; + sourceTree = ""; + }; + 4A993A761FA4B7C700A23610 /* External Data */ = { + isa = PBXGroup; + children = ( + 4A993A771FA4B7D600A23610 /* RestClient.swift */, + 4A993A7F1FA4B8B600A23610 /* RepositoryService.swift */, + 4A993A811FA4B8C200A23610 /* PullRequestService.swift */, + ); + name = "External Data"; + sourceTree = ""; + }; + 4A993A921FA4BB0700A23610 /* PullRequests */ = { + isa = PBXGroup; + children = ( + 4A993A871FA4BA1D00A23610 /* PullRequestsViewController.swift */, + 4A993A971FA4BB7000A23610 /* PullRequestsViewModel.swift */, + 4A993A901FA4BACD00A23610 /* PullRequestTableViewCell.swift */, + ); + name = PullRequests; + sourceTree = ""; + }; + 4A993A931FA4BB0E00A23610 /* Repositories */ = { + isa = PBXGroup; + children = ( + 4A993A851FA4BA0E00A23610 /* RepositoriesViewController.swift */, + 4A993A951FA4BB6300A23610 /* RepositoriesViewModel.swift */, + 4A993A8E1FA4BAC100A23610 /* RepositoryTableViewCell.swift */, + ); + name = Repositories; + sourceTree = ""; + }; + 4A993A941FA4BB3900A23610 /* Web */ = { + isa = PBXGroup; + children = ( + 4A993A891FA4BA3F00A23610 /* WebViewController.swift */, + 4A993A991FA4BB7B00A23610 /* WebViewModel.swift */, + ); + name = Web; + sourceTree = ""; + }; + 4A993A9B1FA4BC5D00A23610 /* Core */ = { + isa = PBXGroup; + children = ( + 4A9011AB1FA6050400494D14 /* Managers */, + 4A54D6571FA51C6500569EB9 /* External Data */, + 4A993AA21FA4BCDB00A23610 /* Supporting Files */, + ); + name = Core; + sourceTree = ""; + }; + 4A993A9C1FA4BC6300A23610 /* Models */ = { + isa = PBXGroup; + children = ( + 4A993AA31FA4BCF700A23610 /* RepositoryTests.swift */, + 4A993AA51FA4BD0600A23610 /* PullRequestTests.swift */, + 4A993AA71FA4BD1B00A23610 /* OwnerTests.swift */, + ); + name = Models; + sourceTree = ""; + }; + 4A993A9D1FA4BC6800A23610 /* Controllers */ = { + isa = PBXGroup; + children = ( + 4A54D65C1FA51EE500569EB9 /* Repositories */, + 4A54D65D1FA51EF100569EB9 /* PullRequests */, + 4A54D65E1FA51EFF00569EB9 /* Web */, + ); + name = Controllers; + sourceTree = ""; + }; + 4A993AA21FA4BCDB00A23610 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 4A427B941FA4AF6700B0E95D /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 7FB0D43E8B06B528EC37013F /* Pods */ = { + isa = PBXGroup; + children = ( + 6B19B80C6E3CBF350F139E84 /* Pods-DesafioIOS.debug.xcconfig */, + 4BA7F1716C11BA8B1E0EB9CD /* Pods-DesafioIOS.release.xcconfig */, + 181FA2BEBF8895A2D9637B8D /* Pods-DesafioIOSTests.debug.xcconfig */, + E892893D796E7ECB915AC464 /* Pods-DesafioIOSTests.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + C14EA81FB48DB572A80FE5DF /* Frameworks */ = { + isa = PBXGroup; + children = ( + D7C9DB27EC4D977B219219A4 /* Pods_DesafioIOS.framework */, + 39C83602D0FE778852F685A5 /* Pods_DesafioIOSTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 4A427B761FA4AF6700B0E95D /* DesafioIOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4A427B971FA4AF6700B0E95D /* Build configuration list for PBXNativeTarget "DesafioIOS" */; + buildPhases = ( + 9A9049DD7D95E01718403CB4 /* [CP] Check Pods Manifest.lock */, + 4A427B731FA4AF6700B0E95D /* Sources */, + 4A427B741FA4AF6700B0E95D /* Frameworks */, + 4A427B751FA4AF6700B0E95D /* Resources */, + 7131FFAB33D5DE554FDC1C82 /* [CP] Embed Pods Frameworks */, + C5BFD387178789BD351200E7 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = DesafioIOS; + productName = DesafioIOS; + productReference = 4A427B771FA4AF6700B0E95D /* DesafioIOS.app */; + productType = "com.apple.product-type.application"; + }; + 4A427B8D1FA4AF6700B0E95D /* DesafioIOSTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4A427B9A1FA4AF6700B0E95D /* Build configuration list for PBXNativeTarget "DesafioIOSTests" */; + buildPhases = ( + 913D7D1A2182E387CCF591A1 /* [CP] Check Pods Manifest.lock */, + 4A427B8A1FA4AF6700B0E95D /* Sources */, + 4A427B8B1FA4AF6700B0E95D /* Frameworks */, + 4A427B8C1FA4AF6700B0E95D /* Resources */, + E9278D7BCEF7E5EF4B6EFA35 /* [CP] Embed Pods Frameworks */, + F609788CEA126B9E27CE8F8B /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + 4A427B901FA4AF6700B0E95D /* PBXTargetDependency */, + ); + name = DesafioIOSTests; + productName = DesafioIOSTests; + productReference = 4A427B8E1FA4AF6700B0E95D /* DesafioIOSTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 4A427B6F1FA4AF6700B0E95D /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0900; + LastUpgradeCheck = 0900; + ORGANIZATIONNAME = Nexaas; + TargetAttributes = { + 4A427B761FA4AF6700B0E95D = { + CreatedOnToolsVersion = 9.0.1; + ProvisioningStyle = Automatic; + }; + 4A427B8D1FA4AF6700B0E95D = { + CreatedOnToolsVersion = 9.0.1; + ProvisioningStyle = Automatic; + TestTargetID = 4A427B761FA4AF6700B0E95D; + }; + }; + }; + buildConfigurationList = 4A427B721FA4AF6700B0E95D /* Build configuration list for PBXProject "DesafioIOS" */; + compatibilityVersion = "Xcode 8.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + "pt-BR", + ); + mainGroup = 4A427B6E1FA4AF6700B0E95D; + productRefGroup = 4A427B781FA4AF6700B0E95D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 4A427B761FA4AF6700B0E95D /* DesafioIOS */, + 4A427B8D1FA4AF6700B0E95D /* DesafioIOSTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 4A427B751FA4AF6700B0E95D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4A427B881FA4AF6700B0E95D /* LaunchScreen.storyboard in Resources */, + 4AACED971FA67573007A02CF /* Localizable.strings in Resources */, + 4A427B851FA4AF6700B0E95D /* Assets.xcassets in Resources */, + 4A427B801FA4AF6700B0E95D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4A427B8C1FA4AF6700B0E95D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 7131FFAB33D5DE554FDC1C82 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-DesafioIOS/Pods-DesafioIOS-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework", + "${BUILT_PRODUCTS_DIR}/CoolDesignables/CoolDesignables.framework", + "${BUILT_PRODUCTS_DIR}/ReachabilitySwift/ReachabilitySwift.framework", + "${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework", + "${BUILT_PRODUCTS_DIR}/SVProgressHUD/SVProgressHUD.framework", + "${BUILT_PRODUCTS_DIR}/StoryboardContext/StoryboardContext.framework", + "${BUILT_PRODUCTS_DIR}/SwiftyJSON/SwiftyJSON.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Alamofire.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CoolDesignables.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ReachabilitySwift.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImage.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SVProgressHUD.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/StoryboardContext.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyJSON.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-DesafioIOS/Pods-DesafioIOS-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 913D7D1A2182E387CCF591A1 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-DesafioIOSTests-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; + }; + 9A9049DD7D95E01718403CB4 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-DesafioIOS-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; + }; + C5BFD387178789BD351200E7 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-DesafioIOS/Pods-DesafioIOS-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + E9278D7BCEF7E5EF4B6EFA35 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-DesafioIOSTests/Pods-DesafioIOSTests-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework", + "${BUILT_PRODUCTS_DIR}/CoolDesignables/CoolDesignables.framework", + "${BUILT_PRODUCTS_DIR}/ReachabilitySwift/ReachabilitySwift.framework", + "${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework", + "${BUILT_PRODUCTS_DIR}/SVProgressHUD/SVProgressHUD.framework", + "${BUILT_PRODUCTS_DIR}/StoryboardContext/StoryboardContext.framework", + "${BUILT_PRODUCTS_DIR}/SwiftyJSON/SwiftyJSON.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Alamofire.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CoolDesignables.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ReachabilitySwift.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImage.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SVProgressHUD.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/StoryboardContext.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyJSON.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-DesafioIOSTests/Pods-DesafioIOSTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + F609788CEA126B9E27CE8F8B /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-DesafioIOSTests/Pods-DesafioIOSTests-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 4A427B731FA4AF6700B0E95D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4A993A781FA4B7D600A23610 /* RestClient.swift in Sources */, + 4A993A861FA4BA0E00A23610 /* RepositoriesViewController.swift in Sources */, + 4A5451DD1FB3754100B71E12 /* NotificationCenter+Custom.swift in Sources */, + 4A427B7B1FA4AF6700B0E95D /* AppDelegate.swift in Sources */, + 4A993A751FA4B4CF00A23610 /* DropShadowNavigationBar.swift in Sources */, + 4A993A911FA4BACD00A23610 /* PullRequestTableViewCell.swift in Sources */, + 4A993A981FA4BB7000A23610 /* PullRequestsViewModel.swift in Sources */, + 4A54D6561FA507AD00569EB9 /* SharedProtocols.swift in Sources */, + 4A993A801FA4B8B600A23610 /* RepositoryService.swift in Sources */, + 4A993A9A1FA4BB7B00A23610 /* WebViewModel.swift in Sources */, + 4A993A881FA4BA1D00A23610 /* PullRequestsViewController.swift in Sources */, + 4A993A701FA4B41900A23610 /* Foundation+Extensions.swift in Sources */, + 4A993A691FA4B32200A23610 /* ReachabilityManager.swift in Sources */, + 4A993A8F1FA4BAC100A23610 /* RepositoryTableViewCell.swift in Sources */, + 4A993A7A1FA4B85100A23610 /* PullRequest.swift in Sources */, + 4A993A7C1FA4B86200A23610 /* Repository.swift in Sources */, + 4A993A7E1FA4B86700A23610 /* Owner.swift in Sources */, + 4A993A8A1FA4BA3F00A23610 /* WebViewController.swift in Sources */, + 4A993A6E1FA4B40C00A23610 /* UIKit+Extensions.swift in Sources */, + 4A993A841FA4B9DA00A23610 /* MainStoryboard.swift in Sources */, + 4A993A821FA4B8C200A23610 /* PullRequestService.swift in Sources */, + 4A993A961FA4BB6300A23610 /* RepositoriesViewModel.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4A427B8A1FA4AF6700B0E95D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4A993AAA1FA4BD4000A23610 /* RepositoriesViewControllerTests.swift in Sources */, + 4A993AA81FA4BD1B00A23610 /* OwnerTests.swift in Sources */, + 4A9011A61FA6040200494D14 /* MockPullRequestsViewController.swift in Sources */, + 4A54D6621FA51F3200569EB9 /* PullRequestsViewModelTests.swift in Sources */, + 4A993A9F1FA4BC9300A23610 /* ReachabilityManagerTests.swift in Sources */, + 4A993AA11FA4BCB200A23610 /* RestClientTests.swift in Sources */, + 4A993AA61FA4BD0600A23610 /* PullRequestTests.swift in Sources */, + 4A993AAC1FA4BD6A00A23610 /* PullRequestsViewControllerTests.swift in Sources */, + 4A9011AA1FA6048500494D14 /* MockRepositoriesViewController.swift in Sources */, + 4A9011A81FA6043500494D14 /* MockPullRequestsViewModel.swift in Sources */, + 4A54D65B1FA51CA100569EB9 /* PullRequestServiceTests.swift in Sources */, + 4A54D6601FA51F2000569EB9 /* RepositoriesViewModelTests.swift in Sources */, + 4A54D6591FA51C8700569EB9 /* RepositoryServiceTests.swift in Sources */, + 4A993AA41FA4BCF700A23610 /* RepositoryTests.swift in Sources */, + 4A993AAE1FA4BD7800A23610 /* WebViewControllerTests.swift in Sources */, + 4A9011A41FA6032E00494D14 /* MockWebViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 4A427B901FA4AF6700B0E95D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 4A427B761FA4AF6700B0E95D /* DesafioIOS */; + targetProxy = 4A427B8F1FA4AF6700B0E95D /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 4A427B7E1FA4AF6700B0E95D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 4A427B7F1FA4AF6700B0E95D /* Base */, + 4AACED8D1FA674EC007A02CF /* pt-BR */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 4A427B861FA4AF6700B0E95D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 4A427B871FA4AF6700B0E95D /* Base */, + 4AACED8E1FA674EC007A02CF /* pt-BR */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; + 4AACED991FA67573007A02CF /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + 4AACED981FA67573007A02CF /* en */, + 4AACED9A1FA67575007A02CF /* pt-BR */, + ); + name = Localizable.strings; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 4A427B951FA4AF6700B0E95D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + 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_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + 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; + CODE_SIGN_IDENTITY = "iPhone Developer"; + 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 = 11.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 4A427B961FA4AF6700B0E95D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + 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_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + 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; + CODE_SIGN_IDENTITY = "iPhone Developer"; + 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 = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 4A427B981FA4AF6700B0E95D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6B19B80C6E3CBF350F139E84 /* Pods-DesafioIOS.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = "$(SRCROOT)/DesafioIOS/Core/Supporting Files/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.nexaas.challenge.DesafioIOS; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "DesafioIOS/Core/Supporting Files/Bridging-Header.h"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 4A427B991FA4AF6700B0E95D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 4BA7F1716C11BA8B1E0EB9CD /* Pods-DesafioIOS.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = "$(SRCROOT)/DesafioIOS/Core/Supporting Files/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.nexaas.challenge.DesafioIOS; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "DesafioIOS/Core/Supporting Files/Bridging-Header.h"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 4A427B9B1FA4AF6700B0E95D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 181FA2BEBF8895A2D9637B8D /* Pods-DesafioIOSTests.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = DesafioIOSTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.nexaas.challenge.DesafioIOSTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DesafioIOS.app/DesafioIOS"; + }; + name = Debug; + }; + 4A427B9C1FA4AF6700B0E95D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E892893D796E7ECB915AC464 /* Pods-DesafioIOSTests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = DesafioIOSTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.nexaas.challenge.DesafioIOSTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DesafioIOS.app/DesafioIOS"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 4A427B721FA4AF6700B0E95D /* Build configuration list for PBXProject "DesafioIOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4A427B951FA4AF6700B0E95D /* Debug */, + 4A427B961FA4AF6700B0E95D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4A427B971FA4AF6700B0E95D /* Build configuration list for PBXNativeTarget "DesafioIOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4A427B981FA4AF6700B0E95D /* Debug */, + 4A427B991FA4AF6700B0E95D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4A427B9A1FA4AF6700B0E95D /* Build configuration list for PBXNativeTarget "DesafioIOSTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4A427B9B1FA4AF6700B0E95D /* Debug */, + 4A427B9C1FA4AF6700B0E95D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 4A427B6F1FA4AF6700B0E95D /* Project object */; +} diff --git a/DesafioIOS/DesafioIOS/AppDelegate.swift b/DesafioIOS/DesafioIOS/AppDelegate.swift new file mode 100644 index 0000000..0d119c1 --- /dev/null +++ b/DesafioIOS/DesafioIOS/AppDelegate.swift @@ -0,0 +1,65 @@ +// +// AppDelegate.swift +// DesafioIOS +// +// Created by Felipe Ricieri on 28/10/17. +// Copyright © 2017 Nexaas. All rights reserved. +// + +import UIKit +import CoreData +import SVProgressHUD + +/** + * Application Delegate + */ +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + /** + * Main window + */ + var window: UIWindow? + + // Setup + + /** + * buildApplication() + * @description Customizes application + */ + func buildApplication() { + + // Status bar + UIApplication.shared.statusBarStyle = UIStatusBarStyle.lightContent + + // Navigation Bar custom colors + UINavigationBar.appearance().titleTextAttributes = [NSAttributedStringKey.foregroundColor: UIColor.white] + UINavigationBar.appearance().tintColor = UIColor.white + + // ProgressHUD custom colors + SVProgressHUD.setBackgroundColor(UIColor.navigationBarColor) + SVProgressHUD.setForegroundColor(UIColor.white) + + // Prepare root controller + if let navigationController = window!.rootViewController as? UINavigationController, + let repositoriesController = navigationController.viewControllers.first as? RepositoriesViewController { + repositoriesController.viewModel = RepositoriesViewModel() + } + } + + // MARK: - 👽 Lifecycle Methods + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { + buildApplication() + return true + } + + // MARK: - 🎃 Reachability Listeners + func applicationDidBecomeActive(_ application: UIApplication) { + ReachabilityManager.subscribe() + } + + func applicationDidEnterBackground(_ application: UIApplication) { + ReachabilityManager.unsubscribe() + } +} + diff --git a/DesafioIOS/DesafioIOS/Assets.xcassets/AppIcon.appiconset/Contents.json b/DesafioIOS/DesafioIOS/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..19882d5 --- /dev/null +++ b/DesafioIOS/DesafioIOS/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,53 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/DesafioIOS/DesafioIOS/Assets.xcassets/Contents.json b/DesafioIOS/DesafioIOS/Assets.xcassets/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/DesafioIOS/DesafioIOS/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/DesafioIOS/DesafioIOS/Assets.xcassets/avatar_noimage.imageset/Contents.json b/DesafioIOS/DesafioIOS/Assets.xcassets/avatar_noimage.imageset/Contents.json new file mode 100644 index 0000000..b8321a2 --- /dev/null +++ b/DesafioIOS/DesafioIOS/Assets.xcassets/avatar_noimage.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "avatar_noimage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/DesafioIOS/DesafioIOS/Assets.xcassets/avatar_noimage.imageset/avatar_noimage@2x.png b/DesafioIOS/DesafioIOS/Assets.xcassets/avatar_noimage.imageset/avatar_noimage@2x.png new file mode 100644 index 0000000..34fe8ee Binary files /dev/null and b/DesafioIOS/DesafioIOS/Assets.xcassets/avatar_noimage.imageset/avatar_noimage@2x.png differ diff --git a/DesafioIOS/DesafioIOS/Assets.xcassets/ic_back.imageset/Contents.json b/DesafioIOS/DesafioIOS/Assets.xcassets/ic_back.imageset/Contents.json new file mode 100644 index 0000000..071d9a9 --- /dev/null +++ b/DesafioIOS/DesafioIOS/Assets.xcassets/ic_back.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_back@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ic_back@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/DesafioIOS/DesafioIOS/Assets.xcassets/ic_back.imageset/ic_back@2x.png b/DesafioIOS/DesafioIOS/Assets.xcassets/ic_back.imageset/ic_back@2x.png new file mode 100644 index 0000000..5c6ec54 Binary files /dev/null and b/DesafioIOS/DesafioIOS/Assets.xcassets/ic_back.imageset/ic_back@2x.png differ diff --git a/DesafioIOS/DesafioIOS/Assets.xcassets/ic_back.imageset/ic_back@3x.png b/DesafioIOS/DesafioIOS/Assets.xcassets/ic_back.imageset/ic_back@3x.png new file mode 100644 index 0000000..bc400d6 Binary files /dev/null and b/DesafioIOS/DesafioIOS/Assets.xcassets/ic_back.imageset/ic_back@3x.png differ diff --git a/DesafioIOS/DesafioIOS/Assets.xcassets/ic_close.imageset/Contents.json b/DesafioIOS/DesafioIOS/Assets.xcassets/ic_close.imageset/Contents.json new file mode 100644 index 0000000..ca59192 --- /dev/null +++ b/DesafioIOS/DesafioIOS/Assets.xcassets/ic_close.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_close@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ic_close@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/DesafioIOS/DesafioIOS/Assets.xcassets/ic_close.imageset/ic_close@2x.png b/DesafioIOS/DesafioIOS/Assets.xcassets/ic_close.imageset/ic_close@2x.png new file mode 100644 index 0000000..cbaea44 Binary files /dev/null and b/DesafioIOS/DesafioIOS/Assets.xcassets/ic_close.imageset/ic_close@2x.png differ diff --git a/DesafioIOS/DesafioIOS/Assets.xcassets/ic_close.imageset/ic_close@3x.png b/DesafioIOS/DesafioIOS/Assets.xcassets/ic_close.imageset/ic_close@3x.png new file mode 100644 index 0000000..a6df3eb Binary files /dev/null and b/DesafioIOS/DesafioIOS/Assets.xcassets/ic_close.imageset/ic_close@3x.png differ diff --git a/DesafioIOS/DesafioIOS/Assets.xcassets/ic_fork.imageset/Contents.json b/DesafioIOS/DesafioIOS/Assets.xcassets/ic_fork.imageset/Contents.json new file mode 100644 index 0000000..2c49515 --- /dev/null +++ b/DesafioIOS/DesafioIOS/Assets.xcassets/ic_fork.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_fork@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ic_fork@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/DesafioIOS/DesafioIOS/Assets.xcassets/ic_fork.imageset/ic_fork@2x.png b/DesafioIOS/DesafioIOS/Assets.xcassets/ic_fork.imageset/ic_fork@2x.png new file mode 100644 index 0000000..147498f Binary files /dev/null and b/DesafioIOS/DesafioIOS/Assets.xcassets/ic_fork.imageset/ic_fork@2x.png differ diff --git a/DesafioIOS/DesafioIOS/Assets.xcassets/ic_fork.imageset/ic_fork@3x.png b/DesafioIOS/DesafioIOS/Assets.xcassets/ic_fork.imageset/ic_fork@3x.png new file mode 100644 index 0000000..a0860fc Binary files /dev/null and b/DesafioIOS/DesafioIOS/Assets.xcassets/ic_fork.imageset/ic_fork@3x.png differ diff --git a/DesafioIOS/DesafioIOS/Assets.xcassets/ic_menu.imageset/Contents.json b/DesafioIOS/DesafioIOS/Assets.xcassets/ic_menu.imageset/Contents.json new file mode 100644 index 0000000..91df290 --- /dev/null +++ b/DesafioIOS/DesafioIOS/Assets.xcassets/ic_menu.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_menu@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ic_menu@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/DesafioIOS/DesafioIOS/Assets.xcassets/ic_menu.imageset/ic_menu@2x.png b/DesafioIOS/DesafioIOS/Assets.xcassets/ic_menu.imageset/ic_menu@2x.png new file mode 100644 index 0000000..7700695 Binary files /dev/null and b/DesafioIOS/DesafioIOS/Assets.xcassets/ic_menu.imageset/ic_menu@2x.png differ diff --git a/DesafioIOS/DesafioIOS/Assets.xcassets/ic_menu.imageset/ic_menu@3x.png b/DesafioIOS/DesafioIOS/Assets.xcassets/ic_menu.imageset/ic_menu@3x.png new file mode 100644 index 0000000..e58c2ff Binary files /dev/null and b/DesafioIOS/DesafioIOS/Assets.xcassets/ic_menu.imageset/ic_menu@3x.png differ diff --git a/DesafioIOS/DesafioIOS/Assets.xcassets/ic_reload.imageset/Contents.json b/DesafioIOS/DesafioIOS/Assets.xcassets/ic_reload.imageset/Contents.json new file mode 100644 index 0000000..edbda68 --- /dev/null +++ b/DesafioIOS/DesafioIOS/Assets.xcassets/ic_reload.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_reload@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ic_reload@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/DesafioIOS/DesafioIOS/Assets.xcassets/ic_reload.imageset/ic_reload@2x.png b/DesafioIOS/DesafioIOS/Assets.xcassets/ic_reload.imageset/ic_reload@2x.png new file mode 100644 index 0000000..b365510 Binary files /dev/null and b/DesafioIOS/DesafioIOS/Assets.xcassets/ic_reload.imageset/ic_reload@2x.png differ diff --git a/DesafioIOS/DesafioIOS/Assets.xcassets/ic_reload.imageset/ic_reload@3x.png b/DesafioIOS/DesafioIOS/Assets.xcassets/ic_reload.imageset/ic_reload@3x.png new file mode 100644 index 0000000..c3747d1 Binary files /dev/null and b/DesafioIOS/DesafioIOS/Assets.xcassets/ic_reload.imageset/ic_reload@3x.png differ diff --git a/DesafioIOS/DesafioIOS/Assets.xcassets/ic_star.imageset/Contents.json b/DesafioIOS/DesafioIOS/Assets.xcassets/ic_star.imageset/Contents.json new file mode 100644 index 0000000..d3a4cae --- /dev/null +++ b/DesafioIOS/DesafioIOS/Assets.xcassets/ic_star.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_star@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ic_star@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/DesafioIOS/DesafioIOS/Assets.xcassets/ic_star.imageset/ic_star@2x.png b/DesafioIOS/DesafioIOS/Assets.xcassets/ic_star.imageset/ic_star@2x.png new file mode 100644 index 0000000..780e64b Binary files /dev/null and b/DesafioIOS/DesafioIOS/Assets.xcassets/ic_star.imageset/ic_star@2x.png differ diff --git a/DesafioIOS/DesafioIOS/Assets.xcassets/ic_star.imageset/ic_star@3x.png b/DesafioIOS/DesafioIOS/Assets.xcassets/ic_star.imageset/ic_star@3x.png new file mode 100644 index 0000000..984d4e8 Binary files /dev/null and b/DesafioIOS/DesafioIOS/Assets.xcassets/ic_star.imageset/ic_star@3x.png differ diff --git a/DesafioIOS/DesafioIOS/Base.lproj/LaunchScreen.storyboard b/DesafioIOS/DesafioIOS/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..0200181 --- /dev/null +++ b/DesafioIOS/DesafioIOS/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DesafioIOS/DesafioIOS/Base.lproj/Main.storyboard b/DesafioIOS/DesafioIOS/Base.lproj/Main.storyboard new file mode 100644 index 0000000..7d19f09 --- /dev/null +++ b/DesafioIOS/DesafioIOS/Base.lproj/Main.storyboard @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + HelveticaNeue-Light + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DesafioIOS/DesafioIOS/Controllers/MainStoryboard.swift b/DesafioIOS/DesafioIOS/Controllers/MainStoryboard.swift new file mode 100644 index 0000000..8c0374d --- /dev/null +++ b/DesafioIOS/DesafioIOS/Controllers/MainStoryboard.swift @@ -0,0 +1,27 @@ +// +// MainStoryboard.swift +// DesafioIOS +// +// Created by Felipe Ricieri on 28/10/17. +// Copyright © 2017 Nexaas. All rights reserved. +// + +import StoryboardContext + +/** + * MainStoryboard + * @description Main Storyboard's representation + */ +class MainStoryboard : StoryboardContext { + + // Existent segues in storyboard + struct Segue { + static let toPullRequest = "toPullRequests" + static let toWebView = "toWebView" + } + + convenience override init() { + self.init(name: "Main") + } +} + diff --git a/DesafioIOS/DesafioIOS/Controllers/PullRequestTableViewCell.swift b/DesafioIOS/DesafioIOS/Controllers/PullRequestTableViewCell.swift new file mode 100644 index 0000000..ad4b8f6 --- /dev/null +++ b/DesafioIOS/DesafioIOS/Controllers/PullRequestTableViewCell.swift @@ -0,0 +1,54 @@ +// +// PullRequestTableViewCell.swift +// DesafioIOS +// +// Created by Felipe Ricieri on 28/10/17. +// Copyright © 2017 Nexaas. All rights reserved. +// + +import UIKit +import SDWebImage + +/** + * PullRequestTableViewCell + * @description Pull Request's table cell + */ +class PullRequestTableViewCell : UITableViewCell, UniqueCell { + + /** + * Cell identifier + */ + static var cellIdentifier: String = "pullRequestCell" + + /** + * Outlets + */ + @IBOutlet weak var nameLabel: UILabel! + @IBOutlet weak var descriptionLabel: UILabel! + @IBOutlet weak var userNicknameLabel: UILabel! + @IBOutlet weak var userNameLabel: UILabel! + @IBOutlet weak var userPicture: UIImageView! + + /** + * configure(object:) + * @description Configures cell with given data + * @param object Pull Request object + */ + func configure(object: PullRequest) { + + // Repository Data + nameLabel.text = object.title + descriptionLabel.text = object.objectDescription + + // Owner Data + guard let owner = object.owner else { return } + userNameLabel.text = owner.name + userNicknameLabel.text = owner.username + if owner.picture != "", + let url = URL(string: owner.picture), + let placeholder = UIImage(named: "avatar_noimage") { + userPicture.sd_setImage(with: url, placeholderImage: placeholder) + } + } +} + diff --git a/DesafioIOS/DesafioIOS/Controllers/PullRequestsViewController.swift b/DesafioIOS/DesafioIOS/Controllers/PullRequestsViewController.swift new file mode 100644 index 0000000..446e98e --- /dev/null +++ b/DesafioIOS/DesafioIOS/Controllers/PullRequestsViewController.swift @@ -0,0 +1,236 @@ +// +// PullRequestsViewController.swift +// DesafioIOS +// +// Created by Felipe Ricieri on 28/10/17. +// Copyright © 2017 Nexaas. All rights reserved. +// + +import UIKit + +/** + * PullRequestsViewController + * @description PullRequests list screen + */ +class PullRequestsViewController : UITableViewController, Hud { + + /** + * View Model + */ + var viewModel : PullRequestsViewModel! + + /** + * Outlets + */ + @IBOutlet weak var tableHeader : UIView? + @IBOutlet weak var pullRequestsCountLabel : UILabel? + + // Setup + + /** + * setup() + * @description Initial State + */ + private func setup() { + + // Title + title = viewModel.repositoryName + navigationItem.title = viewModel.repositoryName + + // Open/Close repos label + pullRequestsCountLabel?.text = "" + + // Refresh Control + refreshControl = UIRefreshControl() + refreshControl!.tintColor = UIColor.navigationBarColor + refreshControl!.attributedTitle = nil + refreshControl!.addTarget(self, action: #selector(PullRequestsViewController.refresh), for: .valueChanged) + tableView.addSubview(refreshControl!) + + // Table cell height + tableView.rowHeight = UITableViewAutomaticDimension + tableView.estimatedRowHeight = 140 + + // Load Data + triggerRefreshControl() + } + + // MARK: - 👽 Lifecycle Methods + + override func viewDidLoad() { + super.viewDidLoad() + self.setup() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.addObservers() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + self.removeObservers() + } + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + if segue.identifier == MainStoryboard.Segue.toWebView { + if let nav = segue.destination as? UINavigationController, + let vc = nav.viewControllers.first as? WebViewController, + let pull = sender as? PullRequest { + vc.viewModel = WebViewModel(pullRequest: pull) + } + } + } + + // MARK: - 🌳 Data + + /** + * refresh() + * @description Pull-to-Refresh action + */ + @objc func refresh() { + viewModel.refresh() + } + + /** + * triggerRefreshControl() + * @description Set the refresh control to its initial state & fires it + */ + func triggerRefreshControl() { + if let safeControl = self.refreshControl { + safeControl.beginRefreshing() + self.tableView.setContentOffset(CGPoint(x: 0, y: -safeControl.frame.size.height), animated: true) + } + refresh() + } + + // MARK: - 🤖 IB Actions + + /** + * actionBack() + * @description Pops the current ViewController from NavigationController + */ + @IBAction func actionBack() { + _ = self.navigationController?.popViewController(animated: true) + } + + /** + * addObservers() + * @description Subscribes the Screen to receive Reachability events + */ + func addObservers() { + + NotificationCenter.default.subscribe( + observer: self, selector: #selector(PullRequestsViewController.notificationIsReachable(n:)), custom: .reachable) + + NotificationCenter.default.subscribe( + observer: self, selector: #selector(PullRequestsViewController.notificationNotReachable(n:)), custom: .notReachable) + + NotificationCenter.default.subscribe( + observer: self, selector: #selector(PullRequestsViewController.notificationReloadData(n:)), custom: .reloadData) + + NotificationCenter.default.subscribe( + observer: self, selector: #selector(PullRequestsViewController.notificationPreparePullsCount(n:)), custom: .preparePullsCount) + + NotificationCenter.default.subscribe( + observer: self, selector: #selector(RepositoriesViewController.notificationDidReceiveError(n:)), custom: .didReceiveError) + } + + /** + * removeObservers() + * @description Unsubscribes the Reachability events + */ + func removeObservers() { + NotificationCenter.default.unsubscribe(observer: self) + } + + // MARK: - 🎃 Reachability + + /** + * notificationIsReachable(n:) + * @description Selector action for when connection is on + * @param n NotificationCenter's notification + */ + @objc func notificationIsReachable(n: Notification) { + guard viewModel.source.count == 0 else { return } + if !viewModel.isProcessing { + triggerRefreshControl() + } + } + + /** + * notificationNotReachable(n:) + * @description Selector action for when connection is off + * @param n NotificationCenter's notification + */ + @objc func notificationNotReachable(n: Notification) { + errorHud("Error.YouAreOffline".localized) + } + + // MARK: - 🚀 Reactions + + /** + * notificationReloadData(n:) + * @description View Model's message to reload data + * @param n NotificationCenter's notification + */ + @objc func notificationReloadData(n: Notification) { + tableView.reloadData() + refreshControl?.endRefreshing() + } + + /** + * notificationPreparePullsCount(n:) + * @description View Model's message to any errors received + * @param n NotificationCenter's notification + */ + @objc func notificationPreparePullsCount(n: Notification) { + let openText = "\(viewModel.openPullsCount) \("opened".localized)" + let text = "\(openText) / \(viewModel.closedPullsCount) \("closed".localized)" + let attrStr = NSMutableAttributedString(string: text) + attrStr.addAttribute(NSAttributedStringKey.foregroundColor, value: UIColor.highlightColor, range: NSMakeRange(0, openText.characters.count)) + pullRequestsCountLabel?.attributedText = attrStr + } + + /** + * notificationDidReceiveError(n:) + * @description View Model's message to any errors received + * @param n NotificationCenter's notification + */ + @objc func notificationDidReceiveError(n: Notification) { + refreshControl?.endRefreshing() + guard let message = n.object as? String else { return } + errorHud(message) + } +} + +// MARK: - 📦 Table Delegate & DataSource +extension PullRequestsViewController { + + // Rows + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + + let object = viewModel.source[indexPath.row] + let cell = tableView.dequeueReusableCell(withIdentifier: PullRequestTableViewCell.cellIdentifier, for: indexPath) as! PullRequestTableViewCell + cell.prepareForReuse() + cell.configure(object: object) + + return cell + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let object = viewModel.source[indexPath.row] + tableView.deselectRow(at: indexPath, animated: true) + performSegue(withIdentifier: MainStoryboard.Segue.toWebView, sender: object) + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return viewModel.source.count + } + + // Sections + override func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } +} + diff --git a/DesafioIOS/DesafioIOS/Controllers/PullRequestsViewModel.swift b/DesafioIOS/DesafioIOS/Controllers/PullRequestsViewModel.swift new file mode 100644 index 0000000..59f9eb3 --- /dev/null +++ b/DesafioIOS/DesafioIOS/Controllers/PullRequestsViewModel.swift @@ -0,0 +1,132 @@ +// +// PullRequestsViewModel.swift +// DesafioIOS +// +// Created by Felipe Ricieri on 28/10/17. +// Copyright © 2017 Nexaas. All rights reserved. +// + +import Foundation + +/** + * PullRequestsViewModel + * @description PullRequestsViewController's View Model + */ +class PullRequestsViewModel { + + /** + * Repository to fetch Pull Requests + */ + var repository : Repository + + /** + * Repository's name + */ + var repositoryName : String { + get { + return repository.name + } + } + + /** + * Pull Request Micro Service + */ + fileprivate var service = PullRequestService() + + /** + * Pull Request Micro Service + */ + fileprivate(set) var source = [PullRequest]() + + /** + * Processing State + */ + fileprivate(set) var isProcessing = false + + /** + * Open Pulls count + */ + fileprivate(set) var openPullsCount = 0 + + /** + * Closed Pulls count + */ + fileprivate(set) var closedPullsCount = 0 + + + // MARK: - 👽 Lifecycle Methods + + /** + * Constructor + */ + init(repository: Repository) { + self.repository = repository + } + + + // MARK: - 🔐 Common Methods + + + /** + * refresh() + * @description Toggles processing state & reset the data + */ + func refresh() { + guard !isProcessing else { return } + isProcessing = true + // Re-fetch + fetchData() + } + + /** + * fetchData(completion:) + * @description Fires Pull Request micro service request + * @param completion Callback fired when request is completed + */ + func fetchData(completion: (() -> Void)?=nil) { + + // Required data + guard let safeOwner = repository.owner else { + NotificationCenter.default.post(.didReceiveError, object: "Error.FailedRepositoryRequest".localized) + completion?() + return + } + + // Send request + service.load(owner: safeOwner.username, repository: repository.name, succeed: { [weak self] results in + + guard let this = self else { return } + + // Check state + for pull in results { + // Open pulls + if pull.state == "open" { + this.openPullsCount += 1 + } + // Closed pulls + if pull.state == "closed" { + this.closedPullsCount += 1 + } + } + + // Fill label + NotificationCenter.default.post(.preparePullsCount) + + // Fill source + this.source = results + this.isProcessing = false + NotificationCenter.default.post(.reloadData) + completion?() + + }) { [weak self] errorDescription in + + guard let this = self else { return } + + // Show error + this.isProcessing = false + NotificationCenter.default.post(.didReceiveError, object: errorDescription) + completion?() + } + } +} + diff --git a/DesafioIOS/DesafioIOS/Controllers/RepositoriesViewController.swift b/DesafioIOS/DesafioIOS/Controllers/RepositoriesViewController.swift new file mode 100644 index 0000000..8d2898a --- /dev/null +++ b/DesafioIOS/DesafioIOS/Controllers/RepositoriesViewController.swift @@ -0,0 +1,232 @@ +// +// RepositoriesViewController.swift +// DesafioIOS +// +// Created by Felipe Ricieri on 28/10/17. +// Copyright © 2017 Nexaas. All rights reserved. +// + +import UIKit + +/** + * RepositoriesViewController + * @description Repositories list screen + */ +class RepositoriesViewController : UITableViewController, Hud { + + /** + * Class View Model + */ + var viewModel : RepositoriesViewModel! + + /** + * Outlets + */ + @IBOutlet weak var infiniteScrollingView: UIActivityIndicatorView? + + // Setup + + /** + * setup() + * @description Initial State + */ + private func setup() { + + // Table cell height + tableView.rowHeight = UITableViewAutomaticDimension + tableView.estimatedRowHeight = 130 + + // Infinite Scrolling + infiniteScrollingView?.alpha = 0 + + // Refresh Control + refreshControl = UIRefreshControl() + refreshControl!.tintColor = UIColor.navigationBarColor + refreshControl!.attributedTitle = nil + refreshControl!.addTarget(self, action: #selector(RepositoriesViewController.refresh), for: .valueChanged) + tableView.addSubview(refreshControl!) + + // Load Repositories + triggerRefreshControl() + } + + // MARK: - 👽 Lifecycle Methods + + override func viewDidLoad() { + super.viewDidLoad() + self.setup() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.addObservers() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + self.removeObservers() + } + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + if segue.identifier == MainStoryboard.Segue.toPullRequest { + let vc = segue.destination as! PullRequestsViewController + guard let repository = sender as? Repository else { return } + vc.viewModel = PullRequestsViewModel(repository: repository) + } + } + + // MARK: - 🌳 Data + + /** + * refresh() + * @description Pull-to-Refresh action + */ + @objc func refresh() { + viewModel.refresh() + } + + /** + * triggerRefreshControl() + * @description Set the refresh control to its initial state & fires it + */ + func triggerRefreshControl() { + if let safeControl = self.refreshControl { + safeControl.beginRefreshing() + tableView.setContentOffset(CGPoint(x: 0, y: -safeControl.frame.size.height), animated: true) + } + refresh() + } + + /** + * addObservers() + * @description Subscribes the Screen to receive Reachability events + */ + func addObservers() { + + NotificationCenter.default.subscribe( + observer: self, selector: #selector(RepositoriesViewController.notificationIsReachable(n:)), custom: .reachable) + + NotificationCenter.default.subscribe( + observer: self, selector: #selector(RepositoriesViewController.notificationNotReachable(n:)), custom: .notReachable) + + NotificationCenter.default.subscribe( + observer: self, selector: #selector(RepositoriesViewController.notificationReloadData(n:)), custom: .reloadData) + + NotificationCenter.default.subscribe( + observer: self, selector: #selector(RepositoriesViewController.notificationDidStartLoading(n:)), custom: .didStartLoading) + + NotificationCenter.default.subscribe( + observer: self, selector: #selector(RepositoriesViewController.notificationDidFinishLoading(n:)), custom: .didFinishLoading) + + NotificationCenter.default.subscribe( + observer: self, selector: #selector(RepositoriesViewController.notificationDidReceiveError(n:)), custom: .didReceiveError) + } + + /** + * removeObservers() + * @description Unsubscribes the Reachability events + */ + func removeObservers() { + NotificationCenter.default.unsubscribe(observer: self) + } + + // MARK: - 🎃 Reachability + + /** + * notificationIsReachable(n:) + * @description Selector action for when connection is on + * @param n NotificationCenter's notification + */ + @objc func notificationIsReachable(n: Notification) { + guard viewModel.source.count == 0 else { return } + if !viewModel.isProcessing { + triggerRefreshControl() + } + } + + /** + * notificationNotReachable(n:) + * @description Selector action for when connection is off + * @param n NotificationCenter's notification + */ + @objc func notificationNotReachable(n: Notification) { + errorHud("Error.YouAreOffline".localized) + } + + // MARK: - 🚀 Reactions + + /** + * notificationReloadData(n:) + * @description View Model's message to reload data + * @param n NotificationCenter's notification + */ + @objc func notificationReloadData(n: Notification) { + tableView.reloadData() + refreshControl?.endRefreshing() + } + + /** + * notificationDidStartLoading(n:) + * @description View Model's message to fire infinite scrolling UI + * @param n NotificationCenter's notification + */ + @objc func notificationDidStartLoading(n: Notification) { + infiniteScrollingView?.alpha = 1 + } + + /** + * notificationDidFinishLoading(n:) + * @description View Model's message to stop infinite scrolling UI + * @param n NotificationCenter's notification + */ + @objc func notificationDidFinishLoading(n: Notification) { + infiniteScrollingView?.alpha = 0 + } + + /** + * notificationDidReceiveError(n:) + * @description View Model's message to any errors received + * @param n NotificationCenter's notification + */ + @objc func notificationDidReceiveError(n: Notification) { + refreshControl?.endRefreshing() + guard let message = n.object as? String else { return } + errorHud(message) + } +} + +// MARK: - 📦 Table Delegate & DataSource +extension RepositoriesViewController { + + // Rows + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + + let object = viewModel.source[indexPath.row] + let cell = tableView.dequeueReusableCell(withIdentifier: RepositoryTableViewCell.cellIdentifier, for: indexPath) as! RepositoryTableViewCell + cell.prepareForReuse() + cell.configure(object: object) + + return cell + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let object = viewModel.source[indexPath.row] + tableView.deselectRow(at: indexPath, animated: true) + performSegue(withIdentifier: MainStoryboard.Segue.toPullRequest, sender: object) + } + + override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { + guard indexPath.row == (viewModel.source.count - 1) else { return } + viewModel.triggerInfiniteScrolling() + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return viewModel.source.count + } + + // Sections + override func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } +} + diff --git a/DesafioIOS/DesafioIOS/Controllers/RepositoriesViewModel.swift b/DesafioIOS/DesafioIOS/Controllers/RepositoriesViewModel.swift new file mode 100644 index 0000000..7cf8bb0 --- /dev/null +++ b/DesafioIOS/DesafioIOS/Controllers/RepositoriesViewModel.swift @@ -0,0 +1,113 @@ +// +// RepositoriesViewModel.swift +// DesafioIOS +// +// Created by Felipe Ricieri on 28/10/17. +// Copyright © 2017 Nexaas. All rights reserved. +// + +import Foundation + +/** + * RepositoriesViewModel + * @description RepositoriesViewController's View Model + */ +class RepositoriesViewModel { + + /** + * Repository Micro Service + */ + fileprivate var service = RepositoryService() + + /** + * Repository source + */ + fileprivate(set) var source = [Repository]() + + /** + * Current page + */ + fileprivate(set) var page = 1 + + /** + * Processing state + */ + fileprivate(set) var isProcessing = false + + + // MARK: - 🔐 Common Methods + + + /** + * refresh() + * @description Toggles processing state & reset the data + */ + func refresh() { + + guard !isProcessing else { return } + isProcessing = true + + // Reset + page = 1 + + // Re-fetch + fetchData() { + NotificationCenter.default.post(.didFinishRefreshing) + } + } + + /** + * fetchData(completion:) + * @description Fires Repository micro service request + * @param completion Callback fired when request is completed + */ + func fetchData(completion: (() -> Void)?=nil) { + + // Send request + service.load(page: self.page, succeed: { [weak self] results in + + guard let this = self else { return } + + if this.page == 1 { + this.source = [] + } + this.source.append(contentsOf: results) + this.isProcessing = false + NotificationCenter.default.post(.reloadData) + completion?() + + }) { [weak self] errorDescription in + + guard let this = self else { return } + + // Show error + NotificationCenter.default.post(.didReceiveError, object: errorDescription) + this.isProcessing = false + } + } + + // MARK: - 🌀 Infinite Scrolling + + /** + * triggerInfiniteScrolling(completion:) + * @description Toggles processing state & loads next page + * @param completion Callback fired when request is completed + */ + func triggerInfiniteScrolling(completion: (()->Void)?=nil) { + + guard !isProcessing else { + completion?() + return + } + + page += 1 + isProcessing = true + NotificationCenter.default.post(.didStartLoading) + + fetchData() { + NotificationCenter.default.post(.didFinishLoading) + completion?() + } + } +} + diff --git a/DesafioIOS/DesafioIOS/Controllers/RepositoryTableViewCell.swift b/DesafioIOS/DesafioIOS/Controllers/RepositoryTableViewCell.swift new file mode 100644 index 0000000..75ed448 --- /dev/null +++ b/DesafioIOS/DesafioIOS/Controllers/RepositoryTableViewCell.swift @@ -0,0 +1,69 @@ +// +// RepositoryTableViewCell.swift +// DesafioIOS +// +// Created by Felipe Ricieri on 28/10/17. +// Copyright © 2017 Nexaas. All rights reserved. +// + +import UIKit +import SDWebImage + +/** + * RepositoryTableViewCell + * @description Repository's table cell + */ +class RepositoryTableViewCell : UITableViewCell, UniqueCell { + + /** + * Cell identifier + */ + static var cellIdentifier: String = "repositoryCell" + + /** + * Outlets + */ + @IBOutlet weak var nameLabel: UILabel! + @IBOutlet weak var descriptionLabel: UILabel! + @IBOutlet weak var forksCountLabel: UILabel! + @IBOutlet weak var starsCountLabel: UILabel! + @IBOutlet weak var userNicknameLabel: UILabel! + @IBOutlet weak var userNameLabel: UILabel! + @IBOutlet weak var userPicture: UIImageView! + + /** + * configure(object:) + * @description Configures cell with given data + * @param object Repository object + */ + func configure(object: Repository) { + + // Repository Data + nameLabel.text = object.fullName + descriptionLabel.text = object.objectDescription + + // Format value + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + formatter.groupingSeparator = "." + formatter.usesGroupingSeparator = true + + let forksNumber = NSNumber(value: object.forks) + let starsNumber = NSNumber(value: object.stars) + + forksCountLabel.text = formatter.string(from: forksNumber) + starsCountLabel.text = formatter.string(from: starsNumber) + + // Owner Data + guard let owner = object.owner else { return } + userNameLabel.text = owner.name + userNicknameLabel.text = owner.username + if owner.picture != "", + let url = URL(string: owner.picture), + let placeholder = UIImage(named: "avatar_noimage") { + userPicture.sd_setImage(with: url, placeholderImage: placeholder) + } + } +} + + diff --git a/DesafioIOS/DesafioIOS/Controllers/WebViewController.swift b/DesafioIOS/DesafioIOS/Controllers/WebViewController.swift new file mode 100644 index 0000000..43769a8 --- /dev/null +++ b/DesafioIOS/DesafioIOS/Controllers/WebViewController.swift @@ -0,0 +1,158 @@ +// +// WebViewController.swift +// DesafioIOS +// +// Created by Felipe Ricieri on 28/10/17. +// Copyright © 2017 Nexaas. All rights reserved. +// + +import UIKit + +/** + * WebViewController + * @description Opens internally the url requests + */ +class WebViewController : UIViewController, Hud { + + /** + * Class View Model + */ + var viewModel : WebViewModel! + + /** + * Outlets + */ + @IBOutlet weak var webView: UIWebView? + + + // MARK: - 👽 Lifecycle Methods + + override func viewDidLoad() { + super.viewDidLoad() + title = "" + navigationItem.title = "" + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + self.updateUI() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.addObservers() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + self.removeObservers() + } + + /** + * addObservers() + * @description Subscribes the Screen to receive Reachability events + */ + public func addObservers() { + + NotificationCenter.default.subscribe( + observer: self, selector: #selector(WebViewController.notificationIsReachable(n:)), custom: .reachable) + + NotificationCenter.default.subscribe( + observer: self, selector: #selector(WebViewController.notificationNotReachable(n:)), custom: .notReachable) + } + + /** + * removeObservers() + * @description Unsubscribes the Reachability events + */ + public func removeObservers() { + NotificationCenter.default.unsubscribe(observer: self) + } + + // MARK: - 🔐 Internal Methods + + /** + * loadWebView() + * @description Loads WebView UI + */ + func loadWebView() { + + guard let safeUrl = viewModel.pullRequestUrl else { + errorHud("Error.NoURL".localized) + return + } + + let urlRequest = URLRequest(url: safeUrl) + webView?.loadRequest(urlRequest) + } + + func updateUI() { + self.title = viewModel.pullRequestTitle + self.navigationItem.title = viewModel.pullRequestTitle + self.loadWebView() + } + + // MARK: - 🤖 IB Actions + + /** + * actionDismiss() + * @description Dismiss the current ViewController + */ + @IBAction func actionDismiss() { + dismiss(animated: true, completion: nil) + } + + // MARK: - 🎃 Reachability + + /** + * notificationIsReachable(n:) + * @description Selector action for when connection is on + * @param n NotificationCenter's notification + */ + @objc func notificationIsReachable(n: Notification) { + if viewModel.didFail && !viewModel.isProcessing { + loadWebView() + } + } + + /** + * notificationNotReachable(n:) + * @description Selector action for when connection is off + * @param n NotificationCenter's notification + */ + @objc func notificationNotReachable(n: Notification) { + errorHud("Error.YouAreOffline".localized) + } +} + +// MARK: - 🍎 Webview Delegate +extension WebViewController : UIWebViewDelegate { + + func webViewDidStartLoad(_ webView: UIWebView) { + viewModel.didFail = false + viewModel.isProcessing = true + } + + func webViewDidFinishLoad(_ webView: UIWebView) { + viewModel.isProcessing = false + } + + func webView(_ webView: UIWebView, didFailLoadWithError error: Error) { + + viewModel.didFail = true + viewModel.isProcessing = false + + // Show Error + let alert = UIAlertController(title: "Error.Title".localized, message: error.localizedDescription, preferredStyle: .alert) + let yesAction = UIAlertAction(title: "TryAgain".localized, style: .destructive) { [weak self](action) in + self?.loadWebView() + } + let noAction = UIAlertAction(title: "Close".localized, style: .cancel, handler: { (action) in + // Nothing + }) + alert.addAction(yesAction) + alert.addAction(noAction) + self.present(alert, animated: true, completion: nil) + } +} + diff --git a/DesafioIOS/DesafioIOS/Controllers/WebViewModel.swift b/DesafioIOS/DesafioIOS/Controllers/WebViewModel.swift new file mode 100644 index 0000000..e0ba090 --- /dev/null +++ b/DesafioIOS/DesafioIOS/Controllers/WebViewModel.swift @@ -0,0 +1,62 @@ +// +// WebViewModel.swift +// DesafioIOS +// +// Created by Felipe Ricieri on 28/10/17. +// Copyright © 2017 Nexaas. All rights reserved. +// + +import Foundation + +/** + * WebViewModel + * @description WebViewController's View Model + */ +class WebViewModel { + + /** + * Pull Request Title + */ + var pullRequestTitle : String { + get { + return pullRequest.title + } + } + + /** + * Pull Request URL + */ + var pullRequestUrl : URL? { + get { + return pullRequest.htmlUrl + } + } + + /** + * Failing state + */ + var didFail = false + + /** + * Processing state + */ + var isProcessing = false + + /** + * Pull Request reference + */ + fileprivate var pullRequest: PullRequest + + + // MARK: - 👽 Lifecycle Methods + + + /** + * Constructor + */ + init(pullRequest: PullRequest) { + self.pullRequest = pullRequest + } +} + + diff --git a/DesafioIOS/DesafioIOS/Core/DropShadowNavigationBar.swift b/DesafioIOS/DesafioIOS/Core/DropShadowNavigationBar.swift new file mode 100644 index 0000000..17aafd5 --- /dev/null +++ b/DesafioIOS/DesafioIOS/Core/DropShadowNavigationBar.swift @@ -0,0 +1,45 @@ +// +// DropShadowNavigationBar.swift +// DesafioIOS +// +// Created by Felipe Ricieri on 28/10/17. +// Copyright © 2017 Nexaas. All rights reserved. +// + +import UIKit + +/** + * DropShadowNavigationBar + * @description Custom Navigation bar with slight drop shadow + */ +class DropShadowNavigationBar : UINavigationBar { + + // MARK: - 👽 Lifecycle Methods + + override func layoutSubviews() { + super.layoutSubviews() + self.addDropShadow() + } + + /** + * addDropShadow() + * @description Adds Slight drop shadow to Navigation Bar + */ + private func addDropShadow() { + + self.layer.shadowColor = UIColor.black.cgColor + self.layer.shadowOpacity = 0.2 + self.layer.shadowOffset = CGSize(width: 0, height: 4) + + let shadowPath = CGRect( + x: self.layer.bounds.origin.x - 10, + y: self.layer.bounds.size.height - 6, + width: self.layer.bounds.size.width + 20, + height: 5 + ) + + self.layer.shadowPath = UIBezierPath(rect: shadowPath).cgPath + self.layer.shouldRasterize = true + } +} + diff --git a/DesafioIOS/DesafioIOS/Core/Foundation+Extensions.swift b/DesafioIOS/DesafioIOS/Core/Foundation+Extensions.swift new file mode 100644 index 0000000..75eb2c0 --- /dev/null +++ b/DesafioIOS/DesafioIOS/Core/Foundation+Extensions.swift @@ -0,0 +1,16 @@ +// +// Foundation+Extensions.swift +// DesafioIOS +// +// Created by Felipe Ricieri on 28/10/17. +// Copyright © 2017 Nexaas. All rights reserved. +// + +import Foundation + +extension String { + var localized: String { + return NSLocalizedString(self, tableName: nil, bundle: Bundle.main, value: "", comment: "") + } +} + diff --git a/DesafioIOS/DesafioIOS/Core/NotificationCenter+Custom.swift b/DesafioIOS/DesafioIOS/Core/NotificationCenter+Custom.swift new file mode 100644 index 0000000..992453c --- /dev/null +++ b/DesafioIOS/DesafioIOS/Core/NotificationCenter+Custom.swift @@ -0,0 +1,48 @@ +// +// NotificationCenter+Custom.swift +// DesafioIOS +// +// Created by Felipe Ricieri on 08/11/2017. +// Copyright © 2017 Nexaas. All rights reserved. +// + +import Foundation + +extension Notification { + + // Notification Names + enum CustomName : String { + + case reachable + case notReachable + + case reloadData + case didStartLoading + case didFinishLoading + case didFinishRefreshing + case didReceiveError + + case preparePullsCount + case launchUrl + + func asNotificationName() -> Notification.Name { + return Notification.Name(self.rawValue) + } + } +} + +extension NotificationCenter { + + func post(_ custom: Notification.CustomName, object: Any? = nil) { + post(name: custom.asNotificationName(), object: object) + } + + func subscribe(observer: Any, selector: Selector, custom: Notification.CustomName, object: Any? = nil) { + addObserver( + observer, selector: selector, name: custom.asNotificationName(), object: object) + } + + func unsubscribe(observer: Any) { + removeObserver(observer) + } +} diff --git a/DesafioIOS/DesafioIOS/Core/PullRequestService.swift b/DesafioIOS/DesafioIOS/Core/PullRequestService.swift new file mode 100644 index 0000000..bcbdde8 --- /dev/null +++ b/DesafioIOS/DesafioIOS/Core/PullRequestService.swift @@ -0,0 +1,55 @@ +// +// PullRequestService.swift +// DesafioIOS +// +// Created by Felipe Ricieri on 28/10/17. +// Copyright © 2017 Nexaas. All rights reserved. +// + +import Foundation + +/** + * RepositoryService + * @description Local Micro-service responsible only for PR's requests + */ +public class PullRequestService { + + /** + * load(owner:repository:succeed:failed:) + * @description Load Github repositories + * @param owner Repository owner + * @param repository Repository slug + * @param succeed Callback fired when request succeed + * @param failed Callback fired when request failed + */ + public func load(owner: String, repository: String, succeed: @escaping ([PullRequest]) -> Void, failed: @escaping (String) -> Void) { + + // Send Request + RestClient.pullRequests(owner: owner, repository: repository) { (didSucceed, data) in + + // Catch errors + guard didSucceed else { + if let error = data as? Error { + failed(error.localizedDescription) + } + else { + failed("Error.Unknown".localized) + } + return + } + + // Unleash data + if let json = data as? [[String:Any]] { + var results = [PullRequest]() + for item in json { + let object = PullRequest(jsonData: item) + results.append(object) + } + succeed(results) + } + else { + failed("Error.FailedRequest".localized) + } + } + } +} diff --git a/DesafioIOS/DesafioIOS/Core/ReachabilityManager.swift b/DesafioIOS/DesafioIOS/Core/ReachabilityManager.swift new file mode 100644 index 0000000..ab4a3e2 --- /dev/null +++ b/DesafioIOS/DesafioIOS/Core/ReachabilityManager.swift @@ -0,0 +1,76 @@ +// +// ReachabilityManager.swift +// DesafioIOS +// +// Created by Felipe Ricieri on 28/10/17. +// Copyright © 2017 Nexaas. All rights reserved. +// + +import Foundation +import ReachabilitySwift + +/** + * Reachability Manager 🎃 + * @description Manager responsible for reporting the connection state to the app + */ +class ReachabilityManager { + + /** + * ReachabilityManager subscription state + */ + static var isSubscribed : Bool = false + + /** + * Private singleton + */ + private static let reachability = Reachability() + private init() {} + + /** + * subscribe() + * @description Subscribes ReachabilityManager to listen connection state changes + */ + class func subscribe() { + + guard let reachability = ReachabilityManager.reachability else { return } + + // When reachable... + reachability.whenReachable = { reachability in + DispatchQueue.main.async { + if reachability.isReachable { + print("ReachabilitySwift -> Reachable") + NotificationCenter.default.post(.reachable) + } + } + } + + // When not reachable... + reachability.whenUnreachable = { reachability in + DispatchQueue.main.async { + print("ReachabilitySwift -> Not reachable") + NotificationCenter.default.post(.notReachable) + } + } + + // Fire + do { + try reachability.startNotifier() + ReachabilityManager.isSubscribed = true + } catch let error { + print("ReachabilitySwift -> Unable to start notifier") + print("ReachabilitySwift Error -> \(error.localizedDescription)") + } + } + + /** + * unsubscribe() + * @description Unsubscribes ReachabilityManager to listen connection state changes + */ + class func unsubscribe() { + if let reachability = ReachabilityManager.reachability { + reachability.stopNotifier() + ReachabilityManager.isSubscribed = false + } + } +} + diff --git a/DesafioIOS/DesafioIOS/Core/RepositoryService.swift b/DesafioIOS/DesafioIOS/Core/RepositoryService.swift new file mode 100644 index 0000000..5b8fe13 --- /dev/null +++ b/DesafioIOS/DesafioIOS/Core/RepositoryService.swift @@ -0,0 +1,55 @@ +// +// RepositoryService.swift +// DesafioIOS +// +// Created by Felipe Ricieri on 28/10/17. +// Copyright © 2017 Nexaas. All rights reserved. +// + +import Foundation + +/** + * RepositoryService + * @description Local Micro-service responsible only for repository requests + */ +public class RepositoryService { + + /** + * load(page:succeed:failed:) + * @description Load Github repositories + * @param page Page to be fetched (default is 1) + * @param succeed Callback fired when request succeed + * @param failed Callback fired when request failed + */ + public func load(page: Int=1, succeed: @escaping ([Repository]) -> Void, failed: @escaping (String) -> Void) { + + // Send request + RestClient.repositories(page: page) { (didSucceed, data) in + + // Catch errors + guard didSucceed else { + if let error = data as? Error { + failed(error.localizedDescription) + } + else { + failed("Error.Unknown".localized) + } + return + } + + // Unleash data + if let json = data as? [String:Any], + let items = json["items"] as? [[String:Any]] { + var results = [Repository]() + for item in items { + let object = Repository(jsonData: item) + results.append(object) + } + succeed(results) + } + else { + failed("Error.FailedRequest".localized) + } + } + } +} diff --git a/DesafioIOS/DesafioIOS/Core/RestClient.swift b/DesafioIOS/DesafioIOS/Core/RestClient.swift new file mode 100644 index 0000000..3935718 --- /dev/null +++ b/DesafioIOS/DesafioIOS/Core/RestClient.swift @@ -0,0 +1,76 @@ +// +// RestClient.swift +// DesafioIOS +// +// Created by Felipe Ricieri on 28/10/17. +// Copyright © 2017 Nexaas. All rights reserved. +// + +import Foundation +import Alamofire + +/** + * RestClient + * @description The RESTful interface + */ +class RestClient { + + /** + * Internal typealias for completion + */ + typealias Callback = (Bool, Any?) -> Void + + /** + * sendRequest(url:completion:) + * @description Sends the request to API + * @param url Given URL to fetch the request + * @param completion Callback fired when request is completed + */ + class func sendRequest(url: String, completion: @escaping Callback) { + print("Rest Client calling --> \(url)") + Alamofire + .request(url) + .validate() + .responseJSON { response in + + switch response.result { + case .success: + print("RestClient -> call succeed") + completion(true, response.result.value) + + case .failure(let error): + print("RestClient -> call failed") + print("RestClient Error -> \(error.localizedDescription)") + completion(false, error) + } + } + } + + // MARK: - 👨🏻‍💻 Repositories + + /** + * repositories(page:completion:) + * @description Fetch Java most popular repositories in Github + * @param page Page to be fetched (default is 1) + * @param completion Callback fired when request is completed + */ + class func repositories(page: Int=1, completion: @escaping Callback) { + let urlWithParams = String(format: "https://api.github.com/search/repositories?q=language:Java&sort=stars&page=%i", page) + RestClient.sendRequest(url: urlWithParams, completion: completion) + } + + // MARK: - 👨🏻‍💻 Pull Requests + + /** + * pullRequests(owner:repository:completion:) + * @description Fetch Available pull requests for a given repository + * @param owner Repository owner + * @param repository Repository slug + * @param completion Callback fired when request is completed + */ + class func pullRequests(owner: String, repository: String, completion: @escaping Callback) { + let urlWithParams = String(format: "https://api.github.com/repos/%@/%@/pulls", owner, repository) + RestClient.sendRequest(url: urlWithParams, completion: completion) + } +} + diff --git a/DesafioIOS/DesafioIOS/Core/SharedProtocols.swift b/DesafioIOS/DesafioIOS/Core/SharedProtocols.swift new file mode 100644 index 0000000..b1dacea --- /dev/null +++ b/DesafioIOS/DesafioIOS/Core/SharedProtocols.swift @@ -0,0 +1,56 @@ +// +// SharedProtocols.swift +// DesafioIOS +// +// Created by Felipe Ricieri on 28/10/17. +// Copyright © 2017 Nexaas. All rights reserved. +// + +import Foundation +import SVProgressHUD + +/** + * UniqueCell Protocol + * @description Every cell representation must have its own unique identifier + */ +public protocol UniqueCell { + static var cellIdentifier : String { get set } +} + +/** + * Hud Protocol + * @description Easy way to show/hide huds + */ +public protocol Hud { } +public extension Hud { + + /** + * showHud() + * @description Shows hud in screen + */ + func showHud() { + DispatchQueue.main.async { + SVProgressHUD.show() + } + } + + /** + * errorHud() + * @description Shows error hud in screen + */ + func errorHud(_ errorString: String) { + DispatchQueue.main.async { + SVProgressHUD.showError(withStatus: errorString) + } + } + + /** + * hideHud() + * @description Dismiss the presented hud + */ + func hideHud() { + DispatchQueue.main.async { + SVProgressHUD.dismiss() + } + } +} diff --git a/DesafioIOS/DesafioIOS/Core/Supporting Files/Bridging-Header.h b/DesafioIOS/DesafioIOS/Core/Supporting Files/Bridging-Header.h new file mode 100644 index 0000000..0b465b9 --- /dev/null +++ b/DesafioIOS/DesafioIOS/Core/Supporting Files/Bridging-Header.h @@ -0,0 +1,15 @@ +// +// Bridging-Header.h +// DesafioIOS +// +// Created by Felipe Ricieri on 28/10/17. +// Copyright © 2017 Nexaas. All rights reserved. +// + +#ifndef Bridging_Header_h +#define Bridging_Header_h + +#import +#import + +#endif /* Bridging_Header_h */ diff --git a/DesafioIOS/DesafioIOS/Core/Supporting Files/Info.plist b/DesafioIOS/DesafioIOS/Core/Supporting Files/Info.plist new file mode 100644 index 0000000..2fc8ad6 --- /dev/null +++ b/DesafioIOS/DesafioIOS/Core/Supporting Files/Info.plist @@ -0,0 +1,53 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationPortraitUpsideDown + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + + diff --git a/DesafioIOS/DesafioIOS/Core/UIKit+Extensions.swift b/DesafioIOS/DesafioIOS/Core/UIKit+Extensions.swift new file mode 100644 index 0000000..e8f8cad --- /dev/null +++ b/DesafioIOS/DesafioIOS/Core/UIKit+Extensions.swift @@ -0,0 +1,54 @@ +// +// UIKit+Extensions.swift +// DesafioIOS +// +// Created by Felipe Ricieri on 28/10/17. +// Copyright © 2017 Nexaas. All rights reserved. +// + +import UIKit + +extension UIColor { + + // Custom Colors + static var navigationBarColor : UIColor { + return UIColor(hex: 0x343438) + } + static var highlightColor : UIColor { + return UIColor(hex: 0xDD9224) + } + static var lineColor : UIColor { + return UIColor(hex: 0xD7D7D8) + } + static var titleColor : UIColor { + return UIColor(hex: 0x4179A9) + } + + // RGB + convenience init(_ r: Double, _ g: Double, _ b: Double, _ a: Double) { + self.init(r/255, g/255, b/255, a) + } + + // Hex to RGB + convenience init(red: Int, green: Int, blue: Int) { + assert(red >= 0 && red <= 255, "Invalid red component") + assert(green >= 0 && green <= 255, "Invalid green component") + assert(blue >= 0 && blue <= 255, "Invalid blue component") + self.init(red: CGFloat(red) / 255.0, green: CGFloat(green) / 255.0, blue: CGFloat(blue) / 255.0, alpha: 1.0) + } + convenience init(hex:Int) { + self.init(red:(hex >> 16) & 0xff, green:(hex >> 8) & 0xff, blue:hex & 0xff) + } +} + +extension UIView { + func enable() { + isHidden = false + isUserInteractionEnabled = true + } + func disable() { + isHidden = true + isUserInteractionEnabled = false + } +} + diff --git a/DesafioIOS/DesafioIOS/Models/Owner.swift b/DesafioIOS/DesafioIOS/Models/Owner.swift new file mode 100644 index 0000000..b32b5cd --- /dev/null +++ b/DesafioIOS/DesafioIOS/Models/Owner.swift @@ -0,0 +1,34 @@ +// +// Owner.swift +// DesafioIOS +// +// Created by Felipe Ricieri on 28/10/17. +// Copyright © 2017 Nexaas. All rights reserved. +// + +import Foundation +import SwiftyJSON + +/** + * Owner + * @description Local representation of Github users + */ +public struct Owner { + + var id : String = "" + var name : String = "" + var username : String = "" + var picture : String = "" + + // Deserializer + init(jsonData: [AnyHashable: Any]) { + + let json = JSON(jsonData) + + id = json["id"].stringValue + name = json["login"].stringValue + username = json["login"].stringValue + picture = json["avatar_url"].stringValue + } +} + diff --git a/DesafioIOS/DesafioIOS/Models/PullRequest.swift b/DesafioIOS/DesafioIOS/Models/PullRequest.swift new file mode 100644 index 0000000..8632669 --- /dev/null +++ b/DesafioIOS/DesafioIOS/Models/PullRequest.swift @@ -0,0 +1,43 @@ +// +// PullRequest.swift +// DesafioIOS +// +// Created by Felipe Ricieri on 28/10/17. +// Copyright © 2017 Nexaas. All rights reserved. +// + +import Foundation +import SwiftyJSON + +/** + * PullRequest + * @description Local representation of Github's PullRequest + */ +public struct PullRequest { + + var id : Int = 0 + var title : String = "" + var objectDescription : String = "" + var state : String = "" + var htmlUrlString : String = "" + var htmlUrl : URL? = nil + var owner : Owner? = nil + + // Deserializer + init(jsonData: [AnyHashable : Any]) { + + let json = JSON(jsonData) + + id = json["id"].intValue + title = json["title"].stringValue + objectDescription = json["description"].stringValue + state = json["state"].stringValue + htmlUrlString = json["html_url"].stringValue + htmlUrl = URL(string: htmlUrlString) + + if let ownerData = json["user"].dictionary { + owner = Owner(jsonData: ownerData) + } + } +} + diff --git a/DesafioIOS/DesafioIOS/Models/Repository.swift b/DesafioIOS/DesafioIOS/Models/Repository.swift new file mode 100644 index 0000000..34f9e57 --- /dev/null +++ b/DesafioIOS/DesafioIOS/Models/Repository.swift @@ -0,0 +1,44 @@ +// +// Repository.swift +// DesafioIOS +// +// Created by Felipe Ricieri on 28/10/17. +// Copyright © 2017 Nexaas. All rights reserved. +// + +import Foundation +import SwiftyJSON + +/** + * Repository + * @description Local representation of Github's Repository + */ +public struct Repository { + + var id : Int = 0 + var name : String = "" + var fullName : String = "" + var objectDescription : String = "" + var forks : Int = 0 + var stars : Int = 0 + var owner : Owner? = nil + + // Deserializer + init(jsonData: [AnyHashable: Any]) { + + let json = JSON(jsonData) + + id = json["id"].intValue + name = json["name"].stringValue + fullName = json["full_name"].stringValue + objectDescription = json["description"].stringValue + forks = json["forks_count"].intValue + stars = json["stargazers_count"].intValue + + if let ownerData = json["owner"].dictionary { + owner = Owner(jsonData: ownerData) + } + } +} + + diff --git a/DesafioIOS/DesafioIOS/en.lproj/Localizable.strings b/DesafioIOS/DesafioIOS/en.lproj/Localizable.strings new file mode 100644 index 0000000..46498cb --- /dev/null +++ b/DesafioIOS/DesafioIOS/en.lproj/Localizable.strings @@ -0,0 +1,21 @@ +/* + Localizable.strings + DesafioIOS + + Created by Felipe Ricieri on 29/10/17. + Copyright © 2017 Nexaas. All rights reserved. +*/ + +"closed" = "closed"; +"opened" = "opened"; + +"TryAgain" = "Try Again"; +"Close" = "Close"; + +"Error.Title" = "An error has ocurred"; +"Error.Unknown" = "An unknown error has ocurred."; +"Error.FailedRequest" = "Your request failed. We couldn't load any data from it."; +"Error.FailedRequestRepository" = "We couldn't load this repository data."; +"Error.NoURL" = "This Pull Request doesn't owns a valid URL (a.k.a. html_url field)"; +"Error.YouAreOffline" = "You're offline ☹️"; + diff --git a/DesafioIOS/DesafioIOS/pt-BR.lproj/LaunchScreen.strings b/DesafioIOS/DesafioIOS/pt-BR.lproj/LaunchScreen.strings new file mode 100644 index 0000000..59636ff --- /dev/null +++ b/DesafioIOS/DesafioIOS/pt-BR.lproj/LaunchScreen.strings @@ -0,0 +1,6 @@ + +/* Class = "UILabel"; text = "Felipe Ricieri"; ObjectID = "YmS-cz-xdM"; */ +"YmS-cz-xdM.text" = "Felipe Ricieri"; + +/* Class = "UILabel"; text = "Desafio iOS"; ObjectID = "rwN-i7-CwM"; */ +"rwN-i7-CwM.text" = "iOS Challenge"; diff --git a/DesafioIOS/DesafioIOS/pt-BR.lproj/Localizable.strings b/DesafioIOS/DesafioIOS/pt-BR.lproj/Localizable.strings new file mode 100644 index 0000000..9727add --- /dev/null +++ b/DesafioIOS/DesafioIOS/pt-BR.lproj/Localizable.strings @@ -0,0 +1,21 @@ +/* + Localizable.strings + DesafioIOS + + Created by Felipe Ricieri on 29/10/17. + Copyright © 2017 Nexaas. All rights reserved. +*/ + +"closed" = "fechadas"; +"opened" = "abertas"; + +"TryAgain" = "Tentar novamente"; +"Close" = "Fechar"; + +"Error.Title" = "Ocorreu um erro"; +"Error.Unknown" = "Ocorreu um erro desconhecido."; +"Error.FailedRequest" = "Não foi possível carregar os dados desta requisição"; +"Error.FailedRequestRepository" = "Não foi possível carregar os dados desse repositório."; +"Error.NoURL" = "Esta Pull Request não possui URL (campo html_url)."; +"Error.YouAreOffline" = "Você está desconectado ☹️"; + diff --git a/DesafioIOS/DesafioIOS/pt-BR.lproj/Main.strings b/DesafioIOS/DesafioIOS/pt-BR.lproj/Main.strings new file mode 100644 index 0000000..363728b --- /dev/null +++ b/DesafioIOS/DesafioIOS/pt-BR.lproj/Main.strings @@ -0,0 +1,42 @@ + +/* Class = "UILabel"; text = "{user name}"; ObjectID = "9Cb-4a-mW3"; */ +"9Cb-4a-mW3.text" = "{user name}"; + +/* Class = "UILabel"; text = "999"; ObjectID = "IQR-Vj-oCs"; */ +"IQR-Vj-oCs.text" = "999"; + +/* Class = "UINavigationItem"; title = "{pull request title}"; ObjectID = "KNI-Df-odK"; */ +"KNI-Df-odK.title" = "{pull request title}"; + +/* Class = "UINavigationItem"; title = "{repository name}"; ObjectID = "PWH-il-Cyk"; */ +"PWH-il-Cyk.title" = "{repository name}"; + +/* Class = "UINavigationItem"; title = "Github JavaPop"; ObjectID = "S94-F4-Nid"; */ +"S94-F4-Nid.title" = "Github JavaPop"; + +/* Class = "UILabel"; text = "{repository description}"; ObjectID = "Sob-Pg-5R1"; */ +"Sob-Pg-5R1.text" = "{repository description}"; + +/* Class = "UILabel"; text = "{num} open / {num} closed"; ObjectID = "d0K-ba-erX"; */ +"d0K-ba-erX.text" = "{num} open / {num} closed"; + +/* Class = "UILabel"; text = "{user nickname}"; ObjectID = "k7e-Cp-6cN"; */ +"k7e-Cp-6cN.text" = "{user nickname}"; + +/* Class = "UILabel"; text = "999"; ObjectID = "khE-mF-f7I"; */ +"khE-mF-f7I.text" = "999"; + +/* Class = "UILabel"; text = "{pull request name}"; ObjectID = "qlg-N6-3qF"; */ +"qlg-N6-3qF.text" = "{pull request name}"; + +/* Class = "UILabel"; text = "{user name}"; ObjectID = "rrg-Qf-Ht3"; */ +"rrg-Qf-Ht3.text" = "{user name}"; + +/* Class = "UILabel"; text = "{pull request description}"; ObjectID = "xxT-5x-UtT"; */ +"xxT-5x-UtT.text" = "{pull request description}"; + +/* Class = "UILabel"; text = "{user nickname}"; ObjectID = "zGW-ho-Oq6"; */ +"zGW-ho-Oq6.text" = "{user nickname}"; + +/* Class = "UILabel"; text = "{repository name}"; ObjectID = "zjC-4k-Cc0"; */ +"zjC-4k-Cc0.text" = "{repository name}"; diff --git a/DesafioIOS/DesafioIOSTests/Info.plist b/DesafioIOS/DesafioIOSTests/Info.plist new file mode 100644 index 0000000..6c40a6c --- /dev/null +++ b/DesafioIOS/DesafioIOSTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/DesafioIOS/DesafioIOSTests/MockPullRequestsViewController.swift b/DesafioIOS/DesafioIOSTests/MockPullRequestsViewController.swift new file mode 100644 index 0000000..5e294b4 --- /dev/null +++ b/DesafioIOS/DesafioIOSTests/MockPullRequestsViewController.swift @@ -0,0 +1,49 @@ +// +// MockPullRequestsViewController.swift +// DesafioIOSTests +// +// Created by Felipe Ricieri on 29/10/17. +// Copyright © 2017 Nexaas. All rights reserved. +// + +@testable import DesafioIOS + +class MockPullRequestsViewController : PullRequestsViewController { + + var notificationReceived : String? = nil + + var hasObservers : Bool? = nil + var didRefresh : Bool = false + var didGoBack : Bool = false + + override func actionBack() { + super.actionBack() + self.didGoBack = true + } + + override func refresh() { + super.refresh() + self.didRefresh = true + } + + override func addObservers() { + super.addObservers() + self.hasObservers = true + } + + override func removeObservers() { + super.removeObservers() + self.hasObservers = false + } + + override func notificationIsReachable(n: Notification) { + super.notificationIsReachable(n: n) + self.notificationReceived = n.name.rawValue + } + + override func notificationNotReachable(n: Notification) { + super.notificationNotReachable(n: n) + self.notificationReceived = n.name.rawValue + } +} + diff --git a/DesafioIOS/DesafioIOSTests/MockPullRequestsViewModel.swift b/DesafioIOS/DesafioIOSTests/MockPullRequestsViewModel.swift new file mode 100644 index 0000000..1528b3f --- /dev/null +++ b/DesafioIOS/DesafioIOSTests/MockPullRequestsViewModel.swift @@ -0,0 +1,19 @@ +// +// MockPullRequestsViewModel.swift +// DesafioIOSTests +// +// Created by Felipe Ricieri on 29/10/17. +// Copyright © 2017 Nexaas. All rights reserved. +// + +@testable import DesafioIOS + +class MockPullRequestsViewModel : PullRequestsViewModel { + + var didRefreshPage : Bool? = nil + + override func refresh() { + super.refresh() + self.didRefreshPage = true + } +} diff --git a/DesafioIOS/DesafioIOSTests/MockRepositoriesViewController.swift b/DesafioIOS/DesafioIOSTests/MockRepositoriesViewController.swift new file mode 100644 index 0000000..c4afc5c --- /dev/null +++ b/DesafioIOS/DesafioIOSTests/MockRepositoriesViewController.swift @@ -0,0 +1,43 @@ +// +// MockRepositoriesViewController.swift +// DesafioIOSTests +// +// Created by Felipe Ricieri on 29/10/17. +// Copyright © 2017 Nexaas. All rights reserved. +// + +@testable import DesafioIOS + +class MockRepositoriesViewController : RepositoriesViewController { + + var notificationReceived : String? = nil + + var hasObservers : Bool? = nil + var didRefresh : Bool = false + + override func refresh() { + super.refresh() + self.didRefresh = true + } + + override func addObservers() { + super.addObservers() + self.hasObservers = true + } + + override func removeObservers() { + super.removeObservers() + self.hasObservers = false + } + + override func notificationIsReachable(n: Notification) { + super.notificationIsReachable(n: n) + self.notificationReceived = n.name.rawValue + } + + override func notificationNotReachable(n: Notification) { + super.notificationNotReachable(n: n) + self.notificationReceived = n.name.rawValue + } +} + diff --git a/DesafioIOS/DesafioIOSTests/MockWebViewController.swift b/DesafioIOS/DesafioIOSTests/MockWebViewController.swift new file mode 100644 index 0000000..96ba942 --- /dev/null +++ b/DesafioIOS/DesafioIOSTests/MockWebViewController.swift @@ -0,0 +1,50 @@ +// +// MockWebViewController.swift +// DesafioIOSTests +// +// Created by Felipe Ricieri on 29/10/17. +// Copyright © 2017 Nexaas. All rights reserved. +// + +@testable import DesafioIOS + +class MockWebViewController : WebViewController { + + var notificationReceived : String? = nil + + var didLoadWebView : Bool = false + var didReload : Bool = false + var didDismissed : Bool = false + var hasObservers : Bool? = nil + + override func loadWebView() { + super.loadWebView() + self.didLoadWebView = true + } + + override func actionDismiss() { + super.actionDismiss() + self.didDismissed = true + } + + override func addObservers() { + super.addObservers() + self.hasObservers = true + } + + override func removeObservers() { + super.removeObservers() + self.hasObservers = false + } + + override func notificationIsReachable(n: Notification) { + super.notificationIsReachable(n: n) + self.notificationReceived = n.name.rawValue + } + + override func notificationNotReachable(n: Notification) { + super.notificationNotReachable(n: n) + self.notificationReceived = n.name.rawValue + } +} + diff --git a/DesafioIOS/DesafioIOSTests/OwnerTests.swift b/DesafioIOS/DesafioIOSTests/OwnerTests.swift new file mode 100644 index 0000000..63e5ba2 --- /dev/null +++ b/DesafioIOS/DesafioIOSTests/OwnerTests.swift @@ -0,0 +1,33 @@ +// +// OwnerTests.swift +// DesafioIOSTests +// +// Created by Felipe Ricieri on 28/10/17. +// Copyright © 2017 Nexaas. All rights reserved. +// + +import XCTest +@testable import DesafioIOS + +class OwnerTests: XCTestCase { + + func testInit() { + + // Our Fake info + let fakeDict : [AnyHashable:Any] = [ + "id" : "fake_id", + "login" : "fakelogin", + "avatar_url" : "http://fakeavatar.com/avatar.png" + ] + + // Apply it + let fakeObject = Owner(jsonData: fakeDict) + + // Assert + XCTAssert(fakeObject.id != "", "O campo \"id\" não foi preenchido.") + XCTAssert(fakeObject.username != "", "O campo \"username\" não foi preenchido.") + XCTAssert(fakeObject.name != "", "O campo \"name\" não foi preenchido.") + XCTAssert(fakeObject.picture != "", "O campo \"picture\" não foi preenchido.") + } +} + diff --git a/DesafioIOS/DesafioIOSTests/PullRequestServiceTests.swift b/DesafioIOS/DesafioIOSTests/PullRequestServiceTests.swift new file mode 100644 index 0000000..0173117 --- /dev/null +++ b/DesafioIOS/DesafioIOSTests/PullRequestServiceTests.swift @@ -0,0 +1,38 @@ +// +// PullRequestServiceTests.swift +// DesafioIOSTests +// +// Created by Felipe Ricieri on 28/10/17. +// Copyright © 2017 Nexaas. All rights reserved. +// + +import XCTest +@testable import DesafioIOS + +class PullRequestServiceTests : XCTestCase { + + fileprivate let timeOutInterval : TimeInterval = 10.0 + + func testLoad() { + + // Expectation + let expectation = self.expectation(description: "Esperando resultado de Pull Requests") + + // Launch + let testOwner = "elastic" + let testRepository = "elasticsearch" + let service = PullRequestService() + service.load(owner: testOwner, repository: testRepository, succeed: { (result) in + XCTAssert(result.count > 0, "Não houveram resultados.") + expectation.fulfill() + }) { (error) in + XCTAssert(error != "", "Descrição do erro falhou.") + expectation.fulfill() + } + + // Assert + self.waitForExpectations(timeout: timeOutInterval, handler: nil) + } +} + + diff --git a/DesafioIOS/DesafioIOSTests/PullRequestTests.swift b/DesafioIOS/DesafioIOSTests/PullRequestTests.swift new file mode 100644 index 0000000..1c6d390 --- /dev/null +++ b/DesafioIOS/DesafioIOSTests/PullRequestTests.swift @@ -0,0 +1,47 @@ +// +// PullRequestTests.swift +// DesafioIOSTests +// +// Created by Felipe Ricieri on 28/10/17. +// Copyright © 2017 Nexaas. All rights reserved. +// + +import XCTest +@testable import DesafioIOS + +class PullRequestTests: XCTestCase { + + fileprivate let timeOutInterval : TimeInterval = 10.0 + + func testInit() { + + // Our Fake info + let fakeDict : [String:Any] = [ + "id" : 12345, + "title" : "Fake Title", + "body" : "Lorem Ipsum Dolor sit amen", + "description" : "Fake Description", + "html_url" : "http://google.com", + "state" : "open", + "user" : [ + "id" : "fake_id", + "login" : "fakelogin", + "avatar_url" : "http://fakeavatar.com/avatar.png" + ] + ] + + // Apply it + let fakeObject = PullRequest(jsonData: fakeDict) + + // Assert + XCTAssert(fakeObject.id != 0, "O campo \"id\" não foi preenchido.") + XCTAssert(fakeObject.title != "", "O campo \"title\" não foi preenchido.") + XCTAssert(fakeObject.objectDescription != "", "O campo \"objectDescription\" não foi preenchido.") + XCTAssert(fakeObject.state != "", "O campo \"state\" não foi preenchido.") + XCTAssert(fakeObject.htmlUrlString != "", "O campo \"htmlUrlString\" não foi preenchido.") + XCTAssertNotNil(fakeObject.htmlUrl, "O campo \"htmlUrlString\" não possui uma URL válida.") + // Assert Owner + XCTAssertNotNil(fakeObject.owner, "O campo \"owner\" não foi preenchido.") + } +} + diff --git a/DesafioIOS/DesafioIOSTests/PullRequestsViewControllerTests.swift b/DesafioIOS/DesafioIOSTests/PullRequestsViewControllerTests.swift new file mode 100644 index 0000000..fd8cd84 --- /dev/null +++ b/DesafioIOS/DesafioIOSTests/PullRequestsViewControllerTests.swift @@ -0,0 +1,142 @@ +// +// PullRequestsViewControllerTests.swift +// DesafioIOSTests +// +// Created by Felipe Ricieri on 28/10/17. +// Copyright © 2017 Nexaas. All rights reserved. +// + +import XCTest +@testable import DesafioIOS + +class PullRequestsViewControllerTests: XCTestCase { + + fileprivate var vc : MockPullRequestsViewController! = nil + fileprivate let timeOutInterval : TimeInterval = 10.0 + fileprivate let fakeDict : [String:Any] = [ + "id" : 12345, + "name" : "elasticsearch", + "full_name" : "elastic/elasticsearch", + "description" : "Lorem ipsum dolor sit amen", + "forks_count" : 999, + "stargazers_count" : 999, + "owner" : [ + "id" : "fake_id", + "login" : "elastic", + "avatar_url" : "http://fakeavatar.com/avatar.png" + ] + ] + + // MARK: - Lifecycle Methods + override func setUp() { + super.setUp() + let repository = Repository(jsonData: fakeDict) + self.vc = MockPullRequestsViewController() + self.vc.viewModel = PullRequestsViewModel(repository: repository) + } + + override func tearDown() { + self.vc = nil + super.tearDown() + } + + // MARK: - Tests + func testRefresh() { + + // Assert View Controller wasn't refreshed yet + XCTAssertFalse(vc.didRefresh == true, "O View Controller não pode ter sido recarregado nesta fase.") + + // Launch + vc.refresh() + + // Assert View Controller was refreshed + XCTAssert(vc.didRefresh == true, "O View Controller deve ter sido recarregado nesta fase.") + } + + func testTriggerRefreshControl() { + + // Init view + var view : UIView? = self.vc.view + + // Trigger + self.vc.triggerRefreshControl() + + // Assert + XCTAssertNotNil(view, "Este View Controller não possui UIView.") + XCTAssert(self.vc.refreshControl!.isRefreshing, "UIRefreshControl não foi ativado.") + XCTAssert(self.vc.tableView.contentOffset.y != 0, "Content offset da UITableView não está diferente.") + + // Release view + view = nil + } + + func testActionBack() { + + // Assert View Controller wasn't popped yet + XCTAssertFalse(vc.didGoBack == true, "O View Controller não pode ter sido fechado nesta fase.") + + // Launch + vc.actionBack() + + // Assert View Controller was popped + XCTAssert(vc.didGoBack == true, "O View Controller deve ter sido fechado nesta fase.") + } + + func testAddObservers() { + + // Try to Add observers + self.vc.addObservers() + + // Test proprieties + XCTAssertNotNil(self.vc.hasObservers, "Este View Controller não possui Observers.") + XCTAssert(self.vc.hasObservers == true, "Não foram adicionados observers neste View Controller") + + // Remove observers + self.vc.removeObservers() + } + + func testRemoveObservers() { + + // Try to Add observers + self.vc.addObservers() + // Remove observers + self.vc.removeObservers() + + // Test proprieties + XCTAssertNotNil(self.vc.hasObservers, "Este View Controller possui Observers.") + XCTAssert(self.vc.hasObservers == false, "Este View Controller possui Observers") + } + + func testNotificationIsReachable() { + + // Try to Add observers + vc.addObservers() + + // Launch Notification + NotificationCenter.default.post(.reachable) + + // Test proprieties + XCTAssertNotNil(vc.notificationReceived, "Este View Controller não recebeu notificações.") + XCTAssert(vc.notificationReceived == Notification.CustomName.reachable.rawValue, "Este View Controller não recebeu a notificação \"\(Notification.CustomName.reachable.rawValue)\".") + + // Remove observers + vc.removeObservers() + } + + func testNotificationNotReachable() { + + // Try to Add observers + vc.addObservers() + + // Launch Notification + NotificationCenter.default.post(.notReachable) + + // Test proprieties + XCTAssertNotNil(vc.notificationReceived, "Este View Controller não recebeu notificações.") + XCTAssert(vc.notificationReceived == Notification.CustomName.notReachable.rawValue, "Este View Controller não recebeu a notificação \"\(Notification.CustomName.notReachable.rawValue)\".") + + // Remove observers + vc.removeObservers() + } +} + diff --git a/DesafioIOS/DesafioIOSTests/PullRequestsViewModelTests.swift b/DesafioIOS/DesafioIOSTests/PullRequestsViewModelTests.swift new file mode 100644 index 0000000..9ef9ee8 --- /dev/null +++ b/DesafioIOS/DesafioIOSTests/PullRequestsViewModelTests.swift @@ -0,0 +1,75 @@ +// +// PullRequestsViewModelTests.swift +// DesafioIOSTests +// +// Created by Felipe Ricieri on 28/10/17. +// Copyright © 2017 Nexaas. All rights reserved. +// + +import XCTest +@testable import DesafioIOS + +class PullRequestsViewModelTests: XCTestCase { + + fileprivate var vm : MockPullRequestsViewModel! = nil + fileprivate let timeOutInterval : TimeInterval = 5.0 + fileprivate let fakeDict : [String:Any] = [ + "id" : 12345, + "name" : "elasticsearch", + "full_name" : "elastic/elasticsearch", + "description" : "Lorem ipsum dolor sit amen", + "forks_count" : 999, + "stargazers_count" : 999, + "owner" : [ + "id" : "fake_id", + "login" : "elastic", + "avatar_url" : "http://fakeavatar.com/avatar.png" + ] + ] + + // MARK: - Lifecycle Methods + override func setUp() { + super.setUp() + let repository = Repository(jsonData: fakeDict) + self.vm = MockPullRequestsViewModel(repository: repository) + } + + override func tearDown() { + self.vm = nil + super.tearDown() + } + + // MARK: - Tests + func testRefresh() { + + // Launch + self.vm.refresh() + + // Assert + XCTAssertNotNil(self.vm.didRefreshPage, "Os dados não foram recarregados.") + } + + func testFetchData() { + + // Apply it + let fakeObject = Repository(jsonData: fakeDict) + self.vm.repository = fakeObject + + // Expectation + let expectation = self.expectation(description: "Esperando dados de de Pull Requests") + + // Launch + let previousSourceCount = vm.source.count + vm.fetchData { [weak self] in + if let this = self { + // Assert + XCTAssert(previousSourceCount < this.vm.source.count, "O conteúdo da coleção de dados é menor ou igual à conferência anterior.") + } + expectation.fulfill() + } + + // Assert + self.waitForExpectations(timeout: self.timeOutInterval, handler: nil) + } +} + diff --git a/DesafioIOS/DesafioIOSTests/ReachabilityManagerTests.swift b/DesafioIOS/DesafioIOSTests/ReachabilityManagerTests.swift new file mode 100644 index 0000000..f4b1416 --- /dev/null +++ b/DesafioIOS/DesafioIOSTests/ReachabilityManagerTests.swift @@ -0,0 +1,41 @@ +// +// ReachabilityManagerTests.swift +// DesafioIOSTests +// +// Created by Felipe Ricieri on 28/10/17. +// Copyright © 2017 Nexaas. All rights reserved. +// + +import XCTest +@testable import DesafioIOS + +class ReachabilityManagerTests: XCTestCase { + + func testSubscribe() { + + // Subscribe + ReachabilityManager.subscribe() + + // Assert + XCTAssert(ReachabilityManager.isSubscribed, "ReachabilityManager não está recebendo eventos.") + + // Unsubscribe + ReachabilityManager.unsubscribe() + } + + func testUnsubscribe() { + + // Subscribe + ReachabilityManager.subscribe() + + // Assert + XCTAssert(ReachabilityManager.isSubscribed, "ReachabilityManager não está recebendo eventos.") + + // Unsubscribe + ReachabilityManager.unsubscribe() + + // Assert + XCTAssertFalse(ReachabilityManager.isSubscribed, "ReachabilityManager está recebendo eventos.") + } +} + diff --git a/DesafioIOS/DesafioIOSTests/RepositoriesViewControllerTests.swift b/DesafioIOS/DesafioIOSTests/RepositoriesViewControllerTests.swift new file mode 100644 index 0000000..93d4723 --- /dev/null +++ b/DesafioIOS/DesafioIOSTests/RepositoriesViewControllerTests.swift @@ -0,0 +1,117 @@ +// +// RepositoriesViewControllerTests.swift +// DesafioIOSTests +// +// Created by Felipe Ricieri on 28/10/17. +// Copyright © 2017 Nexaas. All rights reserved. +// + +import XCTest +@testable import DesafioIOS + +class RepositoriesViewControllerTests: XCTestCase { + + fileprivate var vc : MockRepositoriesViewController! = nil + fileprivate let timeOutInterval : TimeInterval = 5.0 + + // MARK: - Lifecycle Methods + override func setUp() { + super.setUp() + self.vc = MockRepositoriesViewController() + self.vc.viewModel = RepositoriesViewModel() + } + + override func tearDown() { + self.vc = nil + super.tearDown() + } + + // MARK: - Tests + func testRefresh() { + + // Assert View Controller wasn't refreshed yet + XCTAssertFalse(vc.didRefresh == true, "O View Controller não pode ter sido recarregado nesta fase.") + + // Launch + vc.refresh() + + // Assert View Controller was refreshed + XCTAssert(vc.didRefresh == true, "O View Controller deve ter sido recarregado nesta fase.") + } + + func testTriggerRefreshControl() { + + // Init view + var view : UIView? = vc.view + + // Trigger + vc.triggerRefreshControl() + + // Assert + XCTAssertNotNil(view, "Este View Controller não possui UIView.") + XCTAssert(vc.refreshControl!.isRefreshing, "UIRefreshControl não foi ativado.") + XCTAssert(vc.tableView.contentOffset.y != 0, "Content offset da UITableView não está diferente.") + XCTAssert(vc.didRefresh == true, "O View Controller deve ter sido recarregado nesta fase.") + + // Release view + view = nil + } + + func testAddObservers() { + + // Try to Add observers + vc.addObservers() + + // Test proprieties + XCTAssertNotNil(vc.hasObservers, "Este View Controller não possui Observers.") + XCTAssert(vc.hasObservers == true, "Não foram adicionados observers neste View Controller") + + // Remove observers + vc.removeObservers() + } + + func testRemoveObservers() { + + // Try to Add observers + vc.addObservers() + // Remove observers + vc.removeObservers() + + // Test proprieties + XCTAssertNotNil(vc.hasObservers, "Este View Controller possui Observers.") + XCTAssert(vc.hasObservers == false, "Este View Controller possui Observers") + } + + func testNotificationIsReachable() { + + // Try to Add observers + vc.addObservers() + + // Launch Notification + NotificationCenter.default.post(.reachable) + + // Test proprieties + XCTAssertNotNil(vc.notificationReceived, "Este View Controller não recebeu notificações.") + XCTAssert(vc.notificationReceived == Notification.CustomName.reachable.rawValue, "Este View Controller não recebeu a notificação \"\(Notification.CustomName.reachable.rawValue)\".") + + // Remove observers + vc.removeObservers() + } + + func testNotificationNotReachable() { + + // Try to Add observers + vc.addObservers() + + // Launch Notification + NotificationCenter.default.post(.notReachable) + + // Test proprieties + XCTAssertNotNil(vc.notificationReceived, "Este View Controller não recebeu notificações.") + XCTAssert(vc.notificationReceived == Notification.CustomName.notReachable.rawValue, "Este View Controller não recebeu a notificação \"\(Notification.CustomName.notReachable.rawValue)\".") + + // Remove observers + vc.removeObservers() + } +} + diff --git a/DesafioIOS/DesafioIOSTests/RepositoriesViewModelTests.swift b/DesafioIOS/DesafioIOSTests/RepositoriesViewModelTests.swift new file mode 100644 index 0000000..cb0bf53 --- /dev/null +++ b/DesafioIOS/DesafioIOSTests/RepositoriesViewModelTests.swift @@ -0,0 +1,98 @@ +// +// RepositoriesViewModelTests.swift +// DesafioIOSTests +// +// Created by Felipe Ricieri on 28/10/17. +// Copyright © 2017 Nexaas. All rights reserved. +// + +import XCTest +@testable import DesafioIOS + +class RepositoriesViewModelTests: XCTestCase { + + fileprivate var vm : RepositoriesViewModel! = nil + fileprivate let timeOutInterval : TimeInterval = 5.0 + + // MARK: - Lifecycle Methods + override func setUp() { + super.setUp() + self.vm = RepositoriesViewModel() + } + + override func tearDown() { + self.vm = nil + super.tearDown() + } + + // MARK: - Tests + func testRefresh() { + + // Expectation + let expectation = self.expectation(description: "Carregar dados") + + // Fetch Data to fill source + vm.fetchData { [weak self] in + if let this = self { + // Assert + XCTAssert(this.vm.source.count > 0, "A coleção de dados está vazia.") + // Fetch second page + this.vm.triggerInfiniteScrolling { [unowned this] in + // Assert + XCTAssert(this.vm.page > 1, "A página ainda é a primeira.") + expectation.fulfill() + // Try to refresh + this.vm.refresh() + // Assert + XCTAssert(this.vm.page == 1, "A página ativa não é a primeira") + } + } + } + + // Wait + self.waitForExpectations(timeout: self.timeOutInterval, handler: nil) + } + + func testFetchData() { + + // Expectation + let expectation = self.expectation(description: "Esperando dados da primeira página de Repositories") + + // Launch + let previousSourceCount = self.vm.source.count + self.vm.fetchData { [weak self] in + if let this = self { + // Assert + XCTAssert(previousSourceCount < this.vm.source.count, "O conteúdo da coleção de dados é menor ou igual à conferência anterior.") + } + expectation.fulfill() + } + + // Assert + self.waitForExpectations(timeout: self.timeOutInterval, handler: nil) + } + + func testTriggerInfiniteScrolling() { + + // Fetch first page + self.vm.fetchData { [weak self] in + if let this = self { + // Expectation + let expectation = this.expectation(description: "Esperando dados da próxima página de Repositories") + + // Launch + let previousSourceCount = this.vm.source.count + this.vm.triggerInfiniteScrolling { [unowned this] in + // Assert + XCTAssert(previousSourceCount < this.vm.source.count, "O conteúdo da coleção de dados é menor ou igual à conferência anterior.") + XCTAssert(this.vm.page > 1, "A página continua sendo a primeira.") + expectation.fulfill() + } + + // Assert + this.waitForExpectations(timeout: this.timeOutInterval, handler: nil) + } + } + } +} + diff --git a/DesafioIOS/DesafioIOSTests/RepositoryServiceTests.swift b/DesafioIOS/DesafioIOSTests/RepositoryServiceTests.swift new file mode 100644 index 0000000..d036c90 --- /dev/null +++ b/DesafioIOS/DesafioIOSTests/RepositoryServiceTests.swift @@ -0,0 +1,34 @@ +// +// RepositoryService.swift +// DesafioIOSTests +// +// Created by Felipe Ricieri on 28/10/17. +// Copyright © 2017 Nexaas. All rights reserved. +// + +import XCTest +@testable import DesafioIOS + +class RepositoryServiceTests : XCTestCase { + + fileprivate let timeOutInterval : TimeInterval = 10.0 + + func testLoad() { + + // Expectation + let expectation = self.expectation(description: "Esperando resultado de Repositories") + + // Launch + let service = RepositoryService() + service.load(page: 1, succeed: { (result) in + XCTAssert(result.count > 0, "Não houveram resultados.") + expectation.fulfill() + }) { (error) in + XCTAssert(error != "", "Descrição do erro falhou.") + expectation.fulfill() + } + + // Assert + self.waitForExpectations(timeout: timeOutInterval, handler: nil) + } +} diff --git a/DesafioIOS/DesafioIOSTests/RepositoryTests.swift b/DesafioIOS/DesafioIOSTests/RepositoryTests.swift new file mode 100644 index 0000000..ddf1761 --- /dev/null +++ b/DesafioIOS/DesafioIOSTests/RepositoryTests.swift @@ -0,0 +1,47 @@ +// +// RepositoryTests.swift +// DesafioIOSTests +// +// Created by Felipe Ricieri on 28/10/17. +// Copyright © 2017 Nexaas. All rights reserved. +// + +import XCTest +@testable import DesafioIOS + +class RepositoryTests: XCTestCase { + + fileprivate let timeOutInterval : TimeInterval = 10.0 + + func testInit() { + + // Our Fake info + let fakeDict : [String:Any] = [ + "id" : 12345, + "name" : "Fake Name", + "full_name" : "Fake Name/Full Name", + "description" : "Lorem ipsum dolor sit amen", + "forks_count" : 999, + "stargazers_count" : 999, + "owner" : [ + "id" : "fake_id", + "login" : "fakelogin", + "avatar_url" : "http://fakeavatar.com/avatar.png" + ] + ] + + // Apply it + let fakeObject = Repository(jsonData: fakeDict) + + // Assert + XCTAssert(fakeObject.id != 0, "O campo \"id\" não foi preenchido.") + XCTAssert(fakeObject.name != "", "O campo \"name\" não foi preenchido.") + XCTAssert(fakeObject.objectDescription != "", "O campo \"objectDescription\" não foi preenchido.") + XCTAssert(fakeObject.forks != 0, "O campo \"forks\" não foi preenchido.") + XCTAssert(fakeObject.stars != 0, "O campo \"stars\" não foi preenchido.") + // Assert Owner + XCTAssertNotNil(fakeObject.owner, "O campo \"owner\" não foi preenchido.") + } +} + + diff --git a/DesafioIOS/DesafioIOSTests/RestClientTests.swift b/DesafioIOS/DesafioIOSTests/RestClientTests.swift new file mode 100644 index 0000000..0422c39 --- /dev/null +++ b/DesafioIOS/DesafioIOSTests/RestClientTests.swift @@ -0,0 +1,34 @@ +// +// RestClientTests.swift +// DesafioIOSTests +// +// Created by Felipe Ricieri on 28/10/17. +// Copyright © 2017 Nexaas. All rights reserved. +// + +import XCTest +@testable import DesafioIOS + +class RestClientTests: XCTestCase { + + fileprivate let timeOutInterval : TimeInterval = 10.0 + + func testSendRequest() { + + // Expectation + let expectation = self.expectation(description: "Esperando resultado de Rest Client (pull requests)") + + // Launch + RestClient.sendRequest(url: "https://api.github.com/repos/elastic/elasticsearch/pulls") { (succeed, result) in + XCTAssert(succeed, "A requisição falhou.") + XCTAssertNotNil(result, "A resposta é nula.") + if let safeResult = result { + XCTAssert(safeResult is [[String:Any]], "A resposta não é uma coleção de dados.") + } + expectation.fulfill() + } + + // Assert + self.waitForExpectations(timeout: self.timeOutInterval, handler: nil) + } +} diff --git a/DesafioIOS/DesafioIOSTests/WebViewControllerTests.swift b/DesafioIOS/DesafioIOSTests/WebViewControllerTests.swift new file mode 100644 index 0000000..bc24fc3 --- /dev/null +++ b/DesafioIOS/DesafioIOSTests/WebViewControllerTests.swift @@ -0,0 +1,125 @@ +// +// WebViewControllerTests.swift +// DesafioIOSTests +// +// Created by Felipe Ricieri on 28/10/17. +// Copyright © 2017 Nexaas. All rights reserved. +// + +import XCTest +@testable import DesafioIOS + +class WebViewControllerTests: XCTestCase { + + fileprivate var vc : MockWebViewController! = nil + fileprivate let fakeDict : [String:Any] = [ + "id" : 12345, + "title" : "Fake Title", + "body" : "Lorem Ipsum Dolor sit amen", + "description" : "Fake Description", + "html_url" : "http://google.com", + "state" : "open", + "user" : [ + "id" : "fake_id", + "login" : "fakelogin", + "avatar_url" : "http://fakeavatar.com/avatar.png" + ] + ] + + // MARK: - Lifecycle Methods + override func setUp() { + super.setUp() + let pullRequest = PullRequest(jsonData: fakeDict) + self.vc = MockWebViewController() + self.vc.viewModel = WebViewModel(pullRequest: pullRequest) + } + + override func tearDown() { + self.vc = nil + super.tearDown() + } + + // MARK: - Tests + func testLoadWebView() { + + // Assert View Model Don't have a valid URL yet + XCTAssert(vc.didLoadWebView == false, "O View Controller não deve ter carregado a WebView nesta fase.") + + // Launch + vc.loadWebView() + + // Assert + XCTAssert(self.vc.didLoadWebView == true, "O View Controller deve ter carregado a WebView nesta fase.") + } + + func testActionDismiss() { + + // Assert View Controller wasn't reloaded yet + XCTAssertFalse(vc.didDismissed == true, "O View Controller não pode ter sido recarregado nesta fase.") + + // Launch + vc.actionDismiss() + + // Assert View Controller was reloaded + XCTAssert(vc.didDismissed == true, "O View Controller deve ter sido recarregado nesta fase.") + } + + func testAddObservers() { + + // Try to Add observers + vc.addObservers() + + // Test proprieties + XCTAssertNotNil(vc.hasObservers, "Este View Controller não possui Observers.") + XCTAssert(vc.hasObservers == true, "Não foram adicionados observers neste View Controller") + + // Remove observers + vc.removeObservers() + } + + func testRemoveObservers() { + + // Try to Add observers + vc.addObservers() + // Remove observers + vc.removeObservers() + + // Test proprieties + XCTAssertNotNil(vc.hasObservers, "Este View Controller possui Observers.") + XCTAssert(vc.hasObservers == false, "Este View Controller possui Observers") + } + + func testNotificationIsReachable() { + + // Try to Add observers + vc.addObservers() + + // Launch Notification + NotificationCenter.default.post(.reachable) + + // Test proprieties + XCTAssertNotNil(vc.notificationReceived, "Este View Controller não recebeu notificações.") + XCTAssert(vc.notificationReceived == Notification.CustomName.reachable.rawValue, "Este View Controller não recebeu a notificação \"\(Notification.CustomName.reachable.rawValue)\".") + + // Remove observers + vc.removeObservers() + } + + func testNotificationNotReachable() { + + // Try to Add observers + vc.addObservers() + + // Launch Notification + NotificationCenter.default.post(.notReachable) + + // Test proprieties + XCTAssertNotNil(vc.notificationReceived, "Este View Controller não recebeu notificações.") + XCTAssert(vc.notificationReceived == Notification.CustomName.notReachable.rawValue, "Este View Controller não recebeu a notificação \"\(Notification.CustomName.notReachable.rawValue)\".") + + // Remove observers + vc.removeObservers() + } +} + + diff --git a/DesafioIOS/Podfile b/DesafioIOS/Podfile new file mode 100755 index 0000000..12b9add --- /dev/null +++ b/DesafioIOS/Podfile @@ -0,0 +1,28 @@ +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '8.0' +use_frameworks! + +def shared_pods + + # Common Pods + pod 'SDWebImage', '~> 3.7.3' + pod 'SVProgressHUD' + pod 'Alamofire' + pod 'ReachabilitySwift', '~> 3' + pod 'SwiftyJSON' + + # Felipe Ricieri's Pods + pod 'CoolDesignables' + pod 'StoryboardContext' + +end + +# Targeting main project +target "DesafioIOS" do + shared_pods +end + +# Targeting test project +target "DesafioIOSTests" do + shared_pods +end