Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,25 @@
# quizchallenge
# 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

712 changes: 712 additions & 0 deletions quizchallenge/quizchallenge.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
28 changes: 28 additions & 0 deletions quizchallenge/quizchallenge/AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -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
}
}

36 changes: 36 additions & 0 deletions quizchallenge/quizchallenge/Helpers/AlertHelper.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
13 changes: 13 additions & 0 deletions quizchallenge/quizchallenge/Helpers/String+Ext.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
11 changes: 11 additions & 0 deletions quizchallenge/quizchallenge/Helpers/TableView+Ext.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
32 changes: 32 additions & 0 deletions quizchallenge/quizchallenge/Helpers/TableViewDataSource.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
49 changes: 49 additions & 0 deletions quizchallenge/quizchallenge/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIAppFonts</key>
<array>
<string>SF-Pro-Display-Regular.otf</string>
<string>SF-Pro-Display-Semibold.otf</string>
<string>SF-Pro-Display-Bold.otf</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
20 changes: 20 additions & 0 deletions quizchallenge/quizchallenge/Models/QuizModel.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
9 changes: 9 additions & 0 deletions quizchallenge/quizchallenge/Network/EndpointProtocol.swift
Original file line number Diff line number Diff line change
@@ -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 }
}
13 changes: 13 additions & 0 deletions quizchallenge/quizchallenge/Network/HTTPTypes.swift
Original file line number Diff line number Diff line change
@@ -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
}
11 changes: 11 additions & 0 deletions quizchallenge/quizchallenge/Network/NetworkErrors.swift
Original file line number Diff line number Diff line change
@@ -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"
}
49 changes: 49 additions & 0 deletions quizchallenge/quizchallenge/Network/NetworkManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import Foundation

enum Result<T> {
case success(T)
case failure(Error)
}

protocol NetworkManagerType {
func getQuiz(completion: @escaping (_ result: Result<QuizModel>) -> ())
}

class NetworkManager: NetworkManagerType {
private let router = Router<QuizAPI>()

func getQuiz(completion: @escaping (_ result: Result<QuizModel>) -> ()) {
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<URLResponse> {
switch response.statusCode {
case 200...299: return .success(response)
case 501...599: return .failure(NetworkError.badRequest)
default: return .failure(NetworkError.failed)
}
}
}
42 changes: 42 additions & 0 deletions quizchallenge/quizchallenge/Network/NetworkRouter.swift
Original file line number Diff line number Diff line change
@@ -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<Endpoint: EndpointType>: 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
}
}
Loading