diff --git a/README.md b/README.md index 20a7af6..c22c01f 100644 --- a/README.md +++ b/README.md @@ -1 +1,25 @@ -# quizchallenge \ No newline at end of file +# Quiz Challenge + +This app was created as a iOS Developer code challenge. + +## Prerequisites + +It is important to have the 10.2.1 version of Xcode. + +## Installing + +Clone this repository and open the solution using Xcode 10.2.1 + +## Some screens + +![alt text](https://github.com/alnp/quizchallenge/blob/608dee86c289d65b93579ccd1b4e18eb290670ef/iPhoneXR-Screen2.png?raw=true) +![alt text](https://github.com/alnp/quizchallenge/blob/608dee86c289d65b93579ccd1b4e18eb290670ef/iPhoneXR-Screen1.png?raw=true) + +## Author + +* **Alessandra Pereira** + +## License + +This project is licensed under the MIT License + diff --git a/quizchallenge/quizchallenge.xcodeproj/project.pbxproj b/quizchallenge/quizchallenge.xcodeproj/project.pbxproj new file mode 100644 index 0000000..37b9767 --- /dev/null +++ b/quizchallenge/quizchallenge.xcodeproj/project.pbxproj @@ -0,0 +1,712 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + CDF164C5232C473E00EDFE22 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF164C4232C473E00EDFE22 /* AppDelegate.swift */; }; + CDF164C7232C473E00EDFE22 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF164C6232C473E00EDFE22 /* MainViewController.swift */; }; + CDF164CC232C473F00EDFE22 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CDF164CB232C473F00EDFE22 /* Assets.xcassets */; }; + CDF164CF232C473F00EDFE22 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CDF164CD232C473F00EDFE22 /* LaunchScreen.storyboard */; }; + CDF164DA232C473F00EDFE22 /* quizchallengeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF164D9232C473F00EDFE22 /* quizchallengeTests.swift */; }; + CDF164FA232D4A9300EDFE22 /* EndpointProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF164F9232D4A9300EDFE22 /* EndpointProtocol.swift */; }; + CDF164FC232D4AF400EDFE22 /* HTTPTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF164FB232D4AF400EDFE22 /* HTTPTypes.swift */; }; + CDF16506232D4E3D00EDFE22 /* NetworkRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF16505232D4E3D00EDFE22 /* NetworkRouter.swift */; }; + CDF16508232D5C9300EDFE22 /* QuizAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF16507232D5C9300EDFE22 /* QuizAPI.swift */; }; + CDF1650B232D5E1900EDFE22 /* QuizModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF1650A232D5E1900EDFE22 /* QuizModel.swift */; }; + CDF1650D232D5FD700EDFE22 /* NetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF1650C232D5FD700EDFE22 /* NetworkManager.swift */; }; + CDF1650F232D790A00EDFE22 /* NetworkErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF1650E232D790A00EDFE22 /* NetworkErrors.swift */; }; + CDF16513232D7D6100EDFE22 /* QuizAPITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF16512232D7D6100EDFE22 /* QuizAPITests.swift */; }; + CDF16516232D83CF00EDFE22 /* QuizModel.json in Resources */ = {isa = PBXBuildFile; fileRef = CDF16515232D83CF00EDFE22 /* QuizModel.json */; }; + CDF16518232D843400EDFE22 /* XCTestCaseExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF16517232D843400EDFE22 /* XCTestCaseExtension.swift */; }; + CDF1651B232D87F200EDFE22 /* QuizModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF1651A232D87F200EDFE22 /* QuizModelTests.swift */; }; + CDF1651D232E96ED00EDFE22 /* QuizView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF1651C232E96ED00EDFE22 /* QuizView.swift */; }; + CDF16521232E979A00EDFE22 /* Colors+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF16520232E979A00EDFE22 /* Colors+Ext.swift */; }; + CDF16524232E983100EDFE22 /* ColorsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF16523232E983100EDFE22 /* ColorsTests.swift */; }; + CDF16526232E996A00EDFE22 /* Fonts+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF16525232E996A00EDFE22 /* Fonts+Ext.swift */; }; + CDF16529232E9EE800EDFE22 /* SF-Pro-Display-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = CDF16527232E9EE800EDFE22 /* SF-Pro-Display-Regular.otf */; }; + CDF1652A232E9EE800EDFE22 /* SF-Pro-Display-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = CDF16527232E9EE800EDFE22 /* SF-Pro-Display-Regular.otf */; }; + CDF1652B232E9EE800EDFE22 /* SF-Pro-Display-Semibold.otf in Resources */ = {isa = PBXBuildFile; fileRef = CDF16528232E9EE800EDFE22 /* SF-Pro-Display-Semibold.otf */; }; + CDF1652C232E9EE800EDFE22 /* SF-Pro-Display-Semibold.otf in Resources */ = {isa = PBXBuildFile; fileRef = CDF16528232E9EE800EDFE22 /* SF-Pro-Display-Semibold.otf */; }; + CDF1652F232EA39300EDFE22 /* FontFamily.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF1652E232EA39300EDFE22 /* FontFamily.swift */; }; + CDF16530232EA39300EDFE22 /* FontFamily.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF1652E232EA39300EDFE22 /* FontFamily.swift */; }; + CDF16532232EA3D200EDFE22 /* SF-Pro-Display-Bold.otf in Resources */ = {isa = PBXBuildFile; fileRef = CDF16531232EA3D200EDFE22 /* SF-Pro-Display-Bold.otf */; }; + CDF16533232EA3D200EDFE22 /* SF-Pro-Display-Bold.otf in Resources */ = {isa = PBXBuildFile; fileRef = CDF16531232EA3D200EDFE22 /* SF-Pro-Display-Bold.otf */; }; + CDF16535232EA71B00EDFE22 /* FooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF16534232EA71B00EDFE22 /* FooterView.swift */; }; + CDF16536232EA71B00EDFE22 /* FooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF16534232EA71B00EDFE22 /* FooterView.swift */; }; + CDF1653B232EC18900EDFE22 /* Fonts+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF16525232E996A00EDFE22 /* Fonts+Ext.swift */; }; + CDF1653C232EC19600EDFE22 /* Colors+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF16520232E979A00EDFE22 /* Colors+Ext.swift */; }; + CDF16544232EEE9300EDFE22 /* QuizViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF16543232EEE9300EDFE22 /* QuizViewModel.swift */; }; + CDF16547232EFB2700EDFE22 /* AlertHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF16546232EFB2700EDFE22 /* AlertHelper.swift */; }; + CDF16548232EFB2700EDFE22 /* AlertHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF16546232EFB2700EDFE22 /* AlertHelper.swift */; }; + CDF1654C232EFBC100EDFE22 /* AlertHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF1654A232EFBC100EDFE22 /* AlertHelperTests.swift */; }; + CDF1654E232FB3ED00EDFE22 /* TableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF1654D232FB3ED00EDFE22 /* TableViewDataSource.swift */; }; + CDF1654F232FB3ED00EDFE22 /* TableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF1654D232FB3ED00EDFE22 /* TableViewDataSource.swift */; }; + CDF16552232FCAC300EDFE22 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = CDF16550232FCAC300EDFE22 /* Localizable.strings */; }; + CDF16553232FCAC300EDFE22 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = CDF16550232FCAC300EDFE22 /* Localizable.strings */; }; + CDF16556232FCC3900EDFE22 /* LocalizableStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF16555232FCC3900EDFE22 /* LocalizableStrings.swift */; }; + CDF16557232FCC3900EDFE22 /* LocalizableStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF16555232FCC3900EDFE22 /* LocalizableStrings.swift */; }; + CDF16559232FD2B100EDFE22 /* TableView+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF16558232FD2B100EDFE22 /* TableView+Ext.swift */; }; + CDF1655A232FD2B100EDFE22 /* TableView+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF16558232FD2B100EDFE22 /* TableView+Ext.swift */; }; + CDF1655D232FD8BE00EDFE22 /* String+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF1655C232FD8BE00EDFE22 /* String+Ext.swift */; }; + CDF1655E232FD8BE00EDFE22 /* String+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDF1655C232FD8BE00EDFE22 /* String+Ext.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + CDF164D6232C473F00EDFE22 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = CDF164B9232C473E00EDFE22 /* Project object */; + proxyType = 1; + remoteGlobalIDString = CDF164C0232C473E00EDFE22; + remoteInfo = quizchallenge; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + CDF164C1232C473E00EDFE22 /* quizchallenge.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = quizchallenge.app; sourceTree = BUILT_PRODUCTS_DIR; }; + CDF164C4232C473E00EDFE22 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + CDF164C6232C473E00EDFE22 /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; + CDF164CB232C473F00EDFE22 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + CDF164CE232C473F00EDFE22 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + CDF164D0232C473F00EDFE22 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + CDF164D5232C473F00EDFE22 /* quizchallengeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = quizchallengeTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + CDF164D9232C473F00EDFE22 /* quizchallengeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = quizchallengeTests.swift; sourceTree = ""; }; + CDF164DB232C473F00EDFE22 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + CDF164F9232D4A9300EDFE22 /* EndpointProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndpointProtocol.swift; sourceTree = ""; }; + CDF164FB232D4AF400EDFE22 /* HTTPTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPTypes.swift; sourceTree = ""; }; + CDF16505232D4E3D00EDFE22 /* NetworkRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkRouter.swift; sourceTree = ""; }; + CDF16507232D5C9300EDFE22 /* QuizAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuizAPI.swift; sourceTree = ""; }; + CDF1650A232D5E1900EDFE22 /* QuizModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuizModel.swift; sourceTree = ""; }; + CDF1650C232D5FD700EDFE22 /* NetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = ""; }; + CDF1650E232D790A00EDFE22 /* NetworkErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkErrors.swift; sourceTree = ""; }; + CDF16512232D7D6100EDFE22 /* QuizAPITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuizAPITests.swift; sourceTree = ""; }; + CDF16515232D83CF00EDFE22 /* QuizModel.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = QuizModel.json; sourceTree = ""; }; + CDF16517232D843400EDFE22 /* XCTestCaseExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCTestCaseExtension.swift; sourceTree = ""; }; + CDF1651A232D87F200EDFE22 /* QuizModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuizModelTests.swift; sourceTree = ""; }; + CDF1651C232E96ED00EDFE22 /* QuizView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuizView.swift; sourceTree = ""; }; + CDF16520232E979A00EDFE22 /* Colors+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Colors+Ext.swift"; sourceTree = ""; }; + CDF16523232E983100EDFE22 /* ColorsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorsTests.swift; sourceTree = ""; }; + CDF16525232E996A00EDFE22 /* Fonts+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Fonts+Ext.swift"; sourceTree = ""; }; + CDF16527232E9EE800EDFE22 /* SF-Pro-Display-Regular.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SF-Pro-Display-Regular.otf"; sourceTree = ""; }; + CDF16528232E9EE800EDFE22 /* SF-Pro-Display-Semibold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SF-Pro-Display-Semibold.otf"; sourceTree = ""; }; + CDF1652E232EA39300EDFE22 /* FontFamily.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontFamily.swift; sourceTree = ""; }; + CDF16531232EA3D200EDFE22 /* SF-Pro-Display-Bold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SF-Pro-Display-Bold.otf"; sourceTree = ""; }; + CDF16534232EA71B00EDFE22 /* FooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FooterView.swift; sourceTree = ""; }; + CDF16543232EEE9300EDFE22 /* QuizViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuizViewModel.swift; sourceTree = ""; }; + CDF16546232EFB2700EDFE22 /* AlertHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertHelper.swift; sourceTree = ""; }; + CDF1654A232EFBC100EDFE22 /* AlertHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertHelperTests.swift; sourceTree = ""; }; + CDF1654D232FB3ED00EDFE22 /* TableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewDataSource.swift; sourceTree = ""; }; + CDF16551232FCAC300EDFE22 /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = quizchallenge/Base.lproj/Localizable.strings; sourceTree = ""; }; + CDF16555232FCC3900EDFE22 /* LocalizableStrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizableStrings.swift; sourceTree = ""; }; + CDF16558232FD2B100EDFE22 /* TableView+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TableView+Ext.swift"; sourceTree = ""; }; + CDF1655C232FD8BE00EDFE22 /* String+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Ext.swift"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + CDF164BE232C473E00EDFE22 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + CDF164D2232C473F00EDFE22 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + CDF164B8232C473E00EDFE22 = { + isa = PBXGroup; + children = ( + CDF164C3232C473E00EDFE22 /* quizchallenge */, + CDF164D8232C473F00EDFE22 /* quizchallengeTests */, + CDF164C2232C473E00EDFE22 /* Products */, + ); + sourceTree = ""; + }; + CDF164C2232C473E00EDFE22 /* Products */ = { + isa = PBXGroup; + children = ( + CDF164C1232C473E00EDFE22 /* quizchallenge.app */, + CDF164D5232C473F00EDFE22 /* quizchallengeTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + CDF164C3232C473E00EDFE22 /* quizchallenge */ = { + isa = PBXGroup; + children = ( + CDF16537232EB19300EDFE22 /* Helpers */, + CDF1651F232E978300EDFE22 /* Resources */, + CDF164F2232C4AA300EDFE22 /* Network */, + CDF16509232D5E0100EDFE22 /* Models */, + CDF1651E232E96F200EDFE22 /* Views */, + CDF1655F232FE6B100EDFE22 /* ViewModels */, + CDF16560232FE6D900EDFE22 /* ViewControllers */, + CDF164C4232C473E00EDFE22 /* AppDelegate.swift */, + CDF164D0232C473F00EDFE22 /* Info.plist */, + ); + path = quizchallenge; + sourceTree = ""; + }; + CDF164D8232C473F00EDFE22 /* quizchallengeTests */ = { + isa = PBXGroup; + children = ( + CDF16549232EFBAF00EDFE22 /* Helper */, + CDF16522232E982300EDFE22 /* Resources */, + CDF16519232D87E200EDFE22 /* Model */, + CDF16514232D83AF00EDFE22 /* JSONs */, + CDF16511232D7D4E00EDFE22 /* Network */, + CDF164D9232C473F00EDFE22 /* quizchallengeTests.swift */, + CDF164DB232C473F00EDFE22 /* Info.plist */, + ); + path = quizchallengeTests; + sourceTree = ""; + }; + CDF164F2232C4AA300EDFE22 /* Network */ = { + isa = PBXGroup; + children = ( + CDF164F9232D4A9300EDFE22 /* EndpointProtocol.swift */, + CDF164FB232D4AF400EDFE22 /* HTTPTypes.swift */, + CDF1650E232D790A00EDFE22 /* NetworkErrors.swift */, + CDF16505232D4E3D00EDFE22 /* NetworkRouter.swift */, + CDF1650C232D5FD700EDFE22 /* NetworkManager.swift */, + CDF16507232D5C9300EDFE22 /* QuizAPI.swift */, + ); + path = Network; + sourceTree = ""; + }; + CDF16509232D5E0100EDFE22 /* Models */ = { + isa = PBXGroup; + children = ( + CDF1650A232D5E1900EDFE22 /* QuizModel.swift */, + ); + path = Models; + sourceTree = ""; + }; + CDF16511232D7D4E00EDFE22 /* Network */ = { + isa = PBXGroup; + children = ( + CDF16512232D7D6100EDFE22 /* QuizAPITests.swift */, + ); + path = Network; + sourceTree = ""; + }; + CDF16514232D83AF00EDFE22 /* JSONs */ = { + isa = PBXGroup; + children = ( + CDF16515232D83CF00EDFE22 /* QuizModel.json */, + CDF16517232D843400EDFE22 /* XCTestCaseExtension.swift */, + ); + path = JSONs; + sourceTree = ""; + }; + CDF16519232D87E200EDFE22 /* Model */ = { + isa = PBXGroup; + children = ( + CDF1651A232D87F200EDFE22 /* QuizModelTests.swift */, + ); + path = Model; + sourceTree = ""; + }; + CDF1651E232E96F200EDFE22 /* Views */ = { + isa = PBXGroup; + children = ( + CDF1651C232E96ED00EDFE22 /* QuizView.swift */, + CDF16534232EA71B00EDFE22 /* FooterView.swift */, + ); + path = Views; + sourceTree = ""; + }; + CDF1651F232E978300EDFE22 /* Resources */ = { + isa = PBXGroup; + children = ( + CDF164CD232C473F00EDFE22 /* LaunchScreen.storyboard */, + CDF164CB232C473F00EDFE22 /* Assets.xcassets */, + CDF1655B232FD3B200EDFE22 /* LocalizableStrings */, + CDF1652D232E9EF000EDFE22 /* Fonts */, + CDF16520232E979A00EDFE22 /* Colors+Ext.swift */, + CDF16525232E996A00EDFE22 /* Fonts+Ext.swift */, + ); + path = Resources; + sourceTree = ""; + }; + CDF16522232E982300EDFE22 /* Resources */ = { + isa = PBXGroup; + children = ( + CDF16523232E983100EDFE22 /* ColorsTests.swift */, + ); + path = Resources; + sourceTree = ""; + }; + CDF1652D232E9EF000EDFE22 /* Fonts */ = { + isa = PBXGroup; + children = ( + CDF16531232EA3D200EDFE22 /* SF-Pro-Display-Bold.otf */, + CDF16527232E9EE800EDFE22 /* SF-Pro-Display-Regular.otf */, + CDF16528232E9EE800EDFE22 /* SF-Pro-Display-Semibold.otf */, + CDF1652E232EA39300EDFE22 /* FontFamily.swift */, + ); + path = Fonts; + sourceTree = ""; + }; + CDF16537232EB19300EDFE22 /* Helpers */ = { + isa = PBXGroup; + children = ( + CDF16546232EFB2700EDFE22 /* AlertHelper.swift */, + CDF1654D232FB3ED00EDFE22 /* TableViewDataSource.swift */, + CDF16558232FD2B100EDFE22 /* TableView+Ext.swift */, + CDF1655C232FD8BE00EDFE22 /* String+Ext.swift */, + ); + path = Helpers; + sourceTree = ""; + }; + CDF16549232EFBAF00EDFE22 /* Helper */ = { + isa = PBXGroup; + children = ( + CDF1654A232EFBC100EDFE22 /* AlertHelperTests.swift */, + ); + path = Helper; + sourceTree = ""; + }; + CDF1655B232FD3B200EDFE22 /* LocalizableStrings */ = { + isa = PBXGroup; + children = ( + CDF16555232FCC3900EDFE22 /* LocalizableStrings.swift */, + CDF16550232FCAC300EDFE22 /* Localizable.strings */, + ); + path = LocalizableStrings; + sourceTree = ""; + }; + CDF1655F232FE6B100EDFE22 /* ViewModels */ = { + isa = PBXGroup; + children = ( + CDF16543232EEE9300EDFE22 /* QuizViewModel.swift */, + ); + path = ViewModels; + sourceTree = ""; + }; + CDF16560232FE6D900EDFE22 /* ViewControllers */ = { + isa = PBXGroup; + children = ( + CDF164C6232C473E00EDFE22 /* MainViewController.swift */, + ); + path = ViewControllers; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + CDF164C0232C473E00EDFE22 /* quizchallenge */ = { + isa = PBXNativeTarget; + buildConfigurationList = CDF164E9232C473F00EDFE22 /* Build configuration list for PBXNativeTarget "quizchallenge" */; + buildPhases = ( + CDF164BD232C473E00EDFE22 /* Sources */, + CDF164BE232C473E00EDFE22 /* Frameworks */, + CDF164BF232C473E00EDFE22 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = quizchallenge; + productName = quizchallenge; + productReference = CDF164C1232C473E00EDFE22 /* quizchallenge.app */; + productType = "com.apple.product-type.application"; + }; + CDF164D4232C473F00EDFE22 /* quizchallengeTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = CDF164EC232C473F00EDFE22 /* Build configuration list for PBXNativeTarget "quizchallengeTests" */; + buildPhases = ( + CDF164D1232C473F00EDFE22 /* Sources */, + CDF164D2232C473F00EDFE22 /* Frameworks */, + CDF164D3232C473F00EDFE22 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + CDF164D7232C473F00EDFE22 /* PBXTargetDependency */, + ); + name = quizchallengeTests; + productName = quizchallengeTests; + productReference = CDF164D5232C473F00EDFE22 /* quizchallengeTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + CDF164B9232C473E00EDFE22 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1020; + LastUpgradeCheck = 1020; + ORGANIZATIONNAME = alnp; + TargetAttributes = { + CDF164C0232C473E00EDFE22 = { + CreatedOnToolsVersion = 10.2.1; + }; + CDF164D4232C473F00EDFE22 = { + CreatedOnToolsVersion = 10.2.1; + TestTargetID = CDF164C0232C473E00EDFE22; + }; + }; + }; + buildConfigurationList = CDF164BC232C473E00EDFE22 /* Build configuration list for PBXProject "quizchallenge" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = CDF164B8232C473E00EDFE22; + productRefGroup = CDF164C2232C473E00EDFE22 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + CDF164C0232C473E00EDFE22 /* quizchallenge */, + CDF164D4232C473F00EDFE22 /* quizchallengeTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + CDF164BF232C473E00EDFE22 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CDF164CF232C473F00EDFE22 /* LaunchScreen.storyboard in Resources */, + CDF164CC232C473F00EDFE22 /* Assets.xcassets in Resources */, + CDF16532232EA3D200EDFE22 /* SF-Pro-Display-Bold.otf in Resources */, + CDF16552232FCAC300EDFE22 /* Localizable.strings in Resources */, + CDF1652B232E9EE800EDFE22 /* SF-Pro-Display-Semibold.otf in Resources */, + CDF16529232E9EE800EDFE22 /* SF-Pro-Display-Regular.otf in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + CDF164D3232C473F00EDFE22 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CDF16553232FCAC300EDFE22 /* Localizable.strings in Resources */, + CDF1652C232E9EE800EDFE22 /* SF-Pro-Display-Semibold.otf in Resources */, + CDF1652A232E9EE800EDFE22 /* SF-Pro-Display-Regular.otf in Resources */, + CDF16533232EA3D200EDFE22 /* SF-Pro-Display-Bold.otf in Resources */, + CDF16516232D83CF00EDFE22 /* QuizModel.json in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + CDF164BD232C473E00EDFE22 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CDF16508232D5C9300EDFE22 /* QuizAPI.swift in Sources */, + CDF16535232EA71B00EDFE22 /* FooterView.swift in Sources */, + CDF1654E232FB3ED00EDFE22 /* TableViewDataSource.swift in Sources */, + CDF16521232E979A00EDFE22 /* Colors+Ext.swift in Sources */, + CDF16544232EEE9300EDFE22 /* QuizViewModel.swift in Sources */, + CDF1650D232D5FD700EDFE22 /* NetworkManager.swift in Sources */, + CDF16547232EFB2700EDFE22 /* AlertHelper.swift in Sources */, + CDF16506232D4E3D00EDFE22 /* NetworkRouter.swift in Sources */, + CDF16526232E996A00EDFE22 /* Fonts+Ext.swift in Sources */, + CDF16559232FD2B100EDFE22 /* TableView+Ext.swift in Sources */, + CDF1650F232D790A00EDFE22 /* NetworkErrors.swift in Sources */, + CDF1650B232D5E1900EDFE22 /* QuizModel.swift in Sources */, + CDF1655D232FD8BE00EDFE22 /* String+Ext.swift in Sources */, + CDF164FA232D4A9300EDFE22 /* EndpointProtocol.swift in Sources */, + CDF164C7232C473E00EDFE22 /* MainViewController.swift in Sources */, + CDF16556232FCC3900EDFE22 /* LocalizableStrings.swift in Sources */, + CDF1652F232EA39300EDFE22 /* FontFamily.swift in Sources */, + CDF1651D232E96ED00EDFE22 /* QuizView.swift in Sources */, + CDF164C5232C473E00EDFE22 /* AppDelegate.swift in Sources */, + CDF164FC232D4AF400EDFE22 /* HTTPTypes.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + CDF164D1232C473F00EDFE22 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CDF1654C232EFBC100EDFE22 /* AlertHelperTests.swift in Sources */, + CDF16530232EA39300EDFE22 /* FontFamily.swift in Sources */, + CDF1655E232FD8BE00EDFE22 /* String+Ext.swift in Sources */, + CDF1653C232EC19600EDFE22 /* Colors+Ext.swift in Sources */, + CDF16536232EA71B00EDFE22 /* FooterView.swift in Sources */, + CDF1651B232D87F200EDFE22 /* QuizModelTests.swift in Sources */, + CDF16513232D7D6100EDFE22 /* QuizAPITests.swift in Sources */, + CDF1654F232FB3ED00EDFE22 /* TableViewDataSource.swift in Sources */, + CDF1655A232FD2B100EDFE22 /* TableView+Ext.swift in Sources */, + CDF16518232D843400EDFE22 /* XCTestCaseExtension.swift in Sources */, + CDF164DA232C473F00EDFE22 /* quizchallengeTests.swift in Sources */, + CDF1653B232EC18900EDFE22 /* Fonts+Ext.swift in Sources */, + CDF16548232EFB2700EDFE22 /* AlertHelper.swift in Sources */, + CDF16524232E983100EDFE22 /* ColorsTests.swift in Sources */, + CDF16557232FCC3900EDFE22 /* LocalizableStrings.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + CDF164D7232C473F00EDFE22 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = CDF164C0232C473E00EDFE22 /* quizchallenge */; + targetProxy = CDF164D6232C473F00EDFE22 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + CDF164CD232C473F00EDFE22 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + CDF164CE232C473F00EDFE22 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; + CDF16550232FCAC300EDFE22 /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + CDF16551232FCAC300EDFE22 /* Base */, + ); + name = Localizable.strings; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + CDF164E7232C473F00EDFE22 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_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 = 12.2; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + CDF164E8232C473F00EDFE22 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_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 = 12.2; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + CDF164EA232C473F00EDFE22 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = quizchallenge/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.alnp.quizchallenge; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + CDF164EB232C473F00EDFE22 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = quizchallenge/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.alnp.quizchallenge; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + CDF164ED232C473F00EDFE22 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = quizchallengeTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.alnp.quizchallengeTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/quizchallenge.app/quizchallenge"; + }; + name = Debug; + }; + CDF164EE232C473F00EDFE22 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = quizchallengeTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.alnp.quizchallengeTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/quizchallenge.app/quizchallenge"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + CDF164BC232C473E00EDFE22 /* Build configuration list for PBXProject "quizchallenge" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CDF164E7232C473F00EDFE22 /* Debug */, + CDF164E8232C473F00EDFE22 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + CDF164E9232C473F00EDFE22 /* Build configuration list for PBXNativeTarget "quizchallenge" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CDF164EA232C473F00EDFE22 /* Debug */, + CDF164EB232C473F00EDFE22 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + CDF164EC232C473F00EDFE22 /* Build configuration list for PBXNativeTarget "quizchallengeTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CDF164ED232C473F00EDFE22 /* Debug */, + CDF164EE232C473F00EDFE22 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = CDF164B9232C473E00EDFE22 /* Project object */; +} diff --git a/quizchallenge/quizchallenge.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/quizchallenge/quizchallenge.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..d8c2189 --- /dev/null +++ b/quizchallenge/quizchallenge.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/quizchallenge/quizchallenge.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/quizchallenge/quizchallenge.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/quizchallenge/quizchallenge.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/quizchallenge/quizchallenge/AppDelegate.swift b/quizchallenge/quizchallenge/AppDelegate.swift new file mode 100644 index 0000000..e222fa4 --- /dev/null +++ b/quizchallenge/quizchallenge/AppDelegate.swift @@ -0,0 +1,28 @@ +// +// AppDelegate.swift +// quizchallenge +// +// Created by alessandra.l.pereira on 13/09/19. +// Copyright © 2019 alnp. All rights reserved. +// + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + + let viewModel = QuizViewModel() + let controller = MainViewController(viewModel: viewModel) + viewModel.controller = controller + + self.window = UIWindow(frame: UIScreen.main.bounds) + self.window?.rootViewController = controller + self.window?.makeKeyAndVisible() + return true + } +} + diff --git a/quizchallenge/quizchallenge/Helpers/AlertHelper.swift b/quizchallenge/quizchallenge/Helpers/AlertHelper.swift new file mode 100644 index 0000000..12ba816 --- /dev/null +++ b/quizchallenge/quizchallenge/Helpers/AlertHelper.swift @@ -0,0 +1,36 @@ +import UIKit + +class AlertHelper { + static func createLoadingAlert(title: String) -> UIAlertController { + let alertController = UIAlertController(title: title, message: nil, preferredStyle: .alert) + alertController.view.heightAnchor.constraint(equalToConstant: 150).isActive = true + + let activityIndicator = UIActivityIndicatorView(frame: alertController.view.bounds) + activityIndicator.style = .whiteLarge + activityIndicator.color = .black + activityIndicator.translatesAutoresizingMaskIntoConstraints = false + + alertController.view.addSubview(activityIndicator) + + activityIndicator.centerXAnchor.constraint(equalTo: alertController.view.centerXAnchor).isActive = true + activityIndicator.centerYAnchor.constraint(equalTo: alertController.view.centerYAnchor, + constant: 20).isActive = true + + activityIndicator.isUserInteractionEnabled = false + activityIndicator.hidesWhenStopped = true + activityIndicator.startAnimating() + + return alertController + } + + static func createAlert(title: String, message: String?, + buttonTitle: String, + handler: @escaping () -> Void ) -> UIAlertController { + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + let alertAction = UIAlertAction(title: buttonTitle, style: .default) { UIAlertAction in + handler() + } + alertController.addAction(alertAction) + return alertController + } +} diff --git a/quizchallenge/quizchallenge/Helpers/String+Ext.swift b/quizchallenge/quizchallenge/Helpers/String+Ext.swift new file mode 100644 index 0000000..67e248b --- /dev/null +++ b/quizchallenge/quizchallenge/Helpers/String+Ext.swift @@ -0,0 +1,13 @@ +extension String { + func timeFormatted() -> String { + guard let totalSeconds = Int(self) else { return "" } + let minutes: Int = totalSeconds / 60 + let seconds: Int = totalSeconds % 60 + return String(format: "%02d:%02d", minutes, seconds) + } + + func numberFormatted() -> String { + guard let number = Int(self) else { return "" } + return String(format: "%02d", number) + } +} diff --git a/quizchallenge/quizchallenge/Helpers/TableView+Ext.swift b/quizchallenge/quizchallenge/Helpers/TableView+Ext.swift new file mode 100644 index 0000000..f8cd4c5 --- /dev/null +++ b/quizchallenge/quizchallenge/Helpers/TableView+Ext.swift @@ -0,0 +1,11 @@ +import UIKit + +enum ReusableIdentifier: String { + case label = "LabelCell" +} + +extension UITableView { + func register(_ cellClass: AnyClass?, forCellReuseIdentifier identifier: ReusableIdentifier) { + register(cellClass, forCellReuseIdentifier: identifier.rawValue) + } +} diff --git a/quizchallenge/quizchallenge/Helpers/TableViewDataSource.swift b/quizchallenge/quizchallenge/Helpers/TableViewDataSource.swift new file mode 100644 index 0000000..8a4b6c8 --- /dev/null +++ b/quizchallenge/quizchallenge/Helpers/TableViewDataSource.swift @@ -0,0 +1,32 @@ +import UIKit + +public class TableViewDataSource: NSObject, UITableViewDelegate, UITableViewDataSource { + + var items: [String] + private let reuseIdentifier: ReusableIdentifier + + init(items: [String], + reuseIdentifier: ReusableIdentifier) { + self.items = items + self.reuseIdentifier = reuseIdentifier + } + + public func update(with items: [String]) { + self.items = items + } + + public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return items.count + } + + public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let item = items[indexPath.row] + let cell = tableView.dequeueReusableCell( + withIdentifier: reuseIdentifier.rawValue, + for: indexPath + ) + + cell.textLabel?.text = item + return cell + } +} diff --git a/quizchallenge/quizchallenge/Info.plist b/quizchallenge/quizchallenge/Info.plist new file mode 100644 index 0000000..198dc1b --- /dev/null +++ b/quizchallenge/quizchallenge/Info.plist @@ -0,0 +1,49 @@ + + + + + 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 + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIAppFonts + + SF-Pro-Display-Regular.otf + SF-Pro-Display-Semibold.otf + SF-Pro-Display-Bold.otf + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/quizchallenge/quizchallenge/Models/QuizModel.swift b/quizchallenge/quizchallenge/Models/QuizModel.swift new file mode 100644 index 0000000..8b2dfc6 --- /dev/null +++ b/quizchallenge/quizchallenge/Models/QuizModel.swift @@ -0,0 +1,20 @@ +import Foundation + +struct QuizModel { + var question: String = "" + var answer: [String] = [] +} + +extension QuizModel: Decodable { + private enum QuizApiResponseCodingKeys: String, CodingKey { + case question + case answer + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: QuizApiResponseCodingKeys.self) + + question = try container.decode(String.self, forKey: .question) + answer = try container.decode([String].self, forKey: .answer) + } +} diff --git a/quizchallenge/quizchallenge/Network/EndpointProtocol.swift b/quizchallenge/quizchallenge/Network/EndpointProtocol.swift new file mode 100644 index 0000000..0095d76 --- /dev/null +++ b/quizchallenge/quizchallenge/Network/EndpointProtocol.swift @@ -0,0 +1,9 @@ +import Foundation + +protocol EndpointType { + var baseURL: URL { get } + var path: String { get } + var httpMethod: HTTPMethod { get } + var task: HTTPTask { get } + var headers: HTTPHeaders? { get } +} diff --git a/quizchallenge/quizchallenge/Network/HTTPTypes.swift b/quizchallenge/quizchallenge/Network/HTTPTypes.swift new file mode 100644 index 0000000..f9c7482 --- /dev/null +++ b/quizchallenge/quizchallenge/Network/HTTPTypes.swift @@ -0,0 +1,13 @@ +public typealias HTTPHeaders = [String:String] + +public enum HTTPMethod: String { + case get = "GET" + case post = "POST" + case put = "PUT" + case patch = "PATCH" + case delete = "DELETE" +} + +public enum HTTPTask { + case request +} diff --git a/quizchallenge/quizchallenge/Network/NetworkErrors.swift b/quizchallenge/quizchallenge/Network/NetworkErrors.swift new file mode 100644 index 0000000..b79df9b --- /dev/null +++ b/quizchallenge/quizchallenge/Network/NetworkErrors.swift @@ -0,0 +1,11 @@ +enum NetworkError: String, Error { + case noConnection = "No network connection" + case parametersNil = "Paramenters were nil" + case encodingFailed = "Paramenter encoding failed" + case missingURL = "URL is nil" + + case badRequest = "Bad Request" + case failed = "Network request failed" + case noData = "Response returned with no data to encode" + case unableToDecode = "Unable to decode the response" +} diff --git a/quizchallenge/quizchallenge/Network/NetworkManager.swift b/quizchallenge/quizchallenge/Network/NetworkManager.swift new file mode 100644 index 0000000..4154eed --- /dev/null +++ b/quizchallenge/quizchallenge/Network/NetworkManager.swift @@ -0,0 +1,49 @@ +import Foundation + +enum Result { + case success(T) + case failure(Error) +} + +protocol NetworkManagerType { + func getQuiz(completion: @escaping (_ result: Result) -> ()) +} + +class NetworkManager: NetworkManagerType { + private let router = Router() + + func getQuiz(completion: @escaping (_ result: Result) -> ()) { + router.request(.quiz, completion: { data, response, error in + if error != nil { + completion(.failure(NetworkError.noConnection)) + } + + if let response = response as? HTTPURLResponse { + let result = self.handleNetworkResponse(response) + switch result { + case .success: + guard let responseData = data else { + completion(.failure(NetworkError.noData)) + return + } + do { + let apiResponse = try JSONDecoder().decode(QuizModel.self, from: responseData) + completion(.success(apiResponse)) + }catch { + completion(.failure(NetworkError.unableToDecode)) + } + case .failure(let networkFailureError): + completion(.failure(networkFailureError)) + } + } + } + )} + + fileprivate func handleNetworkResponse(_ response: HTTPURLResponse) -> Result { + switch response.statusCode { + case 200...299: return .success(response) + case 501...599: return .failure(NetworkError.badRequest) + default: return .failure(NetworkError.failed) + } + } +} diff --git a/quizchallenge/quizchallenge/Network/NetworkRouter.swift b/quizchallenge/quizchallenge/Network/NetworkRouter.swift new file mode 100644 index 0000000..3b00cf0 --- /dev/null +++ b/quizchallenge/quizchallenge/Network/NetworkRouter.swift @@ -0,0 +1,42 @@ +import Foundation + +public typealias NetworkRouterCompletion = (_ data: Data?,_ response: URLResponse?,_ error: Error?) -> () + +protocol NetworkRouter: class { + associatedtype Endpoint: EndpointType + func request(_ route: Endpoint, completion: @escaping NetworkRouterCompletion) + func cancel() +} + +class Router: NetworkRouter { + private var task: URLSessionTask? + + func request(_ route: Endpoint, completion: @escaping NetworkRouterCompletion) { + let session = URLSession.shared + do { + let request = try self.buildRequest(from: route) + task = session.dataTask(with: request, completionHandler: { data, response, error in + completion(data, response, error) + }) + } catch { + completion(nil, nil, error) + } + self.task?.resume() + } + + func cancel() { + self.task?.cancel() + } + + fileprivate func buildRequest(from route: Endpoint) throws -> URLRequest { + var request = URLRequest(url: route.baseURL.appendingPathComponent(route.path), + cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, + timeoutInterval: 10.0) + request.httpMethod = route.httpMethod.rawValue + switch route.task { + case .request: + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + } + return request + } +} diff --git a/quizchallenge/quizchallenge/Network/QuizAPI.swift b/quizchallenge/quizchallenge/Network/QuizAPI.swift new file mode 100644 index 0000000..5354eb1 --- /dev/null +++ b/quizchallenge/quizchallenge/Network/QuizAPI.swift @@ -0,0 +1,36 @@ +import Foundation + +public enum QuizAPI { + case quiz +} + +extension QuizAPI: EndpointType { + + var environmentBaseURL : String { + return "https://codechallenge.arctouch.com/" + } + + var baseURL: URL { + guard let url = URL(string: environmentBaseURL) else { fatalError("baseURL could not be configured.")} + return url + } + + var path: String { + switch self { + case .quiz: + return "quiz/1" + } + } + + var httpMethod: HTTPMethod { + return .get + } + + var task: HTTPTask { + return .request + } + + var headers: HTTPHeaders? { + return nil + } +} diff --git a/quizchallenge/quizchallenge/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/quizchallenge/quizchallenge/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d8db8d6 --- /dev/null +++ b/quizchallenge/quizchallenge/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "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" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/quizchallenge/quizchallenge/Resources/Assets.xcassets/Contents.json b/quizchallenge/quizchallenge/Resources/Assets.xcassets/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/quizchallenge/quizchallenge/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/quizchallenge/quizchallenge/Resources/Base.lproj/LaunchScreen.storyboard b/quizchallenge/quizchallenge/Resources/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..cb81a1e --- /dev/null +++ b/quizchallenge/quizchallenge/Resources/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + SFProDisplay-Bold + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/quizchallenge/quizchallenge/Resources/Colors+Ext.swift b/quizchallenge/quizchallenge/Resources/Colors+Ext.swift new file mode 100644 index 0000000..9080306 --- /dev/null +++ b/quizchallenge/quizchallenge/Resources/Colors+Ext.swift @@ -0,0 +1,16 @@ +import UIKit + +extension UIColor { + + class var arcGrey: UIColor { + return UIColor(red: 245.0 / 255.0, green: 245.0 / 255.0, blue: 245.0 / 255.0, alpha: 1.0) + } + + class var arcOrange: UIColor { + return UIColor(red: 255.0 / 255.0, green: 131.0 / 255.0, blue: 0.0 / 255.0, alpha: 1.0) + } + + class var darkOrange: UIColor { + return UIColor(red: 200.0 / 255.0, green: 100.0 / 255.0, blue: 0.0 / 255.0, alpha: 1.0) + } +} diff --git a/quizchallenge/quizchallenge/Resources/Fonts+Ext.swift b/quizchallenge/quizchallenge/Resources/Fonts+Ext.swift new file mode 100644 index 0000000..7f815dc --- /dev/null +++ b/quizchallenge/quizchallenge/Resources/Fonts+Ext.swift @@ -0,0 +1,24 @@ +import UIKit + +extension UIFont { + static func customFontWithDefault(familyName: String, size: CGFloat) -> UIFont { + if let font = UIFont(name: familyName, size: size) { + return font + } + return UIFont.systemFont(ofSize: size) + } +} + +extension UIFont { + static var largeTitle: UIFont { + return customFontWithDefault(familyName: FontFamily.SFProDisplay.bold, size: 34) + } + + static var body: UIFont { + return customFontWithDefault(familyName: FontFamily.SFProDisplay.regular, size: 17) + } + + static var button: UIFont { + return customFontWithDefault(familyName: FontFamily.SFProDisplay.semiBold, size: 17) + } +} diff --git a/quizchallenge/quizchallenge/Resources/Fonts/FontFamily.swift b/quizchallenge/quizchallenge/Resources/Fonts/FontFamily.swift new file mode 100644 index 0000000..ad241e1 --- /dev/null +++ b/quizchallenge/quizchallenge/Resources/Fonts/FontFamily.swift @@ -0,0 +1,7 @@ +enum FontFamily { + enum SFProDisplay { + static let regular = "SFProDisplay-Regular" + static let semiBold = "SFProDisplay-SemiBold" + static let bold = "SFProDisplay-Bold" + } +} diff --git a/quizchallenge/quizchallenge/Resources/Fonts/SF-Pro-Display-Bold.otf b/quizchallenge/quizchallenge/Resources/Fonts/SF-Pro-Display-Bold.otf new file mode 100755 index 0000000..ecee5c2 Binary files /dev/null and b/quizchallenge/quizchallenge/Resources/Fonts/SF-Pro-Display-Bold.otf differ diff --git a/quizchallenge/quizchallenge/Resources/Fonts/SF-Pro-Display-Regular.otf b/quizchallenge/quizchallenge/Resources/Fonts/SF-Pro-Display-Regular.otf new file mode 100755 index 0000000..9f8f0bf Binary files /dev/null and b/quizchallenge/quizchallenge/Resources/Fonts/SF-Pro-Display-Regular.otf differ diff --git a/quizchallenge/quizchallenge/Resources/Fonts/SF-Pro-Display-Semibold.otf b/quizchallenge/quizchallenge/Resources/Fonts/SF-Pro-Display-Semibold.otf new file mode 100755 index 0000000..a72b5e5 Binary files /dev/null and b/quizchallenge/quizchallenge/Resources/Fonts/SF-Pro-Display-Semibold.otf differ diff --git a/quizchallenge/quizchallenge/Resources/LocalizableStrings/LocalizableStrings.swift b/quizchallenge/quizchallenge/Resources/LocalizableStrings/LocalizableStrings.swift new file mode 100644 index 0000000..bb78c45 --- /dev/null +++ b/quizchallenge/quizchallenge/Resources/LocalizableStrings/LocalizableStrings.swift @@ -0,0 +1,27 @@ +import Foundation + +public struct LocalizedStrings { + + private static let tableName = "Localizable" + private static let bundleIdentifier = "com.alnp.quizchallenge" + + public static let error = localized(forKey: "error") + public static let tryAgain = localized(forKey: "tryAgain") + public static let timeFinished = localized(forKey: "timeFinished") + public static let congratulations = localized(forKey: "congratulations") + public static let goodJob = localized(forKey: "goodJob") + public static let timeUp = localized(forKey: "timeUp") + public static let playAgain = localized(forKey: "playAgain") + public static let insertWord = localized(forKey: "insertWord") + public static let start = localized(forKey: "start") + public static let reset = localized(forKey: "reset") + public static let loading = localized(forKey: "loading") + +} + +private extension LocalizedStrings { + static func localized(forKey key: String) -> String { + let bundle = Bundle(identifier: bundleIdentifier) + return NSLocalizedString(key, tableName: tableName, bundle: bundle!, value: "", comment: "") + } +} diff --git a/quizchallenge/quizchallenge/Resources/LocalizableStrings/quizchallenge/Base.lproj/Localizable.strings b/quizchallenge/quizchallenge/Resources/LocalizableStrings/quizchallenge/Base.lproj/Localizable.strings new file mode 100644 index 0000000..27b2b5f --- /dev/null +++ b/quizchallenge/quizchallenge/Resources/LocalizableStrings/quizchallenge/Base.lproj/Localizable.strings @@ -0,0 +1,12 @@ +"error" = "Error"; +"tryAgain" = "Try Again"; +"timeFinished" = "Time finished"; +"congratulations" = "Congratulations"; +"goodJob" = "Good job! You found all the answer on time. Keep up with great work."; +"timeUp" = "Sorry, time is up! You got %d out of %d answers."; +"playAgain" = "Play Again"; +"insertWord" = "Insert Word"; +"start" = "Start"; +"reset" = "Reset"; +"loading" = "Loading..."; +"insertWord" = "Insert Word"; diff --git a/quizchallenge/quizchallenge/Resources/quizchallenge/en.lproj/Localizable.strings b/quizchallenge/quizchallenge/Resources/quizchallenge/en.lproj/Localizable.strings new file mode 100644 index 0000000..6bb0890 --- /dev/null +++ b/quizchallenge/quizchallenge/Resources/quizchallenge/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + quizchallenge + + Created by alessandra.l.pereira on 16/09/19. + Copyright © 2019 alnp. All rights reserved. +*/ diff --git a/quizchallenge/quizchallenge/ViewControllers/MainViewController.swift b/quizchallenge/quizchallenge/ViewControllers/MainViewController.swift new file mode 100644 index 0000000..7050015 --- /dev/null +++ b/quizchallenge/quizchallenge/ViewControllers/MainViewController.swift @@ -0,0 +1,135 @@ +import UIKit + +class MainViewController: UIViewController { + + private var viewModel: QuizViewModelType + private var quizView = QuizView() + private var loadingAlertController: UIAlertController = AlertHelper.createLoadingAlert(title: "") + private var alertController: UIAlertController = AlertHelper.createAlert(title: "", + message: nil, + buttonTitle: "", + handler: { }) + + init(viewModel: QuizViewModelType = QuizViewModel(), + quizView: QuizView = QuizView()) { + self.viewModel = viewModel + self.quizView = quizView + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + view = quizView + } + + override func viewDidLoad() { + super.viewDidLoad() + quizView.textField.delegate = self + quizView.footerView.delegate = self + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + viewModel.requestForQuiz() + } + + func showQuiz(with model: QuizModel, secondsToDisplay: Int) { + quizView.show(question: model.question, words: model.answer.count, seconds: secondsToDisplay) + } + + func updateTableView(with items: [String]) { + quizView.reloadTableView(with: items) + + } +} + +extension MainViewController { + func showLoadingAlert(_ title: String = LocalizedStrings.loading, completion: (() -> Void)?) { + loadingAlertController = AlertHelper.createLoadingAlert(title: title) + DispatchQueue.main.async { + self.present(self.loadingAlertController, animated: true, completion: completion) + } + } + + func dismissLoadingAlert() { + DispatchQueue.main.async { + self.loadingAlertController.dismiss(animated: true) + } + } + + func showAlert(title:String, message: String, buttonTitle: String) { + alertController = AlertHelper.createAlert(title: title, + message: message, + buttonTitle: buttonTitle) { [weak self] in + guard let self = self else { return } + self.viewModel.requestForQuiz() + } + DispatchQueue.main.async { + self.present(self.alertController, animated: true) + } + } + + func dismissAlert() { + alertController.dismiss(animated: true) + } + + func finishGame(timeout: Bool, wordsFound: Int = 0, words: Int = 0) { + quizView.footerView.stopTimer() + let title = timeout ? LocalizedStrings.timeFinished : LocalizedStrings.congratulations + let congratulationsMsg = LocalizedStrings.goodJob + let timeMsg = String(format: LocalizedStrings.timeUp, wordsFound, words) + let message = timeout ? timeMsg : congratulationsMsg + let buttonTitle = timeout ? LocalizedStrings.tryAgain : LocalizedStrings.playAgain + + alertController = AlertHelper.createAlert(title: title, + message: message, + buttonTitle: buttonTitle) { [weak self] in + guard let self = self else { return } + self.viewModel.restartQuiz() + } + DispatchQueue.main.async { + self.present(self.alertController, animated: true) + } + } +} + +extension MainViewController: UITextFieldDelegate { + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + self.view.endEditing(true) + return false + } + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + if let text = textField.text, + let textRange = Range(range, in: text) { + let updatedText = text.replacingCharacters(in: textRange, + with: string) + if viewModel.checkAnswerFor(word: updatedText) { + textField.text = "" + return false + } + } + return true + } +} + +extension MainViewController: TimerDelegate { + func wantsToStart() { + quizView.textField.isEnabled = true + quizView.textField.becomeFirstResponder() + } + + func wantsToRestart() { + quizView.textField.isEnabled = false + } + + func timerIsOver() { + quizView.textField.isEnabled = false + finishGame(timeout: true, wordsFound: viewModel.wordsFound ?? 0, words: viewModel.words ?? 0) + } + +} diff --git a/quizchallenge/quizchallenge/ViewModels/QuizViewModel.swift b/quizchallenge/quizchallenge/ViewModels/QuizViewModel.swift new file mode 100644 index 0000000..d556155 --- /dev/null +++ b/quizchallenge/quizchallenge/ViewModels/QuizViewModel.swift @@ -0,0 +1,80 @@ +import Foundation + +protocol QuizViewModelType { + var wordsFound: Int? { get } + var words: Int? { get } + + func requestForQuiz() + func restartQuiz() + func checkAnswerFor(word: String) -> Bool +} + +final class QuizViewModel: QuizViewModelType { + + var wordsFound: Int? + var words: Int? + let secondsToDisplay: Int = 300 + + weak var controller: MainViewController? + private var networkManager: NetworkManagerType + internal var model: QuizModel { + didSet { + words = model.answer.count + self.controller?.showQuiz(with: model, secondsToDisplay: secondsToDisplay) + } + } + private var userAnswers = [String]() { + didSet { + wordsFound = userAnswers.count + self.controller?.updateTableView(with: userAnswers) + if userAnswers.count == model.answer.count, !userAnswers.isEmpty { + self.controller?.finishGame(timeout: false) + } + } + } + + init(networkManager: NetworkManagerType = NetworkManager(), + model: QuizModel = QuizModel()) { + self.networkManager = networkManager + self.model = model + } + + func restartQuiz(){ + userAnswers = [] + controller?.showQuiz(with: model, secondsToDisplay: secondsToDisplay) + } + + func requestForQuiz() { + userAnswers = [] + self.controller?.showLoadingAlert(completion: { + self.networkManager.getQuiz() { result in + let alertTitle = LocalizedStrings.error + let buttonAlertTitle = LocalizedStrings.tryAgain + self.controller?.dismissLoadingAlert() + switch result { + case .success(let response): + self.model = response + case .failure(let error as NetworkError): + self.controller?.showAlert(title: alertTitle, + message: error.rawValue, + buttonTitle: buttonAlertTitle) + case .failure(let error): + self.controller?.showAlert(title: alertTitle, + message: error.localizedDescription, + buttonTitle: buttonAlertTitle) + } + } + }) + } + + func checkAnswerFor(word: String) -> Bool { + let filtredString = model.answer.filter { + if $0.lowercased() == word.lowercased(), !userAnswers.contains($0) { + userAnswers.append($0) + return true + } + return false + } + return !filtredString.isEmpty + } +} diff --git a/quizchallenge/quizchallenge/Views/FooterView.swift b/quizchallenge/quizchallenge/Views/FooterView.swift new file mode 100644 index 0000000..1ff2d8c --- /dev/null +++ b/quizchallenge/quizchallenge/Views/FooterView.swift @@ -0,0 +1,182 @@ +import UIKit + +protocol TimerDelegate: class { + func wantsToStart() + func wantsToRestart() + func timerIsOver() +} + +class FooterView: UIView { + + public var counter: Int = 0 { + didSet { + let counterText = "\(counter)".numberFormatted() + let totalText = "\(total)".numberFormatted() + counterLabel.text = counterText + "/" + totalText + } + } + + public var total: Int = 0 { + didSet { + let totalText = "\(total)".numberFormatted() + counterLabel.text = "00/\(totalText)" + } + } + + public var timerSeconds: Int = 5 { + didSet{ + timerLabel.text = "\(totalSeconds)".timeFormatted() + } + } + + public var totalSeconds: Int = 0 { + didSet{ + timerLabel.text = "\(totalSeconds)".timeFormatted() + } + } + + var delegate: TimerDelegate? + private var timer = Timer() + private var isTimerRunning: Bool = false + private let buttonHeight: CGFloat = 48.0 + + private var infoStackView: UIStackView = { + let stack = UIStackView() + stack.axis = .horizontal + stack.distribution = .fillEqually + stack.alignment = .center + stack.spacing = 16.0 + stack.translatesAutoresizingMaskIntoConstraints = false + + return stack + }() + + private var counterLabel: UILabel = { + let label = UILabel() + label.font = .largeTitle + label.numberOfLines = 0 + label.textAlignment = .left + label.textColor = .black + label.text = "0".numberFormatted() + + return label + }() + + private var timerLabel: UILabel = { + let label = UILabel() + label.font = .largeTitle + label.numberOfLines = 0 + label.textAlignment = .right + label.textColor = .black + label.text = "300".timeFormatted() + + return label + }() + + private var stackView: UIStackView = { + let stack = UIStackView() + stack.axis = .vertical + stack.distribution = .fill + stack.alignment = .fill + stack.spacing = 16.0 + stack.isLayoutMarginsRelativeArrangement = true + stack.layoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16) + stack.translatesAutoresizingMaskIntoConstraints = false + + return stack + }() + + private lazy var button: UIButton = { + let button = UIButton() + button.layer.cornerRadius = CGFloat(buttonHeight / 4.0) + button.setTitle(LocalizedStrings.start, for: .normal) + button.setTitleColor(.darkOrange, for: .highlighted) + button.titleLabel?.font = .button + button.backgroundColor = .arcOrange + + return button + }() + + + override init(frame: CGRect = .zero) { + super.init(frame: frame) + setupUI() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupUI() { + backgroundColor = UIColor.arcGrey + buildViewHierarchy() + addConstraints() + bindLayoutEvents() + } + + private func buildViewHierarchy() { + infoStackView.addArrangedSubview(counterLabel) + infoStackView.addArrangedSubview(timerLabel) + stackView.addArrangedSubview(infoStackView) + stackView.addArrangedSubview(button) + addSubview(stackView) + } + + private func addConstraints() { + translatesAutoresizingMaskIntoConstraints = false + + button.heightAnchor.constraint(equalToConstant: buttonHeight).isActive = true + + stackView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true + stackView.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true + stackView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true + stackView.leftAnchor.constraint(equalTo: self.leftAnchor).isActive = true + } + + private func bindLayoutEvents() { + button.addTarget(self, action: #selector(buttonHandler), for: .touchUpInside) + } + + @objc private func buttonHandler() { + !isTimerRunning ? startTimer() : restartTimer() + } + + private func startTimer() { + self.delegate?.wantsToStart() + button.setTitle(LocalizedStrings.reset, for: .normal) + totalSeconds = timerSeconds + self.timer = Timer.scheduledTimer(timeInterval: 1, + target: self, + selector: #selector(updateTime), + userInfo: nil, + repeats: true) + isTimerRunning = true + } + + private func restartTimer() { + self.delegate?.wantsToRestart() + button.setTitle(LocalizedStrings.start, for: .normal) + timer.invalidate() + isTimerRunning = false + totalSeconds = timerSeconds + } + + @objc private func updateTime() { + guard totalSeconds > 0 else { + timer.invalidate() + delegate?.timerIsOver() + return + } + totalSeconds -= 1 + } + + func stopTimer() { + timer.invalidate() + } + + func show(words: Int, timer: Int) { + total = words + timerSeconds = timer + restartTimer() + } +} diff --git a/quizchallenge/quizchallenge/Views/QuizView.swift b/quizchallenge/quizchallenge/Views/QuizView.swift new file mode 100644 index 0000000..7b4e090 --- /dev/null +++ b/quizchallenge/quizchallenge/Views/QuizView.swift @@ -0,0 +1,163 @@ +import UIKit + +class QuizView: UIView { + + var delegate: UITextFieldDelegate? + var dataSource: TableViewDataSource? + var footerBottomConstraint: NSLayoutConstraint? + + private var containerView = UIView() + + private var title: UILabel = { + let label = UILabel() + label.font = UIFont.largeTitle + label.numberOfLines = 0 + label.textAlignment = .left + label.translatesAutoresizingMaskIntoConstraints = false + + return label + }() + + var textField: UITextField = { + let textField = UITextField() + textField.layer.cornerRadius = CGFloat(12) + textField.enablesReturnKeyAutomatically = false + textField.returnKeyType = .done + textField.backgroundColor = .arcGrey + textField.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 20)) + textField.leftViewMode = .always + textField.placeholder = LocalizedStrings.insertWord + textField.isEnabled = false + textField.translatesAutoresizingMaskIntoConstraints = false + + return textField + }() + + let tableView: UITableView = { + let tableView = UITableView() + tableView.register(UITableViewCell.self, forCellReuseIdentifier: ReusableIdentifier.label) + tableView.backgroundColor = .white + tableView.rowHeight = UITableView.automaticDimension + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.allowsSelection = false + tableView.separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 8) + tableView.tableFooterView = UIView() + return tableView + }() + + let footerView = FooterView() + + override init(frame: CGRect = .zero) { + super.init(frame: frame) + registerNotifications() + setupUI() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupUI() { + containerView.isHidden = true + backgroundColor = .white + buildViewHierarchy() + addConstraints() + bindLayoutEvents() + } + + private func buildViewHierarchy() { + containerView.addSubview(title) + containerView.addSubview(textField) + containerView.addSubview(tableView) + addSubview(containerView) + addSubview(footerView) + } + + private func addConstraints() { + translatesAutoresizingMaskIntoConstraints = false + + containerView.translatesAutoresizingMaskIntoConstraints = false + containerView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true + containerView.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true + containerView.bottomAnchor.constraint(equalTo: footerView.bottomAnchor).isActive = true + containerView.leftAnchor.constraint(equalTo: self.leftAnchor).isActive = true + + title.leftAnchor.constraint(equalTo: containerView.leftAnchor, constant: 16).isActive = true + title.rightAnchor.constraint(equalTo: containerView.rightAnchor, constant: -16).isActive = true + title.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 44).isActive = true + + textField.leftAnchor.constraint(equalTo: containerView.leftAnchor, constant: 16).isActive = true + textField.rightAnchor.constraint(equalTo: containerView.rightAnchor, constant: -16).isActive = true + textField.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 16).isActive = true + textField.heightAnchor.constraint(equalToConstant: 48).isActive = true + + tableView.topAnchor.constraint(equalTo: textField.bottomAnchor, constant: 16).isActive = true + tableView.rightAnchor.constraint(equalTo: containerView.rightAnchor, constant: -16).isActive = true + tableView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor).isActive = true + tableView.leftAnchor.constraint(equalTo: containerView.leftAnchor, constant: 16).isActive = true + + footerView.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true + footerView.leftAnchor.constraint(equalTo: self.leftAnchor).isActive = true + footerBottomConstraint = footerView.bottomAnchor.constraint(equalTo: self.bottomAnchor) + footerBottomConstraint?.isActive = true + } + + private func bindLayoutEvents() { + textField.delegate = delegate + tableView.dataSource = dataSource + } + + private func updateCounter(_ number: Int) { + self.footerView.counter = number + } + + func show(question: String, words: Int, seconds: Int ) { + DispatchQueue.main.async { + self.containerView.isHidden = question.isEmpty + self.title.text = question + self.footerView.show(words: words, timer: seconds) + } + } + + func reloadTableView(with items: [String]){ + if let datasource = dataSource { + datasource.update(with: items) + } else { + let dataSource = TableViewDataSource(items: items, reuseIdentifier: ReusableIdentifier.label) + self.dataSource = dataSource + } + + tableView.dataSource = dataSource + tableView.delegate = dataSource + tableView.reloadData() + updateCounter(items.count) + } +} + +extension QuizView { + func registerNotifications() { + NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil) + + NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil) + + } + + @objc func keyboardWillShow(notification: NSNotification) { + guard let keyboardSize = (notification.userInfo?[ + UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue else { + return + } + if textField.isFirstResponder { + footerBottomConstraint?.isActive = false + footerBottomConstraint = footerView.bottomAnchor.constraint(equalTo: self.bottomAnchor, + constant: -keyboardSize.height) + footerBottomConstraint?.isActive = true + } + } + + @objc func keyboardWillHide() { + footerBottomConstraint?.isActive = false + footerBottomConstraint = footerView.bottomAnchor.constraint(equalTo: self.bottomAnchor) + footerBottomConstraint?.isActive = true + } +} diff --git a/quizchallenge/quizchallengeTests/Helper/AlertHelperTests.swift b/quizchallenge/quizchallengeTests/Helper/AlertHelperTests.swift new file mode 100644 index 0000000..2e0be2e --- /dev/null +++ b/quizchallenge/quizchallengeTests/Helper/AlertHelperTests.swift @@ -0,0 +1,13 @@ +import XCTest +@testable import quizchallenge + +class AlertHelperTests: XCTestCase { + + func testLoadingAlert() { + let title = "title" + let alertController = AlertHelper.createLoadingAlert(title: title) + + XCTAssertEqual(title, alertController.title) + XCTAssert((alertController as Any) is UIAlertController) + } +} diff --git a/quizchallenge/quizchallengeTests/Info.plist b/quizchallenge/quizchallengeTests/Info.plist new file mode 100644 index 0000000..6c40a6c --- /dev/null +++ b/quizchallenge/quizchallengeTests/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/quizchallenge/quizchallengeTests/JSONs/QuizModel.json b/quizchallenge/quizchallengeTests/JSONs/QuizModel.json new file mode 100644 index 0000000..5436466 --- /dev/null +++ b/quizchallenge/quizchallengeTests/JSONs/QuizModel.json @@ -0,0 +1,55 @@ +{ + "question": "What are all the java keywords?", + "answer": [ + "abstract", + "assert", + "boolean", + "break", + "byte", + "case", + "catch", + "char", + "class", + "const", + "continue", + "default", + "do", + "double", + "else", + "enum", + "extends", + "final", + "finally", + "float", + "for", + "goto", + "if", + "implements", + "import", + "instanceof", + "int", + "interface", + "long", + "native", + "new", + "package", + "private", + "protected", + "public", + "return", + "short", + "static", + "strictfp", + "super", + "switch", + "synchronized", + "this", + "throw", + "throws", + "transient", + "try", + "void", + "volatile", + "while" + ] +} diff --git a/quizchallenge/quizchallengeTests/JSONs/XCTestCaseExtension.swift b/quizchallenge/quizchallengeTests/JSONs/XCTestCaseExtension.swift new file mode 100644 index 0000000..34dfd06 --- /dev/null +++ b/quizchallenge/quizchallengeTests/JSONs/XCTestCaseExtension.swift @@ -0,0 +1,21 @@ +import XCTest +@testable import quizchallenge + +extension XCTestCase { + + func loadJSONFixture(_ fileName: String) -> T? { + let bundle = Bundle(for: type(of: self)) + guard let url = bundle.url(forResource: fileName, withExtension: "json") else { + XCTFail("Missing file: \(fileName).json") + return nil + } + do { + let jsonData = try Data(contentsOf: url) + let model = try JSONDecoder().decode(T.self, from: jsonData) + return model + } catch { + XCTFail("Could not decode json") + return nil + } + } +} diff --git a/quizchallenge/quizchallengeTests/Model/QuizModelTests.swift b/quizchallenge/quizchallengeTests/Model/QuizModelTests.swift new file mode 100644 index 0000000..7500f10 --- /dev/null +++ b/quizchallenge/quizchallengeTests/Model/QuizModelTests.swift @@ -0,0 +1,14 @@ +import XCTest +@testable import quizchallenge + +class QuizModelTests: XCTestCase { + + func testQuizModelDecode() { + guard let model: QuizModel = loadJSONFixture("QuizModel") else { + XCTFail() + return + } + XCTAssertEqual("What are all the java keywords?", model.question) + XCTAssertEqual(["abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", "const", "continue", "default", "do", "double", "else", "enum", "extends", "final", "finally", "float", "for", "goto", "if", "implements", "import", "instanceof", "int", "interface", "long", "native", "new", "package", "private", "protected", "public", "return", "short", "static", "strictfp", "super", "switch", "synchronized", "this", "throw", "throws", "transient", "try", "void", "volatile", "while"], model.answer) + } +} diff --git a/quizchallenge/quizchallengeTests/Network/QuizAPITests.swift b/quizchallenge/quizchallengeTests/Network/QuizAPITests.swift new file mode 100644 index 0000000..da1cee7 --- /dev/null +++ b/quizchallenge/quizchallengeTests/Network/QuizAPITests.swift @@ -0,0 +1,28 @@ +import XCTest +@testable import quizchallenge + +class QuizAPITests: XCTestCase { + + func testDefaultAPIValues() { + let environmentBaseURL = QuizAPI.quiz.environmentBaseURL + let baseURL = QuizAPI.quiz.baseURL + let path = QuizAPI.quiz.path + let httpMethod = QuizAPI.quiz.httpMethod + let task = QuizAPI.quiz.task + let headers = QuizAPI.quiz.headers + + let expectedEnvironmentBaseURL = "https://codechallenge.arctouch.com/" + let expectedBaseURL: URL? = URL(string: "https://codechallenge.arctouch.com/") + let expectedPath = "quiz/1" + let expectedHttpMethod: HTTPMethod = .get + let expectedTask: HTTPTask = .request + let expectedHeaders: HTTPHeaders? = nil + + XCTAssertEqual(environmentBaseURL, expectedEnvironmentBaseURL) + XCTAssertEqual(baseURL, expectedBaseURL) + XCTAssertEqual(path, expectedPath) + XCTAssertEqual(httpMethod, expectedHttpMethod) + XCTAssertEqual(task, expectedTask) + XCTAssertEqual(headers, expectedHeaders) + } +} diff --git a/quizchallenge/quizchallengeTests/Resources/ColorsTests.swift b/quizchallenge/quizchallengeTests/Resources/ColorsTests.swift new file mode 100644 index 0000000..3443a06 --- /dev/null +++ b/quizchallenge/quizchallengeTests/Resources/ColorsTests.swift @@ -0,0 +1,29 @@ +import XCTest +@testable import quizchallenge + +class ColorsTests: XCTestCase { + + func testGreyColor() { + let expectedColor = UIColor(red: 245.0 / 255.0, + green: 245.0 / 255.0, + blue: 245.0 / 255.0, + alpha: 1.0) + XCTAssertEqual(expectedColor, UIColor.arcGrey) + } + + func testOrangeColor() { + let expectedColor = UIColor(red: 255.0 / 255.0, + green: 131.0 / 255.0, + blue: 0.0 / 255.0, + alpha: 1.0) + XCTAssertEqual(expectedColor, UIColor.arcOrange) + } + + func testDarkOrangeColor() { + let expectedColor = UIColor(red: 200.0 / 255.0, + green: 100.0 / 255.0, + blue: 0.0 / 255.0, + alpha: 1.0) + XCTAssertEqual(expectedColor, UIColor.darkOrange) + } +} diff --git a/quizchallenge/quizchallengeTests/quizchallengeTests.swift b/quizchallenge/quizchallengeTests/quizchallengeTests.swift new file mode 100644 index 0000000..462c656 --- /dev/null +++ b/quizchallenge/quizchallengeTests/quizchallengeTests.swift @@ -0,0 +1,34 @@ +// +// quizchallengeTests.swift +// quizchallengeTests +// +// Created by alessandra.l.pereira on 13/09/19. +// Copyright © 2019 alnp. All rights reserved. +// + +import XCTest +@testable import quizchallenge + +class quizchallengeTests: XCTestCase { + + override func setUp() { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testPerformanceExample() { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/screenshots/iPhoneXR-Screen1.png b/screenshots/iPhoneXR-Screen1.png new file mode 100644 index 0000000..147b7e0 Binary files /dev/null and b/screenshots/iPhoneXR-Screen1.png differ diff --git a/screenshots/iPhoneXR-Screen2.png b/screenshots/iPhoneXR-Screen2.png new file mode 100644 index 0000000..629c8b3 Binary files /dev/null and b/screenshots/iPhoneXR-Screen2.png differ