Skip to content

Commit

Permalink
Refactor code (#39)
Browse files Browse the repository at this point in the history
* Check searchFactsViewModel to bind SuggestionsCell

* Improve Category View

* Use NSLayoutConstraint

* Make classes final

* Refactor APIError

* FactsListError ViewModel

* Fix unit tests

* ErrorView -> ErrorAlert

* Remove reference to errorView
  • Loading branch information
djorkaeffalexandre authored Nov 2, 2020
1 parent c70c9e8 commit b8b2abb
Show file tree
Hide file tree
Showing 33 changed files with 341 additions and 372 deletions.
22 changes: 13 additions & 9 deletions Chuck Norris Facts.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,9 @@

/* Begin PBXBuildFile section */
1E049E13254F5A5300226E0B /* RxSwift+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E049E12254F5A5300226E0B /* RxSwift+Extensions.swift */; };
1E0E4BA22549F7E30030BC49 /* error.json in Resources */ = {isa = PBXBuildFile; fileRef = 1E0E4BA12549F7E30030BC49 /* error.json */; };
1E135FAA254B4E66009D18AF /* LaunchArgument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E135FA9254B4E66009D18AF /* LaunchArgument.swift */; };
1E135FAD254B4E9F009D18AF /* XCUIApplication+LaunchArgument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E135FAC254B4E9F009D18AF /* XCUIApplication+LaunchArgument.swift */; };
1E135FAF254B52E0009D18AF /* facts.json in Resources */ = {isa = PBXBuildFile; fileRef = 1E135FAE254B52E0009D18AF /* facts.json */; };
1E15408F2549FA6200675DC4 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E15408E2549FA6200675DC4 /* ErrorView.swift */; };
1E23683E253FB07200BE17F3 /* FactCategoryCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E23683D253FB07200BE17F3 /* FactCategoryCell.swift */; };
1E236840253FB13A00BE17F3 /* FactCategoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E23683F253FB13A00BE17F3 /* FactCategoryViewModel.swift */; };
1E3075C2254C9D0B0082A194 /* APITarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E3075C1254C9D0B0082A194 /* APITarget.swift */; };
Expand All @@ -33,6 +31,7 @@
1E655788254CB13B00950706 /* APIResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E655787254CB13B00950706 /* APIResponse.swift */; };
1E65578C254CB20D00950706 /* API+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E65578B254CB20D00950706 /* API+Rx.swift */; };
1E65578E254CB22800950706 /* URLRequest+Encoded.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E65578D254CB22800950706 /* URLRequest+Encoded.swift */; };
1E6D568F25505D5700D27284 /* FactsListErrorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E6D568E25505D5700D27284 /* FactsListErrorViewModel.swift */; };
1E7A6528254DA2B1006E493B /* HTTPTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7A6527254DA2B1006E493B /* HTTPTask.swift */; };
1E7F15BE253324CF0006887B /* FactsListViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7F15BD253324CF0006887B /* FactsListViewModelTests.swift */; };
1E7F15C0253324FD0006887B /* FactsListViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E7F15BF253324FD0006887B /* FactsListViewControllerTests.swift */; };
Expand Down Expand Up @@ -113,11 +112,9 @@
/* Begin PBXFileReference section */
02186229E02F0A408D2EE0C2 /* Pods_Chuck_Norris_Facts.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Chuck_Norris_Facts.framework; sourceTree = BUILT_PRODUCTS_DIR; };
1E049E12254F5A5300226E0B /* RxSwift+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RxSwift+Extensions.swift"; sourceTree = "<group>"; };
1E0E4BA12549F7E30030BC49 /* error.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = error.json; sourceTree = "<group>"; };
1E135FA9254B4E66009D18AF /* LaunchArgument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchArgument.swift; sourceTree = "<group>"; };
1E135FAC254B4E9F009D18AF /* XCUIApplication+LaunchArgument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCUIApplication+LaunchArgument.swift"; sourceTree = "<group>"; };
1E135FAE254B52E0009D18AF /* facts.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = facts.json; sourceTree = "<group>"; };
1E15408E2549FA6200675DC4 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = "<group>"; };
1E23683D253FB07200BE17F3 /* FactCategoryCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactCategoryCell.swift; sourceTree = "<group>"; };
1E23683F253FB13A00BE17F3 /* FactCategoryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactCategoryViewModel.swift; sourceTree = "<group>"; };
1E3075C1254C9D0B0082A194 /* APITarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APITarget.swift; sourceTree = "<group>"; };
Expand All @@ -138,6 +135,7 @@
1E655787254CB13B00950706 /* APIResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIResponse.swift; sourceTree = "<group>"; };
1E65578B254CB20D00950706 /* API+Rx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "API+Rx.swift"; sourceTree = "<group>"; };
1E65578D254CB22800950706 /* URLRequest+Encoded.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLRequest+Encoded.swift"; sourceTree = "<group>"; };
1E6D568E25505D5700D27284 /* FactsListErrorViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsListErrorViewModel.swift; sourceTree = "<group>"; };
1E7A6527254DA2B1006E493B /* HTTPTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPTask.swift; sourceTree = "<group>"; };
1E7F15BD253324CF0006887B /* FactsListViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsListViewModelTests.swift; sourceTree = "<group>"; };
1E7F15BF253324FD0006887B /* FactsListViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactsListViewControllerTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -280,15 +278,13 @@
children = (
1E32758E2532A2C0007E838A /* EmptyListView.swift */,
1ED06C942548AAD300139151 /* LoadingView.swift */,
1E15408E2549FA6200675DC4 /* ErrorView.swift */,
);
path = Views;
sourceTree = "<group>";
};
1E3275902532A2C4007E838A /* Animations */ = {
isa = PBXGroup;
children = (
1E0E4BA12549F7E30030BC49 /* error.json */,
1EACEC98253649BD0006B36D /* loading.json */,
1E3275912532A2CD007E838A /* empty-box.json */,
);
Expand Down Expand Up @@ -343,6 +339,15 @@
path = Services;
sourceTree = "<group>";
};
1E6D568D25505D3F00D27284 /* Error */ = {
isa = PBXGroup;
children = (
1EEDC6A4254A408B00D75F3E /* FactsListError.swift */,
1E6D568E25505D5700D27284 /* FactsListErrorViewModel.swift */,
);
path = Error;
sourceTree = "<group>";
};
1E7F15BA253324760006887B /* Scenes */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -681,12 +686,12 @@
1EFE288C25321337008806B9 /* FactsList */ = {
isa = PBXGroup;
children = (
1E6D568D25505D3F00D27284 /* Error */,
1E32758D2532A2A3007E838A /* Views */,
1EFE289325321CB4008806B9 /* Fact */,
1EFE288D2532135B008806B9 /* FactsListViewController.swift */,
1EFE288F2532137C008806B9 /* FactsListCoordinator.swift */,
1EFE2891253214DB008806B9 /* FactsListViewModel.swift */,
1EEDC6A4254A408B00D75F3E /* FactsListError.swift */,
);
path = FactsList;
sourceTree = "<group>";
Expand Down Expand Up @@ -837,7 +842,6 @@
files = (
1E135FAF254B52E0009D18AF /* facts.json in Resources */,
1EFE287E25321071008806B9 /* search-facts.json in Resources */,
1E0E4BA22549F7E30030BC49 /* error.json in Resources */,
1EE0714925314AF600F6BF6D /* LaunchScreen.storyboard in Resources */,
1E5617282540FAF200BF26A0 /* get-categories.json in Resources */,
1EE0714625314AF600F6BF6D /* Assets.xcassets in Resources */,
Expand Down Expand Up @@ -1059,7 +1063,6 @@
1EE0713F25314AF500F6BF6D /* SceneDelegate.swift in Sources */,
1E8A0FF42547768500565A86 /* DynamicHeightCollectionView.swift in Sources */,
1EFE288E2532135B008806B9 /* FactsListViewController.swift in Sources */,
1E15408F2549FA6200675DC4 /* ErrorView.swift in Sources */,
1EEDC6A1254A331500D75F3E /* UICollectionView+Extensions.swift in Sources */,
1E655788254CB13B00950706 /* APIResponse.swift in Sources */,
1EF066E32545CE1D00ECF611 /* PastSearchViewModel.swift in Sources */,
Expand All @@ -1081,6 +1084,7 @@
1E135FAA254B4E66009D18AF /* LaunchArgument.swift in Sources */,
1EFE2867253206D3008806B9 /* BaseCoordinator.swift in Sources */,
1E92113B253F90BF00DB340B /* FactCategory.swift in Sources */,
1E6D568F25505D5700D27284 /* FactsListErrorViewModel.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
4 changes: 3 additions & 1 deletion Chuck Norris Facts/App/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
if LaunchArgument.check(.mockStorage) {
let entities = [
SearchEntity(searchTerm: "games"),
SearchEntity(searchTerm: "fashion")
SearchEntity(searchTerm: "fashion"),
FactCategoryEntity(category: FactCategory(text: "games")),
FactCategoryEntity(category: FactCategory(text: "fashion"))
]

let realm = try? Realm()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,15 @@
import Foundation

enum FactsListError {

// Error related to syncCategories request
case syncCategories(Error)
// Error related to searchFacts request
case searchFacts(Error)
}

extension FactsListError {

// APIError related to the error.
var error: APIError {
switch self {
case .syncCategories(let error):
Expand All @@ -22,12 +27,23 @@ enum FactsListError {
}
}

// A code to check where the error come.
var code: Int {
switch self {
case .syncCategories:
return -100
return 0
case .searchFacts:
return 1
}
}

// A message that will be shown to user.
var message: String {
switch self {
case .syncCategories:
return L10n.Errors.cantSyncCategories
case .searchFacts:
return -101
return L10n.Errors.cantSearchFacts
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// FactsListErrorViewModel.swift
// Chuck Norris Facts
//
// Created by Djorkaeff Alexandre Vilela Pereira on 11/2/20.
// Copyright © 2020 Djorkaeff Alexandre Vilela Pereira. All rights reserved.
//

import Foundation

struct FactsListErrorViewModel {

let error: APIError
let title: String
let message: String
var shouldRetry: Bool = false

init(factsListError: FactsListError) {
self.error = factsListError.error

self.title = factsListError.message
self.message = error.message

switch factsListError {
case .syncCategories:
self.shouldRetry = error.code != APIError.noConnection.code
default:
break
}
}
}
50 changes: 30 additions & 20 deletions Chuck Norris Facts/App/Scenes/Facts/FactsList/Fact/FactCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,16 @@
import UIKit
import RxSwift

class FactCell: UITableViewCell {

private let categoryView = CategoryView()
final class FactCell: UITableViewCell {

var disposeBag = DisposeBag()

private lazy var categoryView: CategoryView = {
let categoryView = CategoryView()
categoryView.translatesAutoresizingMaskIntoConstraints = false
return categoryView
}()

override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupView()
Expand Down Expand Up @@ -73,26 +77,32 @@ class FactCell: UITableViewCell {
contentView.clipsToBounds = false
contentView.addSubview(shadowView)

shadowView.addSubview(bodyLabel)
shadowView.addSubview(shareButton)
shadowView.addSubview(categoryView)

shadowView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: padding).isActive = true
shadowView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -padding).isActive = true
shadowView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: padding / 2).isActive = true
shadowView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -padding / 2).isActive = true
NSLayoutConstraint.activate([
shadowView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: padding),
shadowView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -padding),
shadowView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: padding / 2),
shadowView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -padding / 2)
])

bodyLabel.topAnchor.constraint(equalTo: shadowView.topAnchor, constant: padding).isActive = true
bodyLabel.leadingAnchor.constraint(equalTo: shadowView.leadingAnchor, constant: padding).isActive = true
bodyLabel.trailingAnchor.constraint(equalTo: shadowView.trailingAnchor, constant: -padding).isActive = true
shadowView.addSubview(bodyLabel)
NSLayoutConstraint.activate([
bodyLabel.topAnchor.constraint(equalTo: shadowView.topAnchor, constant: padding),
bodyLabel.leadingAnchor.constraint(equalTo: shadowView.leadingAnchor, constant: padding),
bodyLabel.trailingAnchor.constraint(equalTo: shadowView.trailingAnchor, constant: -padding)
])

shareButton.topAnchor.constraint(equalTo: bodyLabel.bottomAnchor, constant: padding).isActive = true
shareButton.trailingAnchor.constraint(equalTo: shadowView.trailingAnchor, constant: -padding).isActive = true
shareButton.bottomAnchor.constraint(equalTo: shadowView.bottomAnchor, constant: -padding).isActive = true
shadowView.addSubview(shareButton)
NSLayoutConstraint.activate([
shareButton.topAnchor.constraint(equalTo: bodyLabel.bottomAnchor, constant: padding),
shareButton.trailingAnchor.constraint(equalTo: shadowView.trailingAnchor, constant: -padding),
shareButton.bottomAnchor.constraint(equalTo: shadowView.bottomAnchor, constant: -padding)
])

categoryView.translatesAutoresizingMaskIntoConstraints = false
categoryView.centerYAnchor.constraint(equalTo: shareButton.centerYAnchor).isActive = true
categoryView.leftAnchor.constraint(equalTo: shadowView.leftAnchor, constant: padding).isActive = true
shadowView.addSubview(categoryView)
NSLayoutConstraint.activate([
categoryView.centerYAnchor.constraint(equalTo: shareButton.centerYAnchor),
categoryView.leftAnchor.constraint(equalTo: shadowView.leftAnchor, constant: padding)
])
}

func setup(_ fact: FactViewModel) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ final class FactsListCoordinator: BaseCoordinator<Void> {
.bind(to: factsListViewModel.inputs.setSearchTerm)
.disposed(by: disposeBag)

factsListViewModel.outputs.factsListError
.flatMap { [weak self] error in
self?.showFactsListError(error: error, in: navigationController) ?? .empty()
}
.filter { $0.shouldRetry }
.mapToVoid()
.bind(to: factsListViewModel.inputs.retryAction)
.disposed(by: disposeBag)

window.rootViewController = navigationController
window.makeKeyAndVisible()

Expand Down Expand Up @@ -65,4 +74,26 @@ final class FactsListCoordinator: BaseCoordinator<Void> {
}
}
}

private func showFactsListError(
error: FactsListErrorViewModel,
in navigationController: UINavigationController
) -> Observable<FactsListErrorViewModel> {
Observable.create { observer in
let alert = UIAlertController(title: error.title, message: error.message, preferredStyle: .alert)

let action = UIAlertAction(title: L10n.Common.ok, style: .default) { _ in
observer.onNext(error)
observer.onCompleted()
}

alert.addAction(action)

navigationController.present(alert, animated: true, completion: nil)

return Disposables.create {
alert.dismiss(animated: true, completion: nil)
}
}
}
}
Loading

0 comments on commit b8b2abb

Please sign in to comment.