├── GithubRepository
├── Assets.xcassets
│ ├── Contents.json
│ ├── Octocat.imageset
│ │ ├── Octocat.png
│ │ └── Contents.json
│ ├── noresult.imageset
│ │ ├── noresult.png
│ │ └── Contents.json
│ ├── Placeholder.imageset
│ │ ├── placeholder.png
│ │ └── Contents.json
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ └── AppIcon.appiconset
│ │ └── Contents.json
├── Info.plist
├── Model
│ ├── RepositoriesQuery
│ │ └── RepositoriesQuery.swift
│ └── SearchListModel
│ │ ├── RepositoryBaseModel.swift
│ │ ├── Repository.swift
│ │ └── Owner.swift
├── Classes
│ ├── Protocol
│ │ ├── Factory
│ │ │ └── MainFactory
│ │ │ │ └── MainFactory.swift
│ │ └── CoordinatorPattern
│ │ │ └── Coordinator.swift
│ ├── Constants
│ │ └── Constants.swift
│ ├── Protocols
│ │ └── ViewModelBaseProtocol.swift
│ ├── Extension
│ │ ├── Dictionary + Extension.swift
│ │ ├── String + Extension.swift
│ │ ├── UIColor + Extension.swift
│ │ ├── AFError + Extension.swift
│ │ ├── BaseAPI + Extension.swift
│ │ ├── UIImage + Extension.swift
│ │ └── UIView + EmptyView.swift
│ ├── Helper
│ │ ├── MessageHelper.swift
│ │ └── Throttler.swift
│ ├── Networking
│ │ ├── Configuration.swift
│ │ ├── AlamofireLogger.swift
│ │ ├── Networking.swift
│ │ ├── TargerType.swift
│ │ └── BaseAPI.swift
│ ├── Routing
│ │ └── MainCoordinator.swift
│ ├── BuildConfig
│ │ └── BuildConfig.swift
│ ├── TableViewDataSource
│ │ └── TableViewDataSource.swift
│ ├── Enums
│ │ └── Enums.swift
│ └── Base
│ │ └── BaseViewController.swift
├── App
│ ├── AppConfiguration.swift
│ ├── AppDelegate.swift
│ ├── DependencyAssembler.swift
│ └── AppCoordiantor.swift
├── DataProviders
│ └── RepositoriesSearch
│ │ └── RepositorySearchDataProvider.swift
├── ViewComponents
│ ├── EmptyStateView
│ │ ├── EmptyState.swift
│ │ ├── EmptyStateView.swift
│ │ └── EmptyStateView.xib
│ └── Cells
│ │ └── MainTableViewCell.swift
├── ViewModel
│ └── MainViewModel
│ │ └── MainViewModel.swift
├── View
│ └── MainViewController
│ │ ├── MainView.swift
│ │ └── MainViewController.swift
└── Base.lproj
│ └── LaunchScreen.storyboard
├── GithubRepository.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── swiftpm
│ │ └── Package.resolved
├── xcuserdata
│ ├── mehran.xcuserdatad
│ │ ├── xcdebugger
│ │ │ └── Breakpoints_v2.xcbkptlist
│ │ └── xcschemes
│ │ │ └── xcschememanagement.plist
│ └── mehrankamalifard.xcuserdatad
│ │ ├── xcschemes
│ │ └── xcschememanagement.plist
│ │ └── xcdebugger
│ │ └── Breakpoints_v2.xcbkptlist
└── project.pbxproj
├── GithubRepositoryTests
├── Mock
│ ├── MockServices.swift
│ └── MockModels.swift
├── Fake
│ ├── FakeEndPoint.swift
│ └── FakeBuildConfig.swift
├── Model
│ └── SearchListModelTest.swift
├── BuildConfig
│ └── BuildConfigTest.swift
├── Networking
│ └── EndpointTest.swift
└── ViewModels
│ └── MainViewModelTest.swift
├── README.md
├── GithubRepositoryUITests
├── GithubRepositoryUITestsLaunchTests.swift
└── GithubRepositoryUITests.swift
└── license
/GithubRepository/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/GithubRepository/Assets.xcassets/Octocat.imageset/Octocat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mehrankmlf/Github-Repositories/HEAD/GithubRepository/Assets.xcassets/Octocat.imageset/Octocat.png
--------------------------------------------------------------------------------
/GithubRepository/Assets.xcassets/noresult.imageset/noresult.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mehrankmlf/Github-Repositories/HEAD/GithubRepository/Assets.xcassets/noresult.imageset/noresult.png
--------------------------------------------------------------------------------
/GithubRepository/Assets.xcassets/Placeholder.imageset/placeholder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mehrankmlf/Github-Repositories/HEAD/GithubRepository/Assets.xcassets/Placeholder.imageset/placeholder.png
--------------------------------------------------------------------------------
/GithubRepository/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/GithubRepository.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/GithubRepository/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/GithubRepository.xcodeproj/xcuserdata/mehran.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
--------------------------------------------------------------------------------
/GithubRepository/Model/RepositoriesQuery/RepositoriesQuery.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RepositoriesQuery.swift
3 | // GithubRepository
4 | //
5 | // Created by Mehran Kamalifard on 6/21/22.
6 | //
7 |
8 | import Foundation
9 |
10 | struct RepositoriesQuery: Equatable {
11 | var query : String
12 | }
13 |
--------------------------------------------------------------------------------
/GithubRepository.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/GithubRepository/Classes/Protocol/Factory/MainFactory/MainFactory.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MainFactory.swift
3 | // GithubRepository
4 | //
5 | // Created by Mehran on 3/26/1401 AP.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol MainFactory {
11 | func makeMainViewController(coordinator : MainCoordinator) -> MainViewController
12 | func makeMainViewModel(coordinator : MainCoordinator) -> MainViewModel
13 | }
14 |
--------------------------------------------------------------------------------
/GithubRepository/Classes/Constants/Constants.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Constants.swift
3 | // GithubRepository
4 | //
5 | // Created by Mehran on 3/26/1401 AP.
6 | //
7 |
8 | import Foundation
9 |
10 | enum NavbarTitle {
11 | case home
12 | }
13 |
14 | extension NavbarTitle {
15 |
16 | var desc : String {
17 | switch self {
18 | case .home: return "GitHub Repositories"
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/GithubRepository.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "alamofire",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/Alamofire/Alamofire",
7 | "state" : {
8 | "branch" : "master",
9 | "revision" : "51d67100121d586c9d15ec0227de96442af4dd6d"
10 | }
11 | }
12 | ],
13 | "version" : 2
14 | }
15 |
--------------------------------------------------------------------------------
/GithubRepository/Classes/Protocols/ViewModelBaseProtocol.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewModelBaseProtocol.swift
3 | // GithubRepository
4 | //
5 | // Created by Mehran on 3/25/1401 AP.
6 | //
7 |
8 | import Foundation
9 | import Combine
10 |
11 | // MARK: BaseViewModel
12 |
13 | protocol ViewModelBaseProtocol {
14 | var loadinState : CurrentValueSubject { get set }
15 | var bag : Set { get }
16 | }
17 |
18 |
--------------------------------------------------------------------------------
/GithubRepository/Assets.xcassets/Octocat.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "Octocat.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/GithubRepository/Assets.xcassets/noresult.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "noresult.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/GithubRepository/Assets.xcassets/Placeholder.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "placeholder.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/GithubRepository/Classes/Extension/Dictionary + Extension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Dictionary + Extension.swift
3 | // GithubRepository
4 | //
5 | // Created by Mehran on 3/26/1401 AP.
6 | //
7 |
8 | import Foundation
9 |
10 | extension Dictionary {
11 | var queryString: String {
12 | var output: String = ""
13 | forEach({ output += "\($0.key)=\($0.value)&" })
14 | output = String(output.dropLast())
15 | return "?" + output
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/GithubRepository.xcodeproj/xcuserdata/mehran.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | GithubRepository.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/GithubRepository.xcodeproj/xcuserdata/mehrankamalifard.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | GithubRepository.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/GithubRepositoryTests/Mock/MockServices.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MockServices.swift
3 | // GithubRepositoryTests
4 | //
5 | // Created by Mehran on 3/27/1401 AP.
6 | //
7 |
8 | import XCTest
9 | import Combine
10 |
11 | @testable import GithubRepository
12 |
13 | class MockLoginService : SeachRepositoriesProtocol {
14 | var fetchedResult : AnyPublisher !
15 | func getData(query: String, itemsCount: Int) -> AnyPublisher {
16 | return fetchedResult
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/GithubRepository/Classes/Extension/String + Extension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String + Extension.swift
3 | // GithubRepository
4 | //
5 | // Created by Mehran on 3/25/1401 AP.
6 | //
7 |
8 | import Foundation
9 |
10 | extension String {
11 |
12 | static func isNilOrEmpty(string: String?) -> Bool {
13 | guard let value = string else { return true }
14 | return value.trimmingCharacters(in: .whitespaces).isEmpty
15 | }
16 |
17 | func whiteSpacesRemoved() -> String {
18 | return self.filter { $0 != Character(" ") }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/GithubRepository/Model/SearchListModel/RepositoryBaseModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RepositoryBaseModel.swift
3 | // GithubRepository
4 | //
5 | // Created by Mehran Kamalifard on 6/19/22.
6 | //
7 |
8 | import Foundation
9 |
10 | struct RepositoryBaseModel : Decodable {
11 | var total : Int?
12 | var incomplete_results : Bool?
13 | var items : [Repository]?
14 | }
15 |
16 | extension RepositoryBaseModel {
17 | enum CodingKeys: String, CodingKey {
18 | case total = "total_count"
19 | case incomplete_results = "incomplete_results"
20 | case items = "items"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/GithubRepositoryTests/Fake/FakeEndPoint.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MockEndPoint.swift
3 | // GithubRepositoryTests
4 | //
5 | // Created by Mehran Kamalifard on 6/19/22.
6 | //
7 |
8 | import Foundation
9 | @testable import GithubRepository
10 |
11 | class FakeEndPoint : TargetType {
12 |
13 | var baseURL: String = "www.testUrl.com"
14 | var version: String = "v1"
15 | var path: RequestType = .requestPath(path: "test")
16 | var method: HTTPMethod = .post
17 | var task: Task = .requestParameters(parameters: ["test" : 1], encoding: .default)
18 | var headers: [String : String]? = ["header": "header"]
19 | }
20 |
--------------------------------------------------------------------------------
/GithubRepository/Classes/Extension/UIColor + Extension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIColor + Extension.swift
3 | // GithubRepository
4 | //
5 | // Created by Mehran on 3/26/1401 AP.
6 | //
7 |
8 | import UIKit
9 |
10 | extension UIColor {
11 | static let background = UIColor.white
12 | static let cellBackground = UIColor.white
13 | static let borderColor = UIColor.gray
14 | static let darkFontColor = UIColor.darkText
15 | static let generalBlueColor = UIColor(red: 0.0/255.0, green: 70.0/255.0, blue: 170.0/255.0, alpha: 1.0)
16 | }
17 |
18 | extension CGColor {
19 | static let borderColor = UIColor.gray.cgColor
20 | }
21 |
--------------------------------------------------------------------------------
/GithubRepositoryTests/Fake/FakeBuildConfig.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FakeBuildConfig.swift
3 | // GithubRepositoryTests
4 | //
5 | // Created by Mehran Kamalifard on 6/19/22.
6 | //
7 |
8 | import Foundation
9 | @testable import GithubRepository
10 |
11 | class FakeBuildConfig : AppConfiguration {
12 |
13 | var baseURL: String = "www.test.com"
14 | var version: String = "v0"
15 |
16 | func isVPNConnected() -> Bool {
17 | return true
18 | }
19 |
20 | func isJailBrokenDevice() -> Bool {
21 | return true
22 | }
23 |
24 | func enableCertificatePinning() -> Bool {
25 | return true
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/GithubRepository/App/AppConfiguration.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppConfiguration.swift
3 | // GithubRepository
4 | //
5 | // Created by Mehran on 3/26/1401 AP.
6 | //
7 |
8 | import Foundation
9 | import Combine
10 |
11 | //MARK: - Protocols
12 |
13 | // App Configuration Base
14 | protocol AppConfiguration {
15 | var baseURL : String { get }
16 | var version : String { get }
17 |
18 | func isVPNConnected() -> Bool
19 | func isJailBrokenDevice() -> Bool
20 | func enableCertificatePinning() -> Bool
21 | }
22 |
23 | // App Configuration Set Base
24 | protocol AppConfigurable {
25 | static var setAppState : AppConfiguration { get }
26 | }
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/GithubRepository/Classes/Helper/MessageHelper.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MessageHelper.swift
3 | // GithubRepository
4 | //
5 | // Created by Mehran on 3/26/1401 AP.
6 | //
7 |
8 | import Foundation
9 |
10 | struct MessageHelper {
11 |
12 | /// General Message Handler
13 | struct serverError {
14 | static let general : String = "Bad Request"
15 | static let noInternet : String = "Check the Connection"
16 | static let timeOut : String = "Timeout"
17 | static let notFound : String = "No Result"
18 | static let serverError : String = "Internal Server Error"
19 | }
20 |
21 | struct DeviceStatus {
22 | static let unknownDeviceID : String = "Device ID Not Found"
23 | }
24 | }
25 |
26 |
--------------------------------------------------------------------------------
/GithubRepository/DataProviders/RepositoriesSearch/RepositorySearchDataProvider.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RepositorySearchDataProvider.swift
3 | // GithubRepository
4 | //
5 | // Created by Mehran on 3/26/1401 AP.
6 | //
7 |
8 | import Foundation
9 | import Combine
10 |
11 | protocol SeachRepositoriesProtocol : AnyObject {
12 | func getData(query: String, page: Int) -> AnyPublisher
13 | }
14 |
15 | final class RepositorySearchDataProvider : BaseAPI, SeachRepositoriesProtocol {
16 | func getData(query: String, page: Int) -> AnyPublisher {
17 | self.fetchData(target: .searchRepositories(query: query, page: page), responseClass: RepositoryBaseModel.self)
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/GithubRepository/Classes/Networking/Configuration.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Configuration.swift
3 | // GithubRepository
4 | //
5 | // Created by Mehran on 3/26/1401 AP.
6 | //
7 |
8 | import Foundation
9 |
10 | enum BaseURLType {
11 |
12 | case baseApi
13 | case staging
14 |
15 | var desc : String {
16 |
17 | switch self {
18 | case .baseApi :
19 | return "https://api.github.com"
20 | case .staging :
21 | return "https://api.github.com"
22 | }
23 | }
24 | }
25 |
26 | enum VersionType {
27 | case empty
28 | case v0, v1
29 |
30 | var desc : String {
31 | switch self {
32 | case .empty :
33 | return ""
34 | case .v0 :
35 | return "/v0"
36 | case .v1 :
37 | return "/v1"
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/GithubRepository/Classes/Extension/AFError + Extension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AFError + Extension.swift
3 | // GithubRepository
4 | //
5 | // Created by Mehran on 3/25/1401 AP.
6 | //
7 |
8 | import Foundation
9 | import Alamofire
10 |
11 | extension AFError {
12 |
13 | var isTimeout: Bool {
14 | if isSessionTaskError,
15 | let error = underlyingError as NSError?,
16 | error.code == NSURLErrorTimedOut || error.code == NSURLErrorUnknown {
17 | return true
18 | }
19 | return false
20 | }
21 |
22 | var isConnectedToTheInternet: Bool {
23 | if isSessionTaskError,
24 | let error = underlyingError as NSError?,
25 | error.code == NSURLErrorNotConnectedToInternet || error.code == NSURLErrorDataNotAllowed {
26 | return true
27 | }
28 | return false
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/GithubRepository/Classes/Extension/BaseAPI + Extension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BaseAPI + Extension.swift
3 | // GithubRepository
4 | //
5 | // Created by Mehran on 3/25/1401 AP.
6 | //
7 |
8 | import Foundation
9 | import Alamofire
10 |
11 | extension BaseAPI {
12 |
13 | func buildParameters(task: Task) -> ([String:Any], ParameterEncoding) {
14 | switch task {
15 | case .requestPlain:
16 | return ([:], URLEncoding.default)
17 | case .requestParameters(parameters: let parameters, encoding: let encoding):
18 | return (parameters, encoding)
19 | }
20 | }
21 |
22 | func buildTarget(target : RequestType) -> String {
23 | switch target {
24 | case .requestPath(path: let path):
25 | return path
26 | case .queryParametrs(query: let query):
27 | return query
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/GithubRepository/Classes/Protocol/CoordinatorPattern/Coordinator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Coordinator.swift
3 | // GithubRepository
4 | //
5 | // Created by Mehran on 3/26/1401 AP.
6 | //
7 |
8 | import UIKit
9 | import Combine
10 |
11 | protocol Coordinator : AnyObject {
12 | var finishDelegate : FinishDelegate? { get set }
13 | var navigationController : UINavigationController { get set }
14 | var childCoordinators : [Coordinator] { get set }
15 | var type : CoordinatorType { get }
16 | func start()
17 | func finish()
18 | }
19 |
20 | extension Coordinator {
21 | func finish() {
22 | childCoordinators.removeAll()
23 | finishDelegate?.coordinatorDidFinish(childCoordonator: self)
24 | }
25 | }
26 |
27 | protocol FinishDelegate : AnyObject {
28 | func coordinatorDidFinish(childCoordonator : Coordinator)
29 | }
30 |
31 | enum CoordinatorType {
32 | case main
33 | }
34 |
--------------------------------------------------------------------------------
/GithubRepository/Model/SearchListModel/Repository.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SearchListModel.swift
3 | // GithubRepository
4 | //
5 | // Created by Mehran on 3/26/1401 AP.
6 | //
7 |
8 | import Foundation
9 |
10 | // MARK: - Repository
11 | struct Repository: Decodable {
12 |
13 | var id: Int?
14 | var name: String?
15 | var fullName: String?
16 | var starsCount: Int?
17 | var repoDescription: String?
18 | var openIssuesCount: Int?
19 | var language: String?
20 | var owner: Owner?
21 | }
22 |
23 | extension Repository {
24 |
25 | enum CodingKeys: String, CodingKey {
26 | case id = "id"
27 | case name = "name"
28 | case fullName = "full_name"
29 | case starsCount = "stargazers_count"
30 | case repoDescription = "description"
31 | case openIssuesCount = "open_issues_count"
32 | case language = "language"
33 | case owner = "owner"
34 | }
35 | }
36 |
37 |
38 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Github-Repositories
2 | iOS project for the implementation of MVVM-C in iOS Swift. It's an application that uses github Search api to fetch repositories details.
3 |
4 | It is created with UIKit, Alamofire, Combine, MVVM-C.
5 |
6 | ## How to run
7 |
8 | Github-Repositories requires iOS 13.0 or later. If you are developer, you can set its deployment target to lower iOS version if needed.
9 |
10 | If your XCode 12.0 is not available, you can change `deployment target` to lower iOS version.
11 |
12 | You don't need to install Anything manually, Alamofire will install Automatically when You Run the project for the first time.
13 |
14 | ## Features
15 |
16 | - throttling search without any reactive frameworks
17 | - pagination
18 |
19 | ## Requirements
20 |
21 | - iOS 13.0+
22 | - Swift 5
23 | - Swift package manager
24 |
25 | ## Libraries
26 |
27 | - Alamofire
28 |
29 | ## Author
30 |
31 | [Mehran Kamalifard](https://github.com/mehrankmlf)
32 |
--------------------------------------------------------------------------------
/GithubRepositoryUITests/GithubRepositoryUITestsLaunchTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GithubRepositoryUITestsLaunchTests.swift
3 | // GithubRepositoryUITests
4 | //
5 | // Created by Mehran on 3/25/1401 AP.
6 | //
7 |
8 | import XCTest
9 |
10 | class GithubRepositoryUITestsLaunchTests: XCTestCase {
11 |
12 | override class var runsForEachTargetApplicationUIConfiguration: Bool {
13 | true
14 | }
15 |
16 | override func setUpWithError() throws {
17 | continueAfterFailure = false
18 | }
19 |
20 | func testLaunch() throws {
21 | let app = XCUIApplication()
22 | app.launch()
23 |
24 | // Insert steps here to perform after app launch but before taking a screenshot,
25 | // such as logging into a test account or navigating somewhere in the app
26 |
27 | let attachment = XCTAttachment(screenshot: app.screenshot())
28 | attachment.name = "Launch Screen"
29 | attachment.lifetime = .keepAlways
30 | add(attachment)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/GithubRepository/App/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // GithubRepository
4 | //
5 | // Created by Mehran on 3/25/1401 AP.
6 | //
7 |
8 | import UIKit
9 |
10 | @UIApplicationMain
11 | class AppDelegate: UIResponder, UIApplicationDelegate {
12 |
13 | var window: UIWindow?
14 | var appCoordinator : AppCoordinator?
15 |
16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
17 | // Override point for customization after application launch.
18 | setRootViewController()
19 | return true
20 | }
21 | }
22 |
23 | extension AppDelegate {
24 | func setRootViewController() {
25 | let navigationController: UINavigationController = .init()
26 | self.appCoordinator = AppCoordinator.init(navigationController)
27 |
28 | self.appCoordinator?.start()
29 |
30 | window = UIWindow.init(frame: UIScreen.main.bounds)
31 | window?.rootViewController = navigationController
32 | window?.makeKeyAndVisible()
33 | }
34 | }
35 |
36 |
--------------------------------------------------------------------------------
/GithubRepositoryTests/Mock/MockModels.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MockModels.swift
3 | // GithubRepositoryTests
4 | //
5 | // Created by Mehran Kamalifard on 6/19/22.
6 | //
7 |
8 | import Foundation
9 |
10 | @testable import GithubRepository
11 |
12 | extension RepositoryBaseModel {
13 | static let mockData = RepositoryBaseModel(items: [Repository.mockData])
14 | }
15 |
16 | extension Repository {
17 | static let mockData = Repository(id: 1, name: "name", fullName: "fullName", starsCount: 1, repoDescription: "repoDescription", openIssuesCount: 1, language: "swift", owner: Owner.mockData)
18 | }
19 |
20 | extension Owner {
21 | static let mockData = Owner(id: 1, login: "login", avatarURL: "avatarURL", gravatarID: "gravatarID", url: "url", htmlURL: "htmlURL", followersURL: "followersURL", followingURL: "followingURL", gistsURL: "gistsURL", starredURL: "starredURL", subscriptionsURL: "subscriptionsURL", organizationsURL: "organizationsURL", reposURL: "reposURL", eventsURL: "eventsURL", receivedEventsURL: "receivedEventsURL")
22 | }
23 |
24 | extension RepositoriesQuery {
25 | static let mockData = RepositoriesQuery(query: "test")
26 | }
27 |
--------------------------------------------------------------------------------
/license:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Mehran Kamalifard
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/GithubRepository/Classes/Networking/AlamofireLogger.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AlamofireLogger.swift
3 | // GithubRepository
4 | //
5 | // Created by Mehran on 3/26/1401 AP.
6 | //
7 |
8 | import Alamofire
9 | import Foundation
10 |
11 | final class AlamofireLogger: EventMonitor {
12 |
13 | func requestDidResume(_ request: Request) {
14 |
15 | let allHeaders = request.request.flatMap { $0.allHTTPHeaderFields.map { $0.description } } ?? "None"
16 | let headers = """
17 | ⚡️⚡️⚡️⚡️ Request Started: \(request)
18 | ⚡️⚡️⚡️⚡️ Headers: \(allHeaders)
19 | """
20 | NSLog(headers)
21 |
22 |
23 | let body = request.request.flatMap { $0.httpBody.map { String(decoding: $0, as: UTF8.self) } } ?? "None"
24 | let message = """
25 | ⚡️⚡️⚡️⚡️ Request Started: \(request)
26 | ⚡️⚡️⚡️⚡️ Body Data: \(body)
27 | """
28 | NSLog(message)
29 | }
30 |
31 | func request(_ request: DataRequest, didParseResponse response: AFDataResponse) {
32 |
33 | NSLog("⚡️⚡️⚡️⚡️ Response Received: \(response.debugDescription)")
34 | NSLog("⚡️⚡️⚡️⚡️ Response All Headers: \(String(describing: response.response?.allHeaderFields))")
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/GithubRepository/Classes/Routing/MainCoordinator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MainCoordinator.swift
3 | // GithubRepository
4 | //
5 | // Created by Mehran on 3/26/1401 AP.
6 | //
7 |
8 | import UIKit
9 | import Combine
10 |
11 | protocol MainViewFactory {
12 | var mainFactory : MainFactory { get set }
13 | }
14 |
15 | protocol MainCoordinatorProtocol : Coordinator, MainViewFactory {
16 | func showMainViewController()
17 | }
18 |
19 | final class MainCoordinator : MainCoordinatorProtocol {
20 |
21 | var finishDelegate: FinishDelegate?
22 | var navigationController: UINavigationController
23 | var childCoordinators: [Coordinator] = []
24 | var type: CoordinatorType { .main }
25 | var mainFactory: MainFactory
26 | private var bag = Set()
27 |
28 | init(_ navigationController: UINavigationController, mainFactory : MainFactory) {
29 | self.navigationController = navigationController
30 | self.mainFactory = mainFactory
31 | }
32 |
33 | func start() {
34 | showMainViewController()
35 | }
36 |
37 | func showMainViewController() {
38 | let vc = self.mainFactory.makeMainViewController(coordinator: self)
39 | navigationController.pushViewController(vc, animated: true)
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/GithubRepository/Model/SearchListModel/Owner.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SearchListModel_Owner.swift
3 | // GithubRepository
4 | //
5 | // Created by Mehran on 3/26/1401 AP.
6 | //
7 |
8 | import Foundation
9 |
10 | // MARK: - Owner
11 | struct Owner: Decodable {
12 | var id: Int?
13 | var login : String?
14 | var avatarURL: String?
15 | var gravatarID: String?
16 | var url, htmlURL, followersURL: String?
17 | var followingURL, gistsURL, starredURL: String?
18 | var subscriptionsURL, organizationsURL, reposURL: String?
19 | var eventsURL: String?
20 | var receivedEventsURL: String?
21 |
22 | enum CodingKeys: String, CodingKey {
23 | case id = "id"
24 | case login = "login"
25 | case avatarURL = "avatar_url"
26 | case gravatarID = "gravatar_id"
27 | case url = "url"
28 | case htmlURL = "html_url"
29 | case followersURL = "followers_url"
30 | case followingURL = "following_url"
31 | case gistsURL = "gists_url"
32 | case starredURL = "starred_url"
33 | case subscriptionsURL = "subscriptions_url"
34 | case organizationsURL = "organizations_url"
35 | case reposURL = "repos_url"
36 | case eventsURL = "events_url"
37 | case receivedEventsURL = "received_events_url"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/GithubRepository/Classes/BuildConfig/BuildConfig.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BuildConfig.swift
3 | // GithubRepository
4 | //
5 | // Created by Mehran on 3/26/1401 AP.
6 | //
7 |
8 | import Foundation
9 |
10 | struct BuildConfig : AppConfigurable {
11 |
12 | static var setAppState: AppConfiguration {
13 | // return help you to change thec AppConfigState
14 | return Realese()
15 | }
16 | }
17 |
18 | private struct Realese : AppConfiguration {
19 |
20 | var baseURL : String = BaseURLType.baseApi.desc
21 |
22 | var version : String = VersionType.empty.desc
23 |
24 | func isVPNConnected() -> Bool {
25 | return false
26 | }
27 |
28 | func isJailBrokenDevice() -> Bool {
29 | return false
30 | }
31 |
32 | func enableCertificatePinning() -> Bool {
33 | return false
34 | }
35 | }
36 |
37 | private struct PenTest : AppConfiguration {
38 |
39 | var baseURL : String = BaseURLType.baseApi.desc
40 |
41 | var version : String = VersionType.v1.desc
42 |
43 | func isVPNConnected() -> Bool {
44 | return false
45 | }
46 |
47 | func isJailBrokenDevice() -> Bool {
48 | return false
49 | }
50 |
51 | func enableCertificatePinning() -> Bool {
52 | return false
53 | }
54 | }
55 |
56 |
--------------------------------------------------------------------------------
/GithubRepository/ViewComponents/EmptyStateView/EmptyState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EmptyState.swift
3 | // GithubRepository
4 | //
5 | // Created by Mehran on 3/26/1401 AP.
6 | //
7 |
8 | import UIKit
9 | import Combine
10 |
11 | protocol EmptyStateDelegate : AnyObject {
12 | func emptyStateButtonClicked()
13 | }
14 |
15 | class EmptyState {
16 |
17 | var delegate : EmptyStateDelegate?
18 | private var emptyStateView : EmptyStateView!
19 | var bag = Set()
20 |
21 | fileprivate var hidden = true {
22 | didSet {
23 | self.emptyStateView?.isHidden = hidden
24 | }
25 | }
26 | init(inView view : UIView?) {
27 | emptyStateView = EmptyStateView.getView()
28 | emptyStateView?.isHidden = true
29 | emptyStateView?.fixConstraintsInView(view)
30 | emptyStateView.buttonPressSubject.sink { [weak self] value in
31 | self?.delegate?.emptyStateButtonClicked()
32 | }.store(in: &bag)
33 | }
34 | }
35 |
36 | extension EmptyState {
37 | func show(title : String, errorType : EmptyStateErrorType?, isShow : Bool) {
38 | self.emptyStateView.viewModel = EmptyStateView.ViewModel(title: title, description: nil)
39 | hidden = !isShow
40 | }
41 |
42 | func hide() {
43 | hidden = true
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/GithubRepositoryTests/Model/SearchListModelTest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SearchListModelTest.swift
3 | // GithubRepositoryTests
4 | //
5 | // Created by Mehran on 3/27/1401 AP.
6 | //
7 |
8 | import XCTest
9 | @testable import GithubRepository
10 |
11 | class SearchListModelTest: XCTestCase {
12 |
13 | var sut : Repository!
14 |
15 | override func setUp() {
16 | super.setUp()
17 | sut = Repository(id: 1, name: "test", fullName: "test", starsCount: 2, repoDescription: "testtest", openIssuesCount: 3, language: "swift", owner: nil)
18 | }
19 |
20 | override func tearDown() {
21 | sut = nil
22 | super.tearDown()
23 | }
24 |
25 | func testSearchListModel_WhenDataProvided_ShouldBeCodable() {
26 | XCTAssertTrue(sut as Any is Decodable)
27 | }
28 |
29 | func testSearchListModel_WhenDataProvided_ShouldBeString() {
30 | XCTAssertTrue(sut?.name == "test")
31 | XCTAssertTrue(sut?.fullName == "test")
32 | XCTAssertTrue(sut?.repoDescription == "testtest")
33 | XCTAssertTrue(sut?.language == "swift")
34 | }
35 |
36 | func testSearchListModel_WhenDataProvided_ShouldBeInt() {
37 | XCTAssertTrue(sut?.id == 1)
38 | XCTAssertTrue(sut?.starsCount == 2)
39 | XCTAssertTrue(sut?.openIssuesCount == 3)
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/GithubRepository/App/DependencyAssembler.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DependencyAssembler.swift
3 | // GithubRepository
4 | //
5 | // Created by Mehran on 3/26/1401 AP.
6 | //
7 |
8 | import Foundation
9 |
10 |
11 | fileprivate let sharedDependencyAssembler : DependencyAssembler = DependencyAssembler()
12 |
13 | protocol DependencyAssemblerInjector {
14 | var dependencyAssembler : DependencyAssembler { get }
15 | }
16 |
17 | extension DependencyAssemblerInjector {
18 | var dependencyAssembler : DependencyAssembler{
19 | return sharedDependencyAssembler
20 | }
21 | }
22 |
23 | final class DependencyAssembler {
24 | init() {}
25 | }
26 |
27 | extension DependencyAssembler : MainFactory {
28 | func makeMainViewController(coordinator: MainCoordinator) -> MainViewController {
29 | let vc = MainViewController(viewModel: self.makeMainViewModel(coordinator: coordinator),
30 | contentView: MainView(),
31 | throttler: Throttler(interval: 2.0))
32 | vc.viewModel = self.makeMainViewModel(coordinator: coordinator)
33 | return vc
34 | }
35 |
36 | func makeMainViewModel(coordinator: MainCoordinator) -> MainViewModel {
37 | let viewModel = MainViewModel(seachRepositories: RepositorySearchDataProvider())
38 | return viewModel
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/GithubRepository/Classes/Networking/Networking.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Networking.swift
3 | // GithubRepository
4 | //
5 | // Created by Mehran on 3/25/1401 AP.
6 | //
7 |
8 | import Foundation
9 | import Alamofire
10 |
11 | enum Networking {
12 | case searchRepositories(query: String, page: Int, per_page: Int = 5)
13 | }
14 |
15 | extension Networking : TargetType {
16 |
17 | var baseURL: String {
18 | return BuildConfig.setAppState.baseURL
19 | }
20 |
21 | var version: String {
22 | return BuildConfig.setAppState.version
23 | }
24 |
25 | var path: RequestType {
26 | switch self {
27 | case .searchRepositories(let query, let page, let per_page):
28 | return .queryParametrs(query: "\("/search/repositories")\(["q":query, "page":page, "per_page":per_page].queryString)")
29 | }
30 | }
31 |
32 | var method: HTTPMethod {
33 | switch self {
34 | case .searchRepositories:
35 | return .get
36 | }
37 | }
38 |
39 | var task: Task {
40 | switch self {
41 | case .searchRepositories:
42 | return .requestPlain
43 | }
44 | }
45 |
46 | var headers: [String : String]? {
47 | switch self {
48 | default :
49 | return ["Content-Type":"application/json"]
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/GithubRepository/Classes/Extension/UIImage + Extension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIImage + Extension.swift
3 | // GithubRepository
4 | //
5 | // Created by Mehran on 3/26/1401 AP.
6 | //
7 |
8 | import UIKit
9 |
10 | extension UIImageView {
11 |
12 | func imageFromServerURL(_ URLString: String, placeHolder: UIImage?) {
13 |
14 | self.image = nil
15 | //If imageurl's imagename has space then this line going to work for this
16 | let imageServerUrl = URLString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? ""
17 |
18 | if let url = URL(string: imageServerUrl) {
19 | URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in
20 | if error != nil {
21 | print("ERROR LOADING IMAGES FROM URL: \(String(describing: error))")
22 | DispatchQueue.main.async {
23 | self.image = placeHolder
24 | }
25 | return
26 | }
27 | DispatchQueue.main.async {
28 | if let data = data {
29 | if let downloadedImage = UIImage(data: data) {
30 |
31 | self.image = downloadedImage
32 | }
33 | }
34 | }
35 | }).resume()
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/GithubRepository/Classes/Networking/TargerType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TargerType.swift
3 | // GithubRepository
4 | //
5 | // Created by Mehran on 3/25/1401 AP.
6 | //
7 |
8 | import Foundation
9 | import Alamofire
10 |
11 |
12 | enum HTTPMethod: String {
13 | case get = "GET"
14 | case post = "POST"
15 | case put = "PUT"
16 | case delete = "DELETE"
17 | }
18 |
19 | enum RequestType : Equatable {
20 | /// A request with no additional data.
21 | case requestPath(path : String)
22 | /// A request with query param
23 | case queryParametrs(query : String)
24 | }
25 |
26 | enum Task {
27 |
28 | /// A request with no additional data.
29 | case requestPlain
30 |
31 | /// A requests body set with encoded parameters.
32 | case requestParameters(parameters: [String: Any], encoding: URLEncoding)
33 | }
34 |
35 | protocol TargetType {
36 |
37 | /// The target's base `URL`.
38 | var baseURL: String { get }
39 |
40 | /// The Version of EndPoints
41 | var version : String { get }
42 |
43 | /// The path to be appended to `baseURL` to form the full `URL`.
44 | var path: RequestType { get }
45 |
46 | /// The HTTP method used in the request.
47 | var method: HTTPMethod { get }
48 |
49 | /// The type of HTTP task to be performed.
50 | var task: Task { get }
51 |
52 | /// The headers to be used in the request.
53 | var headers: [String: String]? { get }
54 | }
55 |
--------------------------------------------------------------------------------
/GithubRepository/App/AppCoordiantor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppCoordiantor.swift
3 | // GithubRepository
4 | //
5 | // Created by Mehran on 3/26/1401 AP.
6 | //
7 |
8 | import UIKit
9 |
10 | protocol AppCoordinatorProtocol : Coordinator {
11 | func showMainFlow()
12 | }
13 |
14 | class AppCoordinator : AppCoordinatorProtocol, DependencyAssemblerInjector {
15 |
16 | weak var finishDelegate : FinishDelegate? = nil
17 | var navigationController: UINavigationController
18 | var childCoordinators = [Coordinator]()
19 | var type : CoordinatorType { .main }
20 |
21 | required init(_ navigationController: UINavigationController) {
22 | self.navigationController = navigationController
23 | }
24 |
25 | func start() {
26 | self.showMainFlow()
27 | }
28 |
29 | func showMainFlow() {
30 | let main = MainCoordinator.init(navigationController, mainFactory: self.dependencyAssembler)
31 | main.finishDelegate = self
32 | main.start()
33 | childCoordinators.append(main)
34 | }
35 | }
36 |
37 | extension AppCoordinator : FinishDelegate {
38 |
39 | func coordinatorDidFinish(childCoordonator: Coordinator) {
40 |
41 | childCoordinators = childCoordinators.filter({ $0.type != childCoordonator.type })
42 |
43 | switch childCoordonator.type {
44 | case .main:
45 | navigationController.viewControllers.removeAll()
46 | // show next flow whenever mainflow finished
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/GithubRepositoryTests/BuildConfig/BuildConfigTest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BuildConfigTest.swift
3 | // GithubRepositoryTests
4 | //
5 | // Created by Mehran Kamalifard on 6/19/22.
6 | //
7 |
8 | import XCTest
9 | @testable import GithubRepository
10 |
11 | class BuildConfigTest: XCTestCase {
12 |
13 | var sut : AppConfiguration!
14 |
15 | override func setUp() {
16 | super.setUp()
17 | sut = FakeBuildConfig()
18 | }
19 |
20 | override func tearDown() {
21 | sut = nil
22 | super.tearDown()
23 | }
24 |
25 | func testBuildConfig_WhenBaseURL_ShouldReturnString() {
26 | XCTAssertTrue(sut.baseURL as Any is String)
27 | XCTAssertTrue(sut.baseURL == "www.test.com")
28 | }
29 |
30 | func testBuildConfig_WhenVersion_ShouldReturnString() {
31 | XCTAssertTrue(sut.version as Any is String)
32 | XCTAssertTrue(sut.version == "v0")
33 | }
34 |
35 | func testBuildConfig_WhenIsVPNConnected_ShouldReturnBool() {
36 | XCTAssertTrue(sut.isVPNConnected() as Any is Bool)
37 | XCTAssertTrue(sut.isVPNConnected() == true)
38 | }
39 |
40 | func testBuildConfig_WhenisJailBrokenDevice_ShouldReturnBool() {
41 | XCTAssertTrue(sut.isJailBrokenDevice() as Any is Bool)
42 | XCTAssertTrue(sut.isVPNConnected() == true)
43 | }
44 |
45 | func testBuildConfig_WhenisEnableCertificatePinningDevice_ShouldReturnBool() {
46 | XCTAssertTrue(sut.enableCertificatePinning() as Any is Bool)
47 | XCTAssertTrue(sut.isVPNConnected() == true)
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/GithubRepositoryUITests/GithubRepositoryUITests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GithubRepositoryUITests.swift
3 | // GithubRepositoryUITests
4 | //
5 | // Created by Mehran on 3/25/1401 AP.
6 | //
7 |
8 | import XCTest
9 |
10 | class GithubRepositoryUITests: XCTestCase {
11 |
12 | override func setUpWithError() throws {
13 | // Put setup code here. This method is called before the invocation of each test method in the class.
14 |
15 | // In UI tests it is usually best to stop immediately when a failure occurs.
16 | continueAfterFailure = false
17 |
18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
19 | }
20 |
21 | override func tearDownWithError() throws {
22 | // Put teardown code here. This method is called after the invocation of each test method in the class.
23 | }
24 |
25 | func testExample() throws {
26 | // UI tests must launch the application that they test.
27 | let app = XCUIApplication()
28 | app.launch()
29 |
30 | // Use XCTAssert and related functions to verify your tests produce the correct results.
31 | }
32 |
33 | func testLaunchPerformance() throws {
34 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
35 | // This measures how long it takes to launch your application.
36 | measure(metrics: [XCTApplicationLaunchMetric()]) {
37 | XCUIApplication().launch()
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/GithubRepositoryTests/Networking/EndpointTest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EndpointTest.swift
3 | // GithubRepositoryTests
4 | //
5 | // Created by Mehran Kamalifard on 6/19/22.
6 | //
7 |
8 | import XCTest
9 | @testable import GithubRepository
10 |
11 | class EndpointTest: XCTestCase {
12 |
13 | var endPoint : TargetType!
14 |
15 | override func setUp() {
16 | super.setUp()
17 | endPoint = FakeEndPoint()
18 | }
19 |
20 | override func tearDown() {
21 | endPoint = nil
22 | super.tearDown()
23 | }
24 |
25 | func testTargetType_WhenBaseURl_ShouldReturnString() {
26 | XCTAssertTrue(endPoint.baseURL as Any is String)
27 | }
28 |
29 | func testTargetType_WhenBaseURl_ShouldReturnURL() {
30 | XCTAssertEqual(endPoint.baseURL, "www.testUrl.com")
31 | }
32 |
33 | func testTargetType_WhenVersion_ShouldReturnString() {
34 | XCTAssertTrue(endPoint.version as Any is String)
35 | }
36 |
37 | func testTargetType_WhenVersion_ShouldReturnVersion() {
38 | XCTAssertEqual(endPoint.version, "v1")
39 | }
40 |
41 | func testTargetType_WhenPath_ShouldBeRequestType() {
42 | XCTAssertTrue(endPoint.path as Any is RequestType)
43 | }
44 |
45 | func testTargetType_WhenPath_ShouldReturnPath() {
46 | let endPoint = endPoint
47 | XCTAssertEqual(endPoint?.path, .requestPath(path: "test"))
48 | }
49 |
50 | func testTargetType_WhenMethod_ShouldReturnPost() {
51 | let endPoint = endPoint
52 | XCTAssertEqual(endPoint?.method, .post)
53 | }
54 |
55 | func testTargetType_WhenHeader_ShouldReturnDict() {
56 | XCTAssertEqual(endPoint.headers, ["header": "header"])
57 | }
58 |
59 | }
60 |
--------------------------------------------------------------------------------
/GithubRepository/Classes/TableViewDataSource/TableViewDataSource.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TableViewDataSource.swift
3 | // GithubRepository
4 | //
5 | // Created by Mehran on 3/26/1401 AP.
6 | //
7 |
8 | import UIKit
9 | import Combine
10 |
11 | class TableViewCustomDataSource: NSObject,UITableViewDataSource {
12 |
13 | typealias CellConfigurator = (Model,UITableViewCell)-> Void
14 |
15 | var models:[Model]?
16 | @Published var loadNextPage = false
17 | private let reuseIdentifier:String
18 | private let cellConfigurator: CellConfigurator
19 |
20 | init(models:[Model],reuseIdentifier:String,cellConfigurator:@escaping CellConfigurator) {
21 |
22 | self.models = models
23 | self.reuseIdentifier = reuseIdentifier
24 | self.cellConfigurator = cellConfigurator
25 | }
26 |
27 | func numberOfSections(in tableView: UITableView) -> Int {
28 | return 1
29 | }
30 |
31 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
32 | guard let count = models?.count else {return 0}
33 | return count
34 | }
35 |
36 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
37 | guard let data = self.models else {return UITableViewCell()}
38 | let model = data[indexPath.row]
39 | let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath)
40 | cellConfigurator(model,cell)
41 | return cell
42 | }
43 | }
44 |
45 | extension TableViewCustomDataSource where Model == Repository {
46 | static func displayData(for itemLists:[Repository],withCellidentifier reuseIdentifier: String)-> TableViewCustomDataSource {
47 | return TableViewCustomDataSource(models: itemLists, reuseIdentifier: reuseIdentifier, cellConfigurator: { (data, cell ) in
48 | let itemcell:MainTableViewCell = cell as! MainTableViewCell
49 | itemcell.setupParameters(data: data)
50 | })
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/GithubRepository/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "scale" : "1x",
46 | "size" : "20x20"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "scale" : "2x",
51 | "size" : "20x20"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "scale" : "1x",
56 | "size" : "29x29"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "29x29"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "scale" : "1x",
66 | "size" : "40x40"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "scale" : "2x",
71 | "size" : "40x40"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "scale" : "1x",
76 | "size" : "76x76"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "scale" : "2x",
81 | "size" : "76x76"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "scale" : "2x",
86 | "size" : "83.5x83.5"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "scale" : "1x",
91 | "size" : "1024x1024"
92 | }
93 | ],
94 | "info" : {
95 | "author" : "xcode",
96 | "version" : 1
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/GithubRepository/Classes/Helper/Throttler.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Debouncer.swift
3 | // GithubRepository
4 | //
5 | // Created by Mehran on 3/27/1401 AP.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol ThrottlerProtocol {
11 | var value: String? { get }
12 | var valueTimestamp: Date? { get }
13 | var interval: TimeInterval { get set }
14 | var queue: DispatchQueue { get }
15 | var callbacks: [(String) -> ()] { get }
16 | var nilCallback: (() -> (Void))? { get }
17 | init(interval : TimeInterval, on queue: DispatchQueue)
18 | }
19 |
20 | protocol ThrottleBehavierProtocol : ThrottlerProtocol {
21 | func receive(_ value: String)
22 | func on(throttled: (@escaping (String) -> ()))
23 | func emptyValue(closure: @escaping () -> Void)
24 | func onDispatch()
25 | func sendValue()
26 | }
27 |
28 | public final class Throttler : ThrottleBehavierProtocol {
29 | var value: String?
30 | var valueTimestamp: Date? = nil
31 | var interval: TimeInterval
32 | var queue: DispatchQueue
33 | var callbacks: [(String) -> ()] = []
34 | var nilCallback: (() -> (Void))?
35 | init(interval: TimeInterval, on queue: DispatchQueue = .main) {
36 | self.interval = interval
37 | self.queue = queue
38 | }
39 | public func receive(_ value: String) {
40 | self.value = value
41 | guard valueTimestamp == nil else { return }
42 | self.valueTimestamp = Date()
43 | queue.asyncAfter(deadline: .now() + interval) { [weak self] in
44 | self?.onDispatch()
45 | }
46 | }
47 |
48 | public func on(throttled: (@escaping (String) -> ())) {
49 | self.callbacks.append(throttled)
50 | }
51 |
52 | public func emptyValue(closure: @escaping () -> Void) {
53 | self.nilCallback = closure
54 | }
55 |
56 | func onDispatch() {
57 | self.valueTimestamp = nil
58 | if String.isNilOrEmpty(string: value) {
59 | self.nilCallback?()
60 | }else{
61 | sendValue()
62 | }
63 | }
64 |
65 | func sendValue() {
66 | if let value = self.value {
67 | callbacks.forEach { $0(value) }
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/GithubRepositoryTests/ViewModels/MainViewModelTest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MainViewModelTest.swift
3 | // GithubRepositoryTests
4 | //
5 | // Created by Mehran on 3/27/1401 AP.
6 | //
7 |
8 | import XCTest
9 | import Combine
10 |
11 | @testable import GithubRepository
12 |
13 | class MainViewModelTest: XCTestCase {
14 |
15 | var mocksearchService : MockLoginService!
16 | var viewModelToTest : MainViewModel!
17 | private var bag : Set = []
18 |
19 | override func setUp() {
20 | super.setUp()
21 | mocksearchService = MockLoginService()
22 | viewModelToTest = MainViewModel(seachRepositories: mocksearchService)
23 | }
24 |
25 | override func tearDown() {
26 | mocksearchService = nil
27 | viewModelToTest = nil
28 | super.tearDown()
29 | }
30 |
31 | func testRepositorySearchService_WhenServieCalled_ShouldReturnResponse() {
32 |
33 | let data = RepositoryBaseModel.mockData
34 |
35 | let expectation = XCTestExpectation(description: "State is set to Token")
36 |
37 | viewModelToTest.loadinState.dropFirst().sink { event in
38 | XCTAssertEqual(event, .loadStart)
39 | expectation.fulfill()
40 | }.store(in: &bag)
41 |
42 | mocksearchService.fetchedResult = Result.success(data).publisher.eraseToAnyPublisher()
43 | viewModelToTest.callSearchService(query: RepositoriesQuery.mockData, page: 0)
44 |
45 | wait(for: [expectation], timeout: 1)
46 | }
47 |
48 | func testRepositorySearchService_WhenServieCalled_ShouldReturnNil() {
49 |
50 | let data = RepositoryBaseModel()
51 |
52 | let expectation = XCTestExpectation(description: "State is set to Token")
53 |
54 | viewModelToTest.loadinState.dropFirst().sink { event in
55 | XCTAssertEqual(event, .loadStart)
56 | expectation.fulfill()
57 | }.store(in: &bag)
58 |
59 | mocksearchService.fetchedResult = Result.success(data).publisher.eraseToAnyPublisher()
60 | viewModelToTest.callSearchService(query: RepositoriesQuery.mockData, page: 0)
61 |
62 | wait(for: [expectation], timeout: 1)
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/GithubRepository/Classes/Networking/BaseAPI.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BaseAPI.swift
3 | // GithubRepository
4 | //
5 | // Created by Mehran on 3/25/1401 AP.
6 | //
7 |
8 | import Foundation
9 | import Alamofire
10 | import Combine
11 |
12 | class BaseAPI {
13 |
14 | typealias AnyPublisherResult = AnyPublisher
15 | typealias FutureResult = Future
16 |
17 | let session = Session(eventMonitors: [AlamofireLogger()])
18 |
19 | /// ```
20 | /// Generic Base Class + Combine Concept + Future Promise
21 | ///
22 | /// ```
23 | ///
24 | /// - Returns: `etc promise(.failure(.timeout)) || promise(.success(value))`.
25 | ///
26 |
27 | func fetchData(target: T, responseClass: M.Type) -> AnyPublisherResult {
28 |
29 | let method = Alamofire.HTTPMethod(rawValue: target.method.rawValue)
30 | let headers = Alamofire.HTTPHeaders(target.headers ?? [:])
31 | let params = buildParameters(task: target.task)
32 | let targetPath = buildTarget(target: target.path)
33 | let url = (target.baseURL + target.version + targetPath)
34 |
35 | return FutureResult { [weak self] promise in
36 |
37 | self?.session.request(url, method: method, parameters: params.0, encoding: params.1, headers: headers, requestModifier: { $0.timeoutInterval = 20 })
38 | .validate(statusCode: 200..<300)
39 | .responseDecodable(of: M.self) { response in
40 |
41 | switch response.result {
42 |
43 | case .success(let value):
44 |
45 | promise(.success(value))
46 |
47 | case .failure(let error):
48 | guard !error.isTimeout else {return promise(.failure(.timeout)) }
49 | guard !error.isConnectedToTheInternet else { return promise(.failure(.noNetwork)) }
50 | return promise(.failure(.general))
51 | }
52 | }
53 | }
54 | .eraseToAnyPublisher()
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/GithubRepository/ViewComponents/EmptyStateView/EmptyStateView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EmptyStateView.swift
3 | // GithubRepository
4 | //
5 | // Created by Mehran on 3/26/1401 AP.
6 | //
7 |
8 | import UIKit
9 | import Combine
10 |
11 | enum EmptyStateErrorType {
12 | case serverError
13 | case noConnection
14 | case permisionError(type : PermisionType)
15 | }
16 |
17 | class EmptyStateView: UIView {
18 |
19 | struct ViewModel {
20 | var title : String?
21 | var description : String?
22 | }
23 |
24 | public var buttonPressSubject = PassthroughSubject()
25 |
26 | var errorType : EmptyStateErrorType = .serverError {
27 | didSet {
28 | self.setStyle()
29 | }
30 | }
31 |
32 | var viewModel : ViewModel? {
33 | didSet {
34 | guard let viewModel = viewModel else {return}
35 | self.lblDesc.isHidden = false
36 | self.lblDesc.text = viewModel.title
37 | }
38 | }
39 |
40 | @IBOutlet weak var imageMain: UIImageView!
41 | @IBOutlet weak var lblDesc: UILabel!
42 | @IBOutlet weak var btnFirstAction: UIButton!
43 | @IBOutlet weak var btnSecondAction: UIButton!
44 |
45 | override func awakeFromNib() {
46 | super.awakeFromNib()
47 | self.btnSecondAction.isHidden = true
48 | }
49 |
50 | static func getView() -> EmptyStateView {
51 | return Bundle.main.loadNibNamed("EmptyStateView", owner: self, options: nil)?.first as! EmptyStateView
52 | }
53 |
54 | @IBAction func btnFirstAction_Clicked(_ sender: Any) {
55 | self.buttonPressSubject.send()
56 | }
57 |
58 | @IBAction func btnSecondAction_Clicked(_ sender: Any) {
59 | //do SomeThing
60 | }
61 |
62 | private func setStyle() {
63 | switch self.errorType {
64 | case .serverError:
65 | self.btnFirstAction.setTitle("try again!", for: .normal)
66 | self.animate()
67 | case .permisionError(let type):
68 | self.lblDesc.text = type.desc
69 | self.btnFirstAction.setTitle("", for: .normal)
70 | case .noConnection:
71 | self.btnFirstAction.setTitle("try again!", for: .normal)
72 | }
73 | }
74 |
75 | private func animate() {
76 | self.lblDesc.transform = CGAffineTransform(scaleX: 0.2, y: 2)
77 | UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.3, initialSpringVelocity: 0, options: [.allowUserInteraction, .curveLinear], animations: {
78 | self.lblDesc.transform = .identity
79 | })
80 | self.alpha = 1
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/GithubRepository/Classes/Enums/Enums.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Enums.swift
3 | // GithubRepository
4 | //
5 | // Created by Mehran on 3/26/1401 AP.
6 | //
7 |
8 | import Foundation
9 |
10 | enum ViewModelStatus : Equatable {
11 | case loadStart
12 | case dismissAlert
13 | case emptyStateHandler(title : String, isShow : Bool)
14 | }
15 |
16 | enum APIError : Error {
17 | case general
18 | case timeout
19 | case pageNotFound
20 | case noData
21 | case noNetwork
22 | case unknownError
23 | case serverError
24 | case statusMessage(message : String)
25 | case decodeError(String)
26 | }
27 |
28 | extension APIError {
29 | ///Description of error
30 | var desc: String {
31 |
32 | switch self {
33 | case .general: return MessageHelper.serverError.general
34 | case .timeout: return MessageHelper.serverError.timeOut
35 | case .pageNotFound: return MessageHelper.serverError.notFound
36 | case .noData: return MessageHelper.serverError.notFound
37 | case .noNetwork: return MessageHelper.serverError.noInternet
38 | case .unknownError: return MessageHelper.serverError.general
39 | case .serverError: return MessageHelper.serverError.serverError
40 | case .statusMessage(let message): return message
41 | case .decodeError(let error): return error
42 | }
43 | }
44 | }
45 |
46 | enum StatusCodeType : Int , Codable {
47 | case success = 0
48 | case requestIsNotPermitted = 31
49 | case failed = 42
50 | case NotFound = 1
51 | case ServerError = 2
52 | case InvalidToken = 3
53 | case TokenExpired = 4
54 | case Disabled = 23
55 | case ValueIsNull = 19
56 | }
57 |
58 | enum PermisionType : Int {
59 | case camera = 1
60 | case contact = 2
61 | case photoLibrary = 3
62 | case location = 4
63 | case photoCamera = 5
64 |
65 | var desc: String {
66 | switch self {
67 |
68 | case .camera:
69 | return "You do not have permission to access the Camera, you can access the application through the device settings, Privacy menu"
70 | case .contact:
71 | return "You have not allowed access to Contact, you can access the application through the device settings, under the Privacy menu"
72 | case .photoLibrary:
73 | return "You are not allowed to access Photos, you can access the app through the device settings under the Privacy menu"
74 | case .location:
75 | return "You are not allowed to access Location, you can access the application through the device settings, under the Privacy menu"
76 | case .photoCamera:
77 | return "You are not allowed to access Photo and Camera, you can access the application through the device settings, under the Privacy menu"
78 | }
79 | }
80 | }
81 |
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/GithubRepository/Classes/Extension/UIView + EmptyView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIView + EmptyView.swift
3 | // GithubRepository
4 | //
5 | // Created by Mehran on 3/26/1401 AP.
6 | //
7 |
8 | import Foundation
9 | import UIKit
10 |
11 | enum AssociationPolicy: UInt {
12 | // raw values map to objc_AssociationPolicy's raw values
13 | case assign = 0
14 | case copy = 771
15 | case copyNonatomic = 3
16 | case retain = 769
17 | case retainNonatomic = 1
18 |
19 | var objc: objc_AssociationPolicy {
20 | return objc_AssociationPolicy(rawValue: rawValue)!
21 | }
22 | }
23 |
24 | protocol AssociatedObjects: AnyObject { }
25 |
26 | // transparent wrappers
27 | extension AssociatedObjects {
28 |
29 | /// wrapper around `objc_getAssociatedObject`
30 | func ao_get(pkey: UnsafeRawPointer) -> Any? {
31 | return objc_getAssociatedObject(self, pkey)
32 | }
33 |
34 | /// wrapper around `objc_setAssociatedObject`
35 | func ao_setOptional(_ value: Any?, pkey: UnsafeRawPointer, policy: AssociationPolicy = .retainNonatomic) {
36 | guard let value = value else { return }
37 | objc_setAssociatedObject(self, pkey, value, policy.objc)
38 | }
39 |
40 | /// wrapper around `objc_setAssociatedObject`
41 | func ao_set(_ value: Any, pkey: UnsafeRawPointer, policy: AssociationPolicy = .retainNonatomic) {
42 | objc_setAssociatedObject(self, pkey, value, policy.objc)
43 | }
44 |
45 | /// wrapper around 'objc_removeAssociatedObjects'
46 | func ao_removeAll() {
47 | objc_removeAssociatedObjects(self)
48 | }
49 | }
50 |
51 | extension NSObject: AssociatedObjects { }
52 |
53 |
54 | enum ViewAssociatedKeys {
55 | static var emptyState = "emptyState"
56 | }
57 |
58 | extension UIView {
59 |
60 | var emptyState: EmptyState! {
61 | get {
62 | guard let saved = ao_get(pkey: &ViewAssociatedKeys.emptyState) as? EmptyState else {
63 | self.emptyState = EmptyState(inView: self)
64 | return self.emptyState
65 | }
66 | return saved
67 | }
68 | set { ao_set(newValue ?? EmptyState(inView: self), pkey: &ViewAssociatedKeys.emptyState) }
69 | }
70 | }
71 |
72 | extension UIView {
73 | func fixConstraintsInView(_ container: UIView!) -> Void {
74 | self.translatesAutoresizingMaskIntoConstraints = false
75 | self.frame = container.frame
76 | container.addSubview(self)
77 |
78 | NSLayoutConstraint(item: self, attribute: .leading, relatedBy: .equal, toItem: container, attribute: .leading, multiplier: 1.0, constant: 0).isActive = true
79 | NSLayoutConstraint(item: self, attribute: .trailing, relatedBy: .equal, toItem: container, attribute: .trailing, multiplier: 1.0, constant: 0).isActive = true
80 | NSLayoutConstraint(item: self, attribute: .top, relatedBy: .equal, toItem: container, attribute: .top, multiplier: 1.0, constant: 0).isActive = true
81 | NSLayoutConstraint(item: self, attribute: .bottom, relatedBy: .equal, toItem: container, attribute: .bottom, multiplier: 1.0, constant: 0).isActive = true
82 | }
83 | }
84 |
85 |
--------------------------------------------------------------------------------
/GithubRepository/Classes/Base/BaseViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BaseViewController.swift
3 | // GithubRepository
4 | //
5 | // Created by Mehran on 3/25/1401 AP.
6 | //
7 |
8 | import UIKit
9 | import Combine
10 |
11 | protocol ShowEmptyStateProtocol : AnyObject {
12 | func showEmptyStateView(title: String?, errorType: EmptyStateErrorType, isShow : Bool)
13 | }
14 |
15 | class BaseViewController: UIViewController {
16 |
17 | var bag = Set()
18 | var delegate : ShowEmptyStateProtocol?
19 | var container: UIView = UIView()
20 | var loadingView: UIView = UIView()
21 | var activityIndicator: UIActivityIndicatorView = UIActivityIndicatorView()
22 | }
23 |
24 | extension BaseViewController {
25 | /*
26 | Show customized activity indicator,
27 | actually add activity indicator to passing view
28 |
29 | @param uiView - add activity indicator to this view
30 | */
31 | func showActivityIndicator(uiView: UIView) {
32 | container.frame = uiView.frame
33 | container.center = uiView.center
34 | container.backgroundColor = UIColorFromHex(rgbValue: 0xffffff, alpha: 0.3)
35 |
36 | loadingView.frame = CGRect(x: 0, y: 0, width: 80, height: 80)
37 | loadingView.center = uiView.center
38 | loadingView.backgroundColor = UIColorFromHex(rgbValue: 0x444444, alpha: 0.7)
39 | loadingView.clipsToBounds = true
40 | loadingView.layer.cornerRadius = 10
41 |
42 | activityIndicator.frame = CGRect(x: 0.0, y: 0.0, width: 40.0, height: 40.0);
43 | activityIndicator.style = UIActivityIndicatorView.Style.medium
44 | activityIndicator.center = CGPoint(x: loadingView.frame.size.width / 2, y: loadingView.frame.size.height / 2);
45 |
46 | loadingView.addSubview(activityIndicator)
47 | container.addSubview(loadingView)
48 | uiView.addSubview(container)
49 | activityIndicator.startAnimating()
50 | }
51 |
52 | /*
53 | Hide activity indicator
54 | Actually remove activity indicator from its super view
55 |
56 | @param uiView - remove activity indicator from this view
57 | */
58 | func hideActivityIndicator(uiView: UIView) {
59 | activityIndicator.stopAnimating()
60 | container.removeFromSuperview()
61 | }
62 |
63 | /*
64 | Define UIColor from hex value
65 |
66 | @param rgbValue - hex color value
67 | @param alpha - transparency level
68 | */
69 | func UIColorFromHex(rgbValue:UInt32, alpha:Double=1.0)->UIColor {
70 | let red = CGFloat((rgbValue & 0xFF0000) >> 16)/256.0
71 | let green = CGFloat((rgbValue & 0xFF00) >> 8)/256.0
72 | let blue = CGFloat(rgbValue & 0xFF)/256.0
73 | return UIColor(red:red, green:green, blue:blue, alpha:CGFloat(alpha))
74 | }
75 | }
76 |
77 | extension BaseViewController {
78 | func setViewState(state : ViewModelStatus, viewContainer : UIView) {
79 | switch state {
80 | case .loadStart:
81 | self.self.showActivityIndicator(uiView: self.view)
82 | case .dismissAlert:
83 | self.hideActivityIndicator(uiView: self.view)
84 | case .emptyStateHandler(let title, let isShow):
85 | self.delegate?.showEmptyStateView(title: title, errorType: .serverError, isShow: isShow)
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/GithubRepository/ViewModel/MainViewModel/MainViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MainViewModel.swift
3 | // GithubRepository
4 | //
5 | // Created by Mehran on 3/26/1401 AP.
6 | //
7 |
8 | import Foundation
9 | import Combine
10 |
11 | typealias MainBaseViewModel = ViewModelBaseProtocol & MainViewModelInput & MainViewModelOutput
12 |
13 | protocol MainViewModelInput {
14 | func didLoadNextPage()
15 | func didSearch(query: String, page: Int)
16 | }
17 |
18 | protocol MainViewModelOutput {
19 | var screenTitle: String { get }
20 | func callSearchService(query: RepositoriesQuery, page: Int)
21 | }
22 |
23 | final class MainViewModel : ObservableObject, MainBaseViewModel {
24 |
25 | var seachRepositories: SeachRepositoriesProtocol
26 | var bag = Set()
27 |
28 | @Published var publishedItems : [Repository] = []
29 | @Published var query : String = ""
30 |
31 | var currentPage: Int = 0
32 | var totalPageCount: Int = 1
33 | var isSearching = false
34 |
35 | // MARK: - OUTPUT
36 |
37 | var screenTitle: String = NavbarTitle.home.desc
38 | var loadinState = CurrentValueSubject(.dismissAlert)
39 |
40 | // MARK: - Init
41 |
42 | init(seachRepositories : SeachRepositoriesProtocol) {
43 | self.seachRepositories = seachRepositories
44 | }
45 |
46 | private func appendPage(_ repositoriesPage: RepositoryBaseModel?) {
47 | guard let data = repositoriesPage, let items = data.items else { return }
48 | self.totalPageCount = data.total ?? 1
49 | self.publishedItems.append(contentsOf: items)
50 | }
51 |
52 | private func resetPages() {
53 | currentPage = 1
54 | totalPageCount = 1
55 | publishedItems.removeAll()
56 | }
57 |
58 | func callSearchService(query: RepositoriesQuery, page: Int) {
59 |
60 | self.query = query.query
61 | self.isSearching = true
62 |
63 | self.loadinState.send(.loadStart)
64 | self.seachRepositories.getData(query: query.query, page: page)
65 | .receive(on: DispatchQueue.main)
66 | .sink { [weak self] result in
67 | switch result {
68 | case .finished:
69 | break
70 | case .failure(let error):
71 | self?.loadinState.send(.emptyStateHandler(title: error.desc, isShow: true))
72 | }
73 | self?.loadinState.send(.dismissAlert)
74 | self?.isSearching = false
75 | } receiveValue: { [weak self] data in
76 | self?.appendPage(data)
77 | }
78 | .store(in: &bag)
79 | }
80 |
81 | private func update(query: RepositoriesQuery, page: Int) {
82 | resetPages()
83 | callSearchService(query: query, page: page)
84 | }
85 | }
86 |
87 | // MARK: - INPUT. View event methods
88 |
89 | extension MainViewModel {
90 |
91 | func didLoadNextPage() {
92 | currentPage += 1
93 | self.callSearchService(query: .init(query: self.query), page: currentPage)
94 | }
95 |
96 | func didSearch(query: String, page: Int) {
97 | self.update(query: RepositoriesQuery(query: query), page: page)
98 | }
99 | }
100 |
101 |
--------------------------------------------------------------------------------
/GithubRepository/View/MainViewController/MainView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MainView.swift
3 | // GithubRepository
4 | //
5 | // Created by Mehran on 3/26/1401 AP.
6 | //
7 |
8 | import UIKit
9 |
10 | final class MainView: UIView {
11 |
12 | private var safeArea: UILayoutGuide!
13 |
14 | lazy var viewContainer : UIView = {
15 | let viewContainer = UIView()
16 | viewContainer.backgroundColor = .white
17 | viewContainer.translatesAutoresizingMaskIntoConstraints = false
18 | return viewContainer
19 | }()
20 |
21 | lazy var headerLabel: UILabel = {
22 | let label = UILabel()
23 | label.text = "Repositories"
24 | label.font = UIFont.systemFont(ofSize: 24, weight: .bold)
25 | label.numberOfLines = 1
26 | label.translatesAutoresizingMaskIntoConstraints = false
27 | return label
28 | }()
29 |
30 | lazy var initialRepoLabel: UILabel = {
31 | let label = UILabel()
32 | label.text = "Search repositories ..."
33 | label.font = UIFont.systemFont(ofSize: 18, weight: .medium)
34 | label.textColor = UIColor.systemGray4
35 | label.textAlignment = .center
36 | label.numberOfLines = 1
37 | label.translatesAutoresizingMaskIntoConstraints = false
38 | return label
39 | }()
40 |
41 | lazy var searchController: UISearchController = {
42 | let searchController = UISearchController(searchResultsController: nil)
43 | searchController.obscuresBackgroundDuringPresentation = false
44 | searchController.searchBar.placeholder = "Search"
45 | searchController.searchBar.sizeToFit()
46 | searchController.searchBar.searchBarStyle = .prominent
47 | return searchController
48 | }()
49 |
50 | lazy var tableView: UITableView = {
51 | let tableView = UITableView()
52 | tableView.separatorStyle = .none
53 | tableView.translatesAutoresizingMaskIntoConstraints = false
54 | return tableView
55 | }()
56 |
57 | init() {
58 | super.init(frame: .zero)
59 | setupUI()
60 | addSubviews()
61 | makeAutolayout()
62 | }
63 | required init?(coder: NSCoder) {
64 | fatalError("init(coder:) has not been implemented")
65 | }
66 |
67 | private func addSubviews() {
68 | addSubview(viewContainer)
69 | viewContainer.addSubview(tableView)
70 | }
71 |
72 | private func setupUI() {
73 | backgroundColor = .background
74 | safeArea = self.safeAreaLayoutGuide
75 | }
76 | }
77 |
78 | extension MainView {
79 | private func makeAutolayout() {
80 | NSLayoutConstraint.activate([
81 | viewContainer.topAnchor.constraint(equalTo: safeArea.topAnchor),
82 | viewContainer.leftAnchor.constraint(equalTo: safeArea.leftAnchor),
83 | viewContainer.rightAnchor.constraint(equalTo: safeArea.rightAnchor),
84 | viewContainer.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor)
85 | ])
86 |
87 | NSLayoutConstraint.activate([
88 | tableView.topAnchor.constraint(equalTo: self.viewContainer.topAnchor),
89 | tableView.leftAnchor.constraint(equalTo: self.viewContainer.leftAnchor),
90 | tableView.rightAnchor.constraint(equalTo: self.viewContainer.rightAnchor),
91 | tableView.bottomAnchor.constraint(equalTo: self.viewContainer.bottomAnchor)
92 | ])
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/GithubRepository/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/GithubRepository/View/MainViewController/MainViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MainViewController.swift
3 | // GithubRepository
4 | //
5 | // Created by Mehran on 3/26/1401 AP.
6 | //
7 |
8 | import UIKit
9 | import SwiftUI
10 |
11 | class MainViewController: BaseViewController {
12 |
13 | var contentView : MainView?
14 | var viewModel : MainViewModel!
15 | var throttler : ThrottleBehavierProtocol?
16 | private var dataSource:TableViewCustomDataSource?
17 |
18 | init(viewModel : MainViewModel,
19 | contentView : MainView, throttler: ThrottleBehavierProtocol) {
20 | self.viewModel = viewModel
21 | self.contentView = contentView
22 | self.throttler = throttler
23 | super.init(nibName: nil, bundle: nil)
24 | }
25 |
26 | required init?(coder aDecoder: NSCoder) {
27 | super.init(coder: aDecoder)
28 | }
29 |
30 | override func loadView() {
31 | view = contentView
32 | }
33 |
34 | override func viewDidLoad() {
35 | super.viewDidLoad()
36 | self.delegateHandler()
37 | self.setupUI()
38 | self.bindViewModel()
39 | self.throttleHandler()
40 | }
41 |
42 | private func delegateHandler() {
43 | super.delegate = self
44 | contentView?.viewContainer.emptyState.delegate = self
45 | }
46 |
47 | private func setupUI() {
48 | self.title = viewModel?.screenTitle
49 | view.backgroundColor = .background
50 | self.navigationControllerHandler()
51 | self.setupTableView()
52 | }
53 |
54 | private func navigationControllerHandler() {
55 | navigationController?.navigationBar.prefersLargeTitles = true
56 | navigationItem.searchController = self.contentView?.searchController
57 | self.navigationItem.searchController?.searchBar.delegate = self
58 | self.definesPresentationContext = true
59 | }
60 |
61 | private func bindViewModel() {
62 | self.viewModel?.loadinState
63 | .sink(receiveValue: { state in
64 | guard let view = self.contentView else {return}
65 | super.setViewState(state: state, viewContainer: view.viewContainer)
66 | }).store(in: &bag)
67 |
68 | self.viewModel?.$publishedItems
69 | .compactMap({ $0 })
70 | .sink { [weak self] data in
71 | self?.renderTableViewdataSource(data)
72 | }.store(in: &bag)
73 |
74 | self.dataSource?.$loadNextPage
75 | .sink(receiveValue: { data in
76 | guard data == true else {return}
77 | self.viewModel?.didLoadNextPage()
78 | }).store(in: &bag)
79 | }
80 |
81 | private func throttleHandler() {
82 | self.throttler?.on { [weak self] query in
83 | self?.viewModel?.didSearch(query: query, page: 1)
84 | }
85 |
86 | self.throttler?.emptyValue {
87 | self.dataSource?.models = nil
88 | DispatchQueue.main.async {
89 | self.contentView?.tableView.reloadData()
90 | }
91 | }
92 | }
93 |
94 | private func setupTableView() {
95 | contentView?.tableView.register(MainTableViewCell.self, forCellReuseIdentifier: MainTableViewCell.cellId)
96 | }
97 |
98 | private func renderTableViewdataSource(_ itemlists:[Repository]) {
99 | dataSource = .displayData(for: itemlists, withCellidentifier: MainTableViewCell.cellId)
100 | self.contentView?.tableView.dataSource = dataSource
101 | self.contentView?.tableView.delegate = self
102 | self.contentView?.tableView.reloadData()
103 | }
104 | }
105 |
106 | extension MainViewController : UISearchBarDelegate {
107 |
108 | func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
109 | let safeQuery = searchBar.text?.whiteSpacesRemoved()
110 | self.throttler?.receive(safeQuery ?? "")
111 | }
112 | }
113 |
114 | extension MainViewController : EmptyStateDelegate, ShowEmptyStateProtocol {
115 | func showEmptyStateView(title: String?, errorType: EmptyStateErrorType, isShow: Bool) {
116 | contentView?.viewContainer.emptyState.show(title: title ?? "",
117 | errorType: errorType,
118 | isShow: isShow)
119 | }
120 |
121 | func emptyStateButtonClicked() {
122 | self.contentView?.searchController.searchBar.text = nil
123 | contentView?.viewContainer.emptyState.hide()
124 | }
125 | }
126 |
127 | extension MainViewController : UITableViewDelegate {
128 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
129 | return UITableView.automaticDimension
130 | }
131 |
132 | func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
133 | return 120.0
134 | }
135 |
136 | func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
137 | guard indexPath.row == viewModel.publishedItems.count - 1,
138 | viewModel.currentPage < viewModel.totalPageCount - 1,
139 | !viewModel.isSearching
140 | else {return}
141 | viewModel.didLoadNextPage()
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/GithubRepository/ViewComponents/Cells/MainTableViewCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MainTableViewCell.swift
3 | // GithubRepository
4 | //
5 | // Created by Mehran on 3/26/1401 AP.
6 | //
7 |
8 | import UIKit
9 |
10 | class MainTableViewCell: UITableViewCell {
11 |
12 | static let cellId = "cellId"
13 |
14 | private lazy var containerView : UIView = {
15 | let view = UIView()
16 | view.layer.cornerRadius = 10
17 | view.layer.borderColor = .borderColor
18 | view.layer.borderWidth = 0.5
19 | view.clipsToBounds = true
20 | return view
21 | }()
22 |
23 | lazy var avatarImage: UIImageView = {
24 | let image = UIImageView()
25 | image.clipsToBounds = true
26 | image.layer.cornerRadius = 10
27 | return image
28 | }()
29 |
30 | lazy var lblName : UILabel = {
31 | let label = UILabel()
32 | label.text = "Name : "
33 | label.textColor = .gray
34 | label.minimumScaleFactor = 0.5
35 | label.numberOfLines = 2
36 | label.font = UIFont.boldSystemFont(ofSize: 12)
37 | return label
38 | }()
39 |
40 | lazy var lblRepoName : UILabel = {
41 | let label = UILabel()
42 | label.text = "RepoName : "
43 | label.textColor = .darkFontColor
44 | label.minimumScaleFactor = 0.5
45 | label.numberOfLines = 1
46 | label.font = UIFont.boldSystemFont(ofSize: 12)
47 | return label
48 | }()
49 |
50 | lazy var lblRepoDesc : UILabel = {
51 | let label = UILabel()
52 | label.text = "RepoDesc : "
53 | label.textColor = .darkFontColor
54 | label.minimumScaleFactor = 0.5
55 | label.numberOfLines = 0
56 | label.font = UIFont.systemFont(ofSize: 12)
57 | return label
58 | }()
59 |
60 | lazy var lblLanguageName : UILabel = {
61 | let label = UILabel()
62 | label.text = "RepoDesc : "
63 | label.textColor = .gray
64 | label.minimumScaleFactor = 0.5
65 | label.font = UIFont.boldSystemFont(ofSize: 14)
66 | return label
67 | }()
68 |
69 | override func prepareForReuse() {
70 | super.prepareForReuse()
71 | self.avatarImage.image = nil
72 | self.lblName.text = ""
73 | self.lblRepoName.text = ""
74 | }
75 |
76 | override func setSelected(_ selected: Bool, animated: Bool) {
77 | super.setSelected(selected, animated: animated)
78 | selectionStyle = .none
79 | addSubviews()
80 | setupUI()
81 | setupAutoLayout()
82 | }
83 |
84 | private func addSubviews() {
85 | [containerView,
86 | avatarImage,
87 | lblName,
88 | lblRepoName,
89 | lblRepoDesc,
90 | lblLanguageName]
91 | .forEach { items in
92 | items.translatesAutoresizingMaskIntoConstraints = false
93 | }
94 | self.contentView.addSubview(containerView)
95 | self.containerView.addSubview(avatarImage)
96 | self.containerView.addSubview(lblName)
97 | self.containerView.addSubview(lblRepoName)
98 | self.containerView.addSubview(lblRepoDesc)
99 | self.containerView.addSubview(lblLanguageName)
100 | }
101 | private func setupUI() {
102 | backgroundColor = .background
103 | }
104 |
105 | func setupParameters(data : Repository) {
106 | let ownerData = data.owner
107 | self.lblName.text = ownerData?.login
108 | self.lblRepoName.text = data.name
109 | self.lblRepoDesc.text = data.repoDescription
110 | self.lblLanguageName.text = data.language
111 | self.avatarImage.imageFromServerURL(ownerData?.avatarURL ?? "", placeHolder: UIImage(named: "Placeholder"))
112 | }
113 | }
114 |
115 | extension MainTableViewCell {
116 | private func setupAutoLayout() {
117 | containerView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 5).isActive = true
118 | containerView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -5).isActive = true
119 | containerView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 5).isActive = true
120 | containerView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -5).isActive = true
121 |
122 | avatarImage.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 10).isActive = true
123 | avatarImage.leftAnchor.constraint(equalTo: containerView.leftAnchor, constant: 10).isActive = true
124 | avatarImage.heightAnchor.constraint(equalToConstant: 40).isActive = true
125 | avatarImage.widthAnchor.constraint(equalToConstant: 40).isActive = true
126 |
127 | lblName.leftAnchor.constraint(equalTo: avatarImage.rightAnchor, constant: 10).isActive = true
128 | lblName.centerYAnchor.constraint(equalTo: avatarImage.centerYAnchor).isActive = true
129 | lblName.rightAnchor.constraint(greaterThanOrEqualTo: avatarImage.rightAnchor, constant: 10).isActive = true
130 |
131 | lblRepoName.topAnchor.constraint(equalTo: avatarImage.bottomAnchor, constant: 10).isActive = true
132 | lblRepoName.leftAnchor.constraint(equalTo: containerView.leftAnchor, constant: 10).isActive = true
133 | lblRepoName.rightAnchor.constraint(greaterThanOrEqualTo: avatarImage.rightAnchor, constant: -10).isActive = true
134 |
135 | lblRepoDesc.topAnchor.constraint(equalTo: lblRepoName.bottomAnchor, constant: 10).isActive = true
136 | lblRepoDesc.leftAnchor.constraint(equalTo: containerView.leftAnchor, constant: 10).isActive = true
137 | lblRepoDesc.rightAnchor.constraint(equalTo: containerView.rightAnchor, constant: -10).isActive = true
138 |
139 | lblLanguageName.topAnchor.constraint(equalTo: lblRepoDesc.bottomAnchor, constant: 10).isActive = true
140 | lblLanguageName.leftAnchor.constraint(equalTo: containerView.leftAnchor, constant: 10).isActive = true
141 | lblLanguageName.rightAnchor.constraint(equalTo: containerView.rightAnchor, constant: -10).isActive = true
142 | lblLanguageName.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -10).isActive = true
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/GithubRepository.xcodeproj/xcuserdata/mehrankamalifard.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
9 |
21 |
22 |
23 |
25 |
37 |
38 |
39 |
41 |
53 |
54 |
55 |
57 |
69 |
70 |
71 |
73 |
85 |
86 |
87 |
89 |
101 |
102 |
103 |
105 |
117 |
118 |
119 |
121 |
133 |
134 |
135 |
136 |
137 |
--------------------------------------------------------------------------------
/GithubRepository/ViewComponents/EmptyStateView/EmptyStateView.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | IRANSansMobile-Medium
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
33 |
34 |
60 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
--------------------------------------------------------------------------------
/GithubRepository.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 55;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 022039F8285B0FEB009E2B98 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 022039F7285B0FEB009E2B98 /* Alamofire */; };
11 | 022B6213285A3FA80088A1D2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B6212285A3FA80088A1D2 /* AppDelegate.swift */; };
12 | 022B621C285A3FAA0088A1D2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 022B621B285A3FAA0088A1D2 /* Assets.xcassets */; };
13 | 022B621F285A3FAA0088A1D2 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 022B621D285A3FAA0088A1D2 /* LaunchScreen.storyboard */; };
14 | 022B6234285A3FAA0088A1D2 /* GithubRepositoryUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B6233285A3FAA0088A1D2 /* GithubRepositoryUITests.swift */; };
15 | 022B6236285A3FAA0088A1D2 /* GithubRepositoryUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B6235285A3FAA0088A1D2 /* GithubRepositoryUITestsLaunchTests.swift */; };
16 | 022B6248285A425C0088A1D2 /* BaseAPI + Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B6247285A425C0088A1D2 /* BaseAPI + Extension.swift */; };
17 | 022B624A285A42810088A1D2 /* AFError + Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B6249285A42810088A1D2 /* AFError + Extension.swift */; };
18 | 022B624C285A42990088A1D2 /* String + Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B624B285A42990088A1D2 /* String + Extension.swift */; };
19 | 022B6251285A43190088A1D2 /* ViewModelBaseProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B6250285A43190088A1D2 /* ViewModelBaseProtocol.swift */; };
20 | 022B6258285A446F0088A1D2 /* BaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B6257285A446F0088A1D2 /* BaseViewController.swift */; };
21 | 022B625E285A45170088A1D2 /* Networking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B625D285A45170088A1D2 /* Networking.swift */; };
22 | 022B6262285A481D0088A1D2 /* BaseAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B6261285A481D0088A1D2 /* BaseAPI.swift */; };
23 | 022B6264285A48320088A1D2 /* TargerType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022B6263285A48320088A1D2 /* TargerType.swift */; };
24 | 02324D12285C7F8C0046BBFB /* Throttler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02324D11285C7F8C0046BBFB /* Throttler.swift */; };
25 | 02324D15285CE15E0046BBFB /* MainViewModelTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02324D14285CE15E0046BBFB /* MainViewModelTest.swift */; };
26 | 02324D18285CE20C0046BBFB /* MockServices.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02324D17285CE20C0046BBFB /* MockServices.swift */; };
27 | 02324D1B285CEA150046BBFB /* SearchListModelTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02324D1A285CEA150046BBFB /* SearchListModelTest.swift */; };
28 | 026BB01D285B013A00445411 /* MessageHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 026BB01C285B013A00445411 /* MessageHelper.swift */; };
29 | 026BB021285B01A500445411 /* MainCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 026BB020285B01A500445411 /* MainCoordinator.swift */; };
30 | 026BB023285B01B900445411 /* AppCoordiantor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 026BB022285B01B900445411 /* AppCoordiantor.swift */; };
31 | 026BB028285B025F00445411 /* MainFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 026BB027285B025F00445411 /* MainFactory.swift */; };
32 | 026BB02F285B03F600445411 /* AppConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 026BB02E285B03F600445411 /* AppConfiguration.swift */; };
33 | 026BB032285B049500445411 /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 026BB031285B049500445411 /* Coordinator.swift */; };
34 | 026BB034285B056900445411 /* Dictionary + Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 026BB033285B056900445411 /* Dictionary + Extension.swift */; };
35 | 026BB036285B059600445411 /* UIColor + Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 026BB035285B059600445411 /* UIColor + Extension.swift */; };
36 | 026BB038285B05B100445411 /* UIView + EmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 026BB037285B05B100445411 /* UIView + EmptyView.swift */; };
37 | 026BB03B285B05E200445411 /* BuildConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 026BB03A285B05E200445411 /* BuildConfig.swift */; };
38 | 026BB03E285B061E00445411 /* DependencyAssembler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 026BB03D285B061E00445411 /* DependencyAssembler.swift */; };
39 | 026BB045285B075300445411 /* EmptyState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 026BB042285B075300445411 /* EmptyState.swift */; };
40 | 026BB046285B075300445411 /* EmptyStateView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 026BB043285B075300445411 /* EmptyStateView.xib */; };
41 | 026BB047285B075300445411 /* EmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 026BB044285B075300445411 /* EmptyStateView.swift */; };
42 | 026BB04A285B0A3700445411 /* Repository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 026BB049285B0A3700445411 /* Repository.swift */; };
43 | 026BB050285B0CC300445411 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 026BB04F285B0CC300445411 /* MainViewController.swift */; };
44 | 026BB053285B0CDC00445411 /* MainViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 026BB052285B0CDC00445411 /* MainViewModel.swift */; };
45 | 026BB056285B0D1E00445411 /* Enums.swift in Sources */ = {isa = PBXBuildFile; fileRef = 026BB055285B0D1E00445411 /* Enums.swift */; };
46 | 026BB058285B0D8D00445411 /* AlamofireLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 026BB057285B0D8D00445411 /* AlamofireLogger.swift */; };
47 | 02A16051285B220F00A6571D /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A16050285B220F00A6571D /* MainView.swift */; };
48 | 02A16055285B2B2900A6571D /* RepositorySearchDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A16054285B2B2900A6571D /* RepositorySearchDataProvider.swift */; };
49 | 02A16057285B52A700A6571D /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A16056285B52A700A6571D /* Configuration.swift */; };
50 | 02A16059285B69E000A6571D /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A16058285B69E000A6571D /* Constants.swift */; };
51 | 02A1605B285B6F4C00A6571D /* MainTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A1605A285B6F4C00A6571D /* MainTableViewCell.swift */; };
52 | 02A1605E285B734A00A6571D /* TableViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A1605D285B734A00A6571D /* TableViewDataSource.swift */; };
53 | 02A16060285B8EBE00A6571D /* Owner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A1605F285B8EBE00A6571D /* Owner.swift */; };
54 | 02A16062285B9EE800A6571D /* UIImage + Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A16061285B9EE800A6571D /* UIImage + Extension.swift */; };
55 | A846F4AE285EEB4F00F027EC /* FakeEndPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = A846F4AD285EEB4F00F027EC /* FakeEndPoint.swift */; };
56 | A846F4B1285EEBB100F027EC /* EndpointTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A846F4B0285EEBB100F027EC /* EndpointTest.swift */; };
57 | A846F4BE285F010600F027EC /* FakeBuildConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A846F4BD285F010600F027EC /* FakeBuildConfig.swift */; };
58 | A846F4C1285F019400F027EC /* BuildConfigTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A846F4C0285F019400F027EC /* BuildConfigTest.swift */; };
59 | A846F4C3285F2CB500F027EC /* RepositoryBaseModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A846F4C2285F2CB500F027EC /* RepositoryBaseModel.swift */; };
60 | A846F4C5285F3CDC00F027EC /* MockModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = A846F4C4285F3CDC00F027EC /* MockModels.swift */; };
61 | A867B4BE2861E1820068B5EF /* RepositoriesQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = A867B4BD2861E1820068B5EF /* RepositoriesQuery.swift */; };
62 | /* End PBXBuildFile section */
63 |
64 | /* Begin PBXContainerItemProxy section */
65 | 022B6226285A3FAA0088A1D2 /* PBXContainerItemProxy */ = {
66 | isa = PBXContainerItemProxy;
67 | containerPortal = 022B6207285A3FA80088A1D2 /* Project object */;
68 | proxyType = 1;
69 | remoteGlobalIDString = 022B620E285A3FA80088A1D2;
70 | remoteInfo = GithubRepository;
71 | };
72 | 022B6230285A3FAA0088A1D2 /* PBXContainerItemProxy */ = {
73 | isa = PBXContainerItemProxy;
74 | containerPortal = 022B6207285A3FA80088A1D2 /* Project object */;
75 | proxyType = 1;
76 | remoteGlobalIDString = 022B620E285A3FA80088A1D2;
77 | remoteInfo = GithubRepository;
78 | };
79 | /* End PBXContainerItemProxy section */
80 |
81 | /* Begin PBXFileReference section */
82 | 022B620F285A3FA80088A1D2 /* GithubRepository.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GithubRepository.app; sourceTree = BUILT_PRODUCTS_DIR; };
83 | 022B6212285A3FA80088A1D2 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
84 | 022B621B285A3FAA0088A1D2 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
85 | 022B621E285A3FAA0088A1D2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
86 | 022B6220285A3FAA0088A1D2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
87 | 022B6225285A3FAA0088A1D2 /* GithubRepositoryTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GithubRepositoryTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
88 | 022B622F285A3FAA0088A1D2 /* GithubRepositoryUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GithubRepositoryUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
89 | 022B6233285A3FAA0088A1D2 /* GithubRepositoryUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubRepositoryUITests.swift; sourceTree = ""; };
90 | 022B6235285A3FAA0088A1D2 /* GithubRepositoryUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubRepositoryUITestsLaunchTests.swift; sourceTree = ""; };
91 | 022B6247285A425C0088A1D2 /* BaseAPI + Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseAPI + Extension.swift"; sourceTree = ""; };
92 | 022B6249285A42810088A1D2 /* AFError + Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AFError + Extension.swift"; sourceTree = ""; };
93 | 022B624B285A42990088A1D2 /* String + Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String + Extension.swift"; sourceTree = ""; };
94 | 022B6250285A43190088A1D2 /* ViewModelBaseProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModelBaseProtocol.swift; sourceTree = ""; };
95 | 022B6257285A446F0088A1D2 /* BaseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseViewController.swift; sourceTree = ""; };
96 | 022B625D285A45170088A1D2 /* Networking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Networking.swift; sourceTree = ""; };
97 | 022B6261285A481D0088A1D2 /* BaseAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseAPI.swift; sourceTree = ""; };
98 | 022B6263285A48320088A1D2 /* TargerType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TargerType.swift; sourceTree = ""; };
99 | 02324D11285C7F8C0046BBFB /* Throttler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Throttler.swift; sourceTree = ""; };
100 | 02324D14285CE15E0046BBFB /* MainViewModelTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewModelTest.swift; sourceTree = ""; };
101 | 02324D17285CE20C0046BBFB /* MockServices.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockServices.swift; sourceTree = ""; };
102 | 02324D1A285CEA150046BBFB /* SearchListModelTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchListModelTest.swift; sourceTree = ""; };
103 | 026BB01C285B013A00445411 /* MessageHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageHelper.swift; sourceTree = ""; };
104 | 026BB020285B01A500445411 /* MainCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainCoordinator.swift; sourceTree = ""; };
105 | 026BB022285B01B900445411 /* AppCoordiantor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordiantor.swift; sourceTree = ""; };
106 | 026BB027285B025F00445411 /* MainFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFactory.swift; sourceTree = ""; };
107 | 026BB02E285B03F600445411 /* AppConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfiguration.swift; sourceTree = ""; };
108 | 026BB031285B049500445411 /* Coordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Coordinator.swift; sourceTree = ""; };
109 | 026BB033285B056900445411 /* Dictionary + Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dictionary + Extension.swift"; sourceTree = ""; };
110 | 026BB035285B059600445411 /* UIColor + Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor + Extension.swift"; sourceTree = ""; };
111 | 026BB037285B05B100445411 /* UIView + EmptyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView + EmptyView.swift"; sourceTree = ""; };
112 | 026BB03A285B05E200445411 /* BuildConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildConfig.swift; sourceTree = ""; };
113 | 026BB03D285B061E00445411 /* DependencyAssembler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DependencyAssembler.swift; sourceTree = ""; };
114 | 026BB042285B075300445411 /* EmptyState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmptyState.swift; sourceTree = ""; };
115 | 026BB043285B075300445411 /* EmptyStateView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = EmptyStateView.xib; sourceTree = ""; };
116 | 026BB044285B075300445411 /* EmptyStateView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmptyStateView.swift; sourceTree = ""; };
117 | 026BB049285B0A3700445411 /* Repository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Repository.swift; sourceTree = ""; };
118 | 026BB04F285B0CC300445411 /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; };
119 | 026BB052285B0CDC00445411 /* MainViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewModel.swift; sourceTree = ""; };
120 | 026BB055285B0D1E00445411 /* Enums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Enums.swift; sourceTree = ""; };
121 | 026BB057285B0D8D00445411 /* AlamofireLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlamofireLogger.swift; sourceTree = ""; };
122 | 02A16050285B220F00A6571D /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; };
123 | 02A16054285B2B2900A6571D /* RepositorySearchDataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositorySearchDataProvider.swift; sourceTree = ""; };
124 | 02A16056285B52A700A6571D /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; };
125 | 02A16058285B69E000A6571D /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; };
126 | 02A1605A285B6F4C00A6571D /* MainTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTableViewCell.swift; sourceTree = ""; };
127 | 02A1605D285B734A00A6571D /* TableViewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewDataSource.swift; sourceTree = ""; };
128 | 02A1605F285B8EBE00A6571D /* Owner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Owner.swift; sourceTree = ""; };
129 | 02A16061285B9EE800A6571D /* UIImage + Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage + Extension.swift"; sourceTree = ""; };
130 | A846F4AD285EEB4F00F027EC /* FakeEndPoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeEndPoint.swift; sourceTree = ""; };
131 | A846F4B0285EEBB100F027EC /* EndpointTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndpointTest.swift; sourceTree = ""; };
132 | A846F4BD285F010600F027EC /* FakeBuildConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeBuildConfig.swift; sourceTree = ""; };
133 | A846F4C0285F019400F027EC /* BuildConfigTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildConfigTest.swift; sourceTree = ""; };
134 | A846F4C2285F2CB500F027EC /* RepositoryBaseModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoryBaseModel.swift; sourceTree = ""; };
135 | A846F4C4285F3CDC00F027EC /* MockModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockModels.swift; sourceTree = ""; };
136 | A867B4BD2861E1820068B5EF /* RepositoriesQuery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoriesQuery.swift; sourceTree = ""; };
137 | /* End PBXFileReference section */
138 |
139 | /* Begin PBXFrameworksBuildPhase section */
140 | 022B620C285A3FA80088A1D2 /* Frameworks */ = {
141 | isa = PBXFrameworksBuildPhase;
142 | buildActionMask = 2147483647;
143 | files = (
144 | 022039F8285B0FEB009E2B98 /* Alamofire in Frameworks */,
145 | );
146 | runOnlyForDeploymentPostprocessing = 0;
147 | };
148 | 022B6222285A3FAA0088A1D2 /* Frameworks */ = {
149 | isa = PBXFrameworksBuildPhase;
150 | buildActionMask = 2147483647;
151 | files = (
152 | );
153 | runOnlyForDeploymentPostprocessing = 0;
154 | };
155 | 022B622C285A3FAA0088A1D2 /* Frameworks */ = {
156 | isa = PBXFrameworksBuildPhase;
157 | buildActionMask = 2147483647;
158 | files = (
159 | );
160 | runOnlyForDeploymentPostprocessing = 0;
161 | };
162 | /* End PBXFrameworksBuildPhase section */
163 |
164 | /* Begin PBXGroup section */
165 | 022B6206285A3FA80088A1D2 = {
166 | isa = PBXGroup;
167 | children = (
168 | 022B6211285A3FA80088A1D2 /* GithubRepository */,
169 | 022B6228285A3FAA0088A1D2 /* GithubRepositoryTests */,
170 | 022B6232285A3FAA0088A1D2 /* GithubRepositoryUITests */,
171 | 022B6210285A3FA80088A1D2 /* Products */,
172 | );
173 | sourceTree = "";
174 | };
175 | 022B6210285A3FA80088A1D2 /* Products */ = {
176 | isa = PBXGroup;
177 | children = (
178 | 022B620F285A3FA80088A1D2 /* GithubRepository.app */,
179 | 022B6225285A3FAA0088A1D2 /* GithubRepositoryTests.xctest */,
180 | 022B622F285A3FAA0088A1D2 /* GithubRepositoryUITests.xctest */,
181 | );
182 | name = Products;
183 | sourceTree = "";
184 | };
185 | 022B6211285A3FA80088A1D2 /* GithubRepository */ = {
186 | isa = PBXGroup;
187 | children = (
188 | 026BB02D285B03CE00445411 /* App */,
189 | 022B6245285A41610088A1D2 /* Classes */,
190 | 02A16052285B2ACB00A6571D /* DataProviders */,
191 | 026BB03F285B06D400445411 /* ViewComponents */,
192 | 022B6243285A41450088A1D2 /* Model */,
193 | 022B6244285A414B0088A1D2 /* View */,
194 | 022B6242285A413C0088A1D2 /* ViewModel */,
195 | 022B621B285A3FAA0088A1D2 /* Assets.xcassets */,
196 | 022B621D285A3FAA0088A1D2 /* LaunchScreen.storyboard */,
197 | 022B6220285A3FAA0088A1D2 /* Info.plist */,
198 | );
199 | path = GithubRepository;
200 | sourceTree = "";
201 | };
202 | 022B6228285A3FAA0088A1D2 /* GithubRepositoryTests */ = {
203 | isa = PBXGroup;
204 | children = (
205 | A846F4BF285F018700F027EC /* BuildConfig */,
206 | A846F4AF285EEB8C00F027EC /* Networking */,
207 | 02324D19285CE9F10046BBFB /* Model */,
208 | 02324D13285CE1090046BBFB /* ViewModels */,
209 | 02324D16285CE1ED0046BBFB /* Mock */,
210 | A846F4BC285F00EF00F027EC /* Fake */,
211 | );
212 | path = GithubRepositoryTests;
213 | sourceTree = "";
214 | };
215 | 022B6232285A3FAA0088A1D2 /* GithubRepositoryUITests */ = {
216 | isa = PBXGroup;
217 | children = (
218 | 022B6233285A3FAA0088A1D2 /* GithubRepositoryUITests.swift */,
219 | 022B6235285A3FAA0088A1D2 /* GithubRepositoryUITestsLaunchTests.swift */,
220 | );
221 | path = GithubRepositoryUITests;
222 | sourceTree = "";
223 | };
224 | 022B6242285A413C0088A1D2 /* ViewModel */ = {
225 | isa = PBXGroup;
226 | children = (
227 | 026BB051285B0CCC00445411 /* MainViewModel */,
228 | );
229 | path = ViewModel;
230 | sourceTree = "";
231 | };
232 | 022B6243285A41450088A1D2 /* Model */ = {
233 | isa = PBXGroup;
234 | children = (
235 | A867B4BC2861E0ED0068B5EF /* RepositoriesQuery */,
236 | 026BB048285B0A1700445411 /* SearchListModel */,
237 | );
238 | path = Model;
239 | sourceTree = "";
240 | };
241 | 022B6244285A414B0088A1D2 /* View */ = {
242 | isa = PBXGroup;
243 | children = (
244 | 026BB04E285B0CB000445411 /* MainViewController */,
245 | );
246 | path = View;
247 | sourceTree = "";
248 | };
249 | 022B6245285A41610088A1D2 /* Classes */ = {
250 | isa = PBXGroup;
251 | children = (
252 | 022B6256285A44500088A1D2 /* Base */,
253 | 026BB054285B0CFF00445411 /* Enums */,
254 | 026BB01B285B012200445411 /* Helper */,
255 | 026BB01F285B018200445411 /* Routing */,
256 | 026BB024285B01CD00445411 /* Protocol */,
257 | 026BB01E285B016100445411 /* Constants */,
258 | 022B624D285A42BD0088A1D2 /* Protocols */,
259 | 022B6246285A42450088A1D2 /* Extension */,
260 | 026BB039285B05D600445411 /* BuildConfig */,
261 | 022B625B285A44ED0088A1D2 /* Networking */,
262 | 02A1605C285B732D00A6571D /* TableViewDataSource */,
263 | );
264 | path = Classes;
265 | sourceTree = "";
266 | };
267 | 022B6246285A42450088A1D2 /* Extension */ = {
268 | isa = PBXGroup;
269 | children = (
270 | 022B6247285A425C0088A1D2 /* BaseAPI + Extension.swift */,
271 | 022B6249285A42810088A1D2 /* AFError + Extension.swift */,
272 | 022B624B285A42990088A1D2 /* String + Extension.swift */,
273 | 026BB033285B056900445411 /* Dictionary + Extension.swift */,
274 | 026BB035285B059600445411 /* UIColor + Extension.swift */,
275 | 026BB037285B05B100445411 /* UIView + EmptyView.swift */,
276 | 02A16061285B9EE800A6571D /* UIImage + Extension.swift */,
277 | );
278 | path = Extension;
279 | sourceTree = "";
280 | };
281 | 022B624D285A42BD0088A1D2 /* Protocols */ = {
282 | isa = PBXGroup;
283 | children = (
284 | 022B6250285A43190088A1D2 /* ViewModelBaseProtocol.swift */,
285 | );
286 | path = Protocols;
287 | sourceTree = "";
288 | };
289 | 022B6256285A44500088A1D2 /* Base */ = {
290 | isa = PBXGroup;
291 | children = (
292 | 022B6257285A446F0088A1D2 /* BaseViewController.swift */,
293 | );
294 | path = Base;
295 | sourceTree = "";
296 | };
297 | 022B625B285A44ED0088A1D2 /* Networking */ = {
298 | isa = PBXGroup;
299 | children = (
300 | 022B625F285A47DC0088A1D2 /* BaseAPI */,
301 | 022B625C285A44FF0088A1D2 /* EndPoint */,
302 | 022B6260285A47E90088A1D2 /* Utilities */,
303 | );
304 | path = Networking;
305 | sourceTree = "";
306 | };
307 | 022B625C285A44FF0088A1D2 /* EndPoint */ = {
308 | isa = PBXGroup;
309 | children = (
310 | 022B625D285A45170088A1D2 /* Networking.swift */,
311 | );
312 | name = EndPoint;
313 | sourceTree = "";
314 | };
315 | 022B625F285A47DC0088A1D2 /* BaseAPI */ = {
316 | isa = PBXGroup;
317 | children = (
318 | 022B6261285A481D0088A1D2 /* BaseAPI.swift */,
319 | );
320 | name = BaseAPI;
321 | sourceTree = "";
322 | };
323 | 022B6260285A47E90088A1D2 /* Utilities */ = {
324 | isa = PBXGroup;
325 | children = (
326 | 022B6263285A48320088A1D2 /* TargerType.swift */,
327 | 026BB057285B0D8D00445411 /* AlamofireLogger.swift */,
328 | 02A16056285B52A700A6571D /* Configuration.swift */,
329 | );
330 | name = Utilities;
331 | sourceTree = "";
332 | };
333 | 02324D13285CE1090046BBFB /* ViewModels */ = {
334 | isa = PBXGroup;
335 | children = (
336 | 02324D14285CE15E0046BBFB /* MainViewModelTest.swift */,
337 | );
338 | path = ViewModels;
339 | sourceTree = "";
340 | };
341 | 02324D16285CE1ED0046BBFB /* Mock */ = {
342 | isa = PBXGroup;
343 | children = (
344 | 02324D17285CE20C0046BBFB /* MockServices.swift */,
345 | A846F4C4285F3CDC00F027EC /* MockModels.swift */,
346 | );
347 | path = Mock;
348 | sourceTree = "";
349 | };
350 | 02324D19285CE9F10046BBFB /* Model */ = {
351 | isa = PBXGroup;
352 | children = (
353 | 02324D1A285CEA150046BBFB /* SearchListModelTest.swift */,
354 | );
355 | path = Model;
356 | sourceTree = "";
357 | };
358 | 026BB01B285B012200445411 /* Helper */ = {
359 | isa = PBXGroup;
360 | children = (
361 | 026BB01C285B013A00445411 /* MessageHelper.swift */,
362 | 02324D11285C7F8C0046BBFB /* Throttler.swift */,
363 | );
364 | path = Helper;
365 | sourceTree = "";
366 | };
367 | 026BB01E285B016100445411 /* Constants */ = {
368 | isa = PBXGroup;
369 | children = (
370 | 02A16058285B69E000A6571D /* Constants.swift */,
371 | );
372 | path = Constants;
373 | sourceTree = "";
374 | };
375 | 026BB01F285B018200445411 /* Routing */ = {
376 | isa = PBXGroup;
377 | children = (
378 | 026BB020285B01A500445411 /* MainCoordinator.swift */,
379 | );
380 | path = Routing;
381 | sourceTree = "";
382 | };
383 | 026BB024285B01CD00445411 /* Protocol */ = {
384 | isa = PBXGroup;
385 | children = (
386 | 026BB030285B047900445411 /* CoordinatorPattern */,
387 | 026BB025285B024200445411 /* Factory */,
388 | );
389 | path = Protocol;
390 | sourceTree = "";
391 | };
392 | 026BB025285B024200445411 /* Factory */ = {
393 | isa = PBXGroup;
394 | children = (
395 | 026BB026285B024B00445411 /* MainFactory */,
396 | );
397 | path = Factory;
398 | sourceTree = "";
399 | };
400 | 026BB026285B024B00445411 /* MainFactory */ = {
401 | isa = PBXGroup;
402 | children = (
403 | 026BB027285B025F00445411 /* MainFactory.swift */,
404 | );
405 | path = MainFactory;
406 | sourceTree = "";
407 | };
408 | 026BB02D285B03CE00445411 /* App */ = {
409 | isa = PBXGroup;
410 | children = (
411 | 022B6212285A3FA80088A1D2 /* AppDelegate.swift */,
412 | 026BB02E285B03F600445411 /* AppConfiguration.swift */,
413 | 026BB022285B01B900445411 /* AppCoordiantor.swift */,
414 | 026BB03D285B061E00445411 /* DependencyAssembler.swift */,
415 | );
416 | path = App;
417 | sourceTree = "";
418 | };
419 | 026BB030285B047900445411 /* CoordinatorPattern */ = {
420 | isa = PBXGroup;
421 | children = (
422 | 026BB031285B049500445411 /* Coordinator.swift */,
423 | );
424 | path = CoordinatorPattern;
425 | sourceTree = "";
426 | };
427 | 026BB039285B05D600445411 /* BuildConfig */ = {
428 | isa = PBXGroup;
429 | children = (
430 | 026BB03A285B05E200445411 /* BuildConfig.swift */,
431 | );
432 | path = BuildConfig;
433 | sourceTree = "";
434 | };
435 | 026BB03F285B06D400445411 /* ViewComponents */ = {
436 | isa = PBXGroup;
437 | children = (
438 | 026BB041285B070100445411 /* Cells */,
439 | 026BB040285B06FA00445411 /* EmptyStateView */,
440 | );
441 | path = ViewComponents;
442 | sourceTree = "";
443 | };
444 | 026BB040285B06FA00445411 /* EmptyStateView */ = {
445 | isa = PBXGroup;
446 | children = (
447 | 026BB042285B075300445411 /* EmptyState.swift */,
448 | 026BB044285B075300445411 /* EmptyStateView.swift */,
449 | 026BB043285B075300445411 /* EmptyStateView.xib */,
450 | );
451 | path = EmptyStateView;
452 | sourceTree = "";
453 | };
454 | 026BB041285B070100445411 /* Cells */ = {
455 | isa = PBXGroup;
456 | children = (
457 | 02A1605A285B6F4C00A6571D /* MainTableViewCell.swift */,
458 | );
459 | path = Cells;
460 | sourceTree = "";
461 | };
462 | 026BB048285B0A1700445411 /* SearchListModel */ = {
463 | isa = PBXGroup;
464 | children = (
465 | A846F4C2285F2CB500F027EC /* RepositoryBaseModel.swift */,
466 | 026BB049285B0A3700445411 /* Repository.swift */,
467 | 02A1605F285B8EBE00A6571D /* Owner.swift */,
468 | );
469 | path = SearchListModel;
470 | sourceTree = "";
471 | };
472 | 026BB04E285B0CB000445411 /* MainViewController */ = {
473 | isa = PBXGroup;
474 | children = (
475 | 026BB04F285B0CC300445411 /* MainViewController.swift */,
476 | 02A16050285B220F00A6571D /* MainView.swift */,
477 | );
478 | path = MainViewController;
479 | sourceTree = "";
480 | };
481 | 026BB051285B0CCC00445411 /* MainViewModel */ = {
482 | isa = PBXGroup;
483 | children = (
484 | 026BB052285B0CDC00445411 /* MainViewModel.swift */,
485 | );
486 | path = MainViewModel;
487 | sourceTree = "";
488 | };
489 | 026BB054285B0CFF00445411 /* Enums */ = {
490 | isa = PBXGroup;
491 | children = (
492 | 026BB055285B0D1E00445411 /* Enums.swift */,
493 | );
494 | path = Enums;
495 | sourceTree = "";
496 | };
497 | 02A16052285B2ACB00A6571D /* DataProviders */ = {
498 | isa = PBXGroup;
499 | children = (
500 | 02A16053285B2ADB00A6571D /* RepositoriesSearch */,
501 | );
502 | path = DataProviders;
503 | sourceTree = "";
504 | };
505 | 02A16053285B2ADB00A6571D /* RepositoriesSearch */ = {
506 | isa = PBXGroup;
507 | children = (
508 | 02A16054285B2B2900A6571D /* RepositorySearchDataProvider.swift */,
509 | );
510 | path = RepositoriesSearch;
511 | sourceTree = "";
512 | };
513 | 02A1605C285B732D00A6571D /* TableViewDataSource */ = {
514 | isa = PBXGroup;
515 | children = (
516 | 02A1605D285B734A00A6571D /* TableViewDataSource.swift */,
517 | );
518 | path = TableViewDataSource;
519 | sourceTree = "";
520 | };
521 | A846F4AF285EEB8C00F027EC /* Networking */ = {
522 | isa = PBXGroup;
523 | children = (
524 | A846F4B0285EEBB100F027EC /* EndpointTest.swift */,
525 | );
526 | path = Networking;
527 | sourceTree = "";
528 | };
529 | A846F4BC285F00EF00F027EC /* Fake */ = {
530 | isa = PBXGroup;
531 | children = (
532 | A846F4AD285EEB4F00F027EC /* FakeEndPoint.swift */,
533 | A846F4BD285F010600F027EC /* FakeBuildConfig.swift */,
534 | );
535 | path = Fake;
536 | sourceTree = "";
537 | };
538 | A846F4BF285F018700F027EC /* BuildConfig */ = {
539 | isa = PBXGroup;
540 | children = (
541 | A846F4C0285F019400F027EC /* BuildConfigTest.swift */,
542 | );
543 | path = BuildConfig;
544 | sourceTree = "";
545 | };
546 | A867B4BC2861E0ED0068B5EF /* RepositoriesQuery */ = {
547 | isa = PBXGroup;
548 | children = (
549 | A867B4BD2861E1820068B5EF /* RepositoriesQuery.swift */,
550 | );
551 | path = RepositoriesQuery;
552 | sourceTree = "";
553 | };
554 | /* End PBXGroup section */
555 |
556 | /* Begin PBXNativeTarget section */
557 | 022B620E285A3FA80088A1D2 /* GithubRepository */ = {
558 | isa = PBXNativeTarget;
559 | buildConfigurationList = 022B6239285A3FAA0088A1D2 /* Build configuration list for PBXNativeTarget "GithubRepository" */;
560 | buildPhases = (
561 | 022B620B285A3FA80088A1D2 /* Sources */,
562 | 022B620C285A3FA80088A1D2 /* Frameworks */,
563 | 022B620D285A3FA80088A1D2 /* Resources */,
564 | );
565 | buildRules = (
566 | );
567 | dependencies = (
568 | );
569 | name = GithubRepository;
570 | packageProductDependencies = (
571 | 022039F7285B0FEB009E2B98 /* Alamofire */,
572 | );
573 | productName = GithubRepository;
574 | productReference = 022B620F285A3FA80088A1D2 /* GithubRepository.app */;
575 | productType = "com.apple.product-type.application";
576 | };
577 | 022B6224285A3FAA0088A1D2 /* GithubRepositoryTests */ = {
578 | isa = PBXNativeTarget;
579 | buildConfigurationList = 022B623C285A3FAA0088A1D2 /* Build configuration list for PBXNativeTarget "GithubRepositoryTests" */;
580 | buildPhases = (
581 | 022B6221285A3FAA0088A1D2 /* Sources */,
582 | 022B6222285A3FAA0088A1D2 /* Frameworks */,
583 | 022B6223285A3FAA0088A1D2 /* Resources */,
584 | );
585 | buildRules = (
586 | );
587 | dependencies = (
588 | 022B6227285A3FAA0088A1D2 /* PBXTargetDependency */,
589 | );
590 | name = GithubRepositoryTests;
591 | productName = GithubRepositoryTests;
592 | productReference = 022B6225285A3FAA0088A1D2 /* GithubRepositoryTests.xctest */;
593 | productType = "com.apple.product-type.bundle.unit-test";
594 | };
595 | 022B622E285A3FAA0088A1D2 /* GithubRepositoryUITests */ = {
596 | isa = PBXNativeTarget;
597 | buildConfigurationList = 022B623F285A3FAA0088A1D2 /* Build configuration list for PBXNativeTarget "GithubRepositoryUITests" */;
598 | buildPhases = (
599 | 022B622B285A3FAA0088A1D2 /* Sources */,
600 | 022B622C285A3FAA0088A1D2 /* Frameworks */,
601 | 022B622D285A3FAA0088A1D2 /* Resources */,
602 | );
603 | buildRules = (
604 | );
605 | dependencies = (
606 | 022B6231285A3FAA0088A1D2 /* PBXTargetDependency */,
607 | );
608 | name = GithubRepositoryUITests;
609 | productName = GithubRepositoryUITests;
610 | productReference = 022B622F285A3FAA0088A1D2 /* GithubRepositoryUITests.xctest */;
611 | productType = "com.apple.product-type.bundle.ui-testing";
612 | };
613 | /* End PBXNativeTarget section */
614 |
615 | /* Begin PBXProject section */
616 | 022B6207285A3FA80088A1D2 /* Project object */ = {
617 | isa = PBXProject;
618 | attributes = {
619 | BuildIndependentTargetsInParallel = 1;
620 | LastSwiftUpdateCheck = 1330;
621 | LastUpgradeCheck = 1330;
622 | TargetAttributes = {
623 | 022B620E285A3FA80088A1D2 = {
624 | CreatedOnToolsVersion = 13.3;
625 | };
626 | 022B6224285A3FAA0088A1D2 = {
627 | CreatedOnToolsVersion = 13.3;
628 | LastSwiftMigration = 1330;
629 | TestTargetID = 022B620E285A3FA80088A1D2;
630 | };
631 | 022B622E285A3FAA0088A1D2 = {
632 | CreatedOnToolsVersion = 13.3;
633 | TestTargetID = 022B620E285A3FA80088A1D2;
634 | };
635 | };
636 | };
637 | buildConfigurationList = 022B620A285A3FA80088A1D2 /* Build configuration list for PBXProject "GithubRepository" */;
638 | compatibilityVersion = "Xcode 13.0";
639 | developmentRegion = en;
640 | hasScannedForEncodings = 0;
641 | knownRegions = (
642 | en,
643 | Base,
644 | );
645 | mainGroup = 022B6206285A3FA80088A1D2;
646 | packageReferences = (
647 | 022039F6285B0FEB009E2B98 /* XCRemoteSwiftPackageReference "Alamofire" */,
648 | );
649 | productRefGroup = 022B6210285A3FA80088A1D2 /* Products */;
650 | projectDirPath = "";
651 | projectRoot = "";
652 | targets = (
653 | 022B620E285A3FA80088A1D2 /* GithubRepository */,
654 | 022B6224285A3FAA0088A1D2 /* GithubRepositoryTests */,
655 | 022B622E285A3FAA0088A1D2 /* GithubRepositoryUITests */,
656 | );
657 | };
658 | /* End PBXProject section */
659 |
660 | /* Begin PBXResourcesBuildPhase section */
661 | 022B620D285A3FA80088A1D2 /* Resources */ = {
662 | isa = PBXResourcesBuildPhase;
663 | buildActionMask = 2147483647;
664 | files = (
665 | 026BB046285B075300445411 /* EmptyStateView.xib in Resources */,
666 | 022B621F285A3FAA0088A1D2 /* LaunchScreen.storyboard in Resources */,
667 | 022B621C285A3FAA0088A1D2 /* Assets.xcassets in Resources */,
668 | );
669 | runOnlyForDeploymentPostprocessing = 0;
670 | };
671 | 022B6223285A3FAA0088A1D2 /* Resources */ = {
672 | isa = PBXResourcesBuildPhase;
673 | buildActionMask = 2147483647;
674 | files = (
675 | );
676 | runOnlyForDeploymentPostprocessing = 0;
677 | };
678 | 022B622D285A3FAA0088A1D2 /* Resources */ = {
679 | isa = PBXResourcesBuildPhase;
680 | buildActionMask = 2147483647;
681 | files = (
682 | );
683 | runOnlyForDeploymentPostprocessing = 0;
684 | };
685 | /* End PBXResourcesBuildPhase section */
686 |
687 | /* Begin PBXSourcesBuildPhase section */
688 | 022B620B285A3FA80088A1D2 /* Sources */ = {
689 | isa = PBXSourcesBuildPhase;
690 | buildActionMask = 2147483647;
691 | files = (
692 | 026BB03E285B061E00445411 /* DependencyAssembler.swift in Sources */,
693 | 02A16059285B69E000A6571D /* Constants.swift in Sources */,
694 | 026BB03B285B05E200445411 /* BuildConfig.swift in Sources */,
695 | 022B6213285A3FA80088A1D2 /* AppDelegate.swift in Sources */,
696 | A846F4C3285F2CB500F027EC /* RepositoryBaseModel.swift in Sources */,
697 | 026BB045285B075300445411 /* EmptyState.swift in Sources */,
698 | 026BB021285B01A500445411 /* MainCoordinator.swift in Sources */,
699 | 026BB04A285B0A3700445411 /* Repository.swift in Sources */,
700 | A867B4BE2861E1820068B5EF /* RepositoriesQuery.swift in Sources */,
701 | 022B624C285A42990088A1D2 /* String + Extension.swift in Sources */,
702 | 026BB050285B0CC300445411 /* MainViewController.swift in Sources */,
703 | 026BB058285B0D8D00445411 /* AlamofireLogger.swift in Sources */,
704 | 026BB053285B0CDC00445411 /* MainViewModel.swift in Sources */,
705 | 026BB02F285B03F600445411 /* AppConfiguration.swift in Sources */,
706 | 022B625E285A45170088A1D2 /* Networking.swift in Sources */,
707 | 026BB01D285B013A00445411 /* MessageHelper.swift in Sources */,
708 | 022B624A285A42810088A1D2 /* AFError + Extension.swift in Sources */,
709 | 026BB032285B049500445411 /* Coordinator.swift in Sources */,
710 | 026BB047285B075300445411 /* EmptyStateView.swift in Sources */,
711 | 02A1605E285B734A00A6571D /* TableViewDataSource.swift in Sources */,
712 | 026BB036285B059600445411 /* UIColor + Extension.swift in Sources */,
713 | 026BB038285B05B100445411 /* UIView + EmptyView.swift in Sources */,
714 | 026BB023285B01B900445411 /* AppCoordiantor.swift in Sources */,
715 | 022B6258285A446F0088A1D2 /* BaseViewController.swift in Sources */,
716 | 02324D12285C7F8C0046BBFB /* Throttler.swift in Sources */,
717 | 02A16057285B52A700A6571D /* Configuration.swift in Sources */,
718 | 02A16062285B9EE800A6571D /* UIImage + Extension.swift in Sources */,
719 | 02A1605B285B6F4C00A6571D /* MainTableViewCell.swift in Sources */,
720 | 026BB028285B025F00445411 /* MainFactory.swift in Sources */,
721 | 026BB056285B0D1E00445411 /* Enums.swift in Sources */,
722 | 02A16055285B2B2900A6571D /* RepositorySearchDataProvider.swift in Sources */,
723 | 022B6264285A48320088A1D2 /* TargerType.swift in Sources */,
724 | 022B6248285A425C0088A1D2 /* BaseAPI + Extension.swift in Sources */,
725 | 022B6251285A43190088A1D2 /* ViewModelBaseProtocol.swift in Sources */,
726 | 02A16060285B8EBE00A6571D /* Owner.swift in Sources */,
727 | 02A16051285B220F00A6571D /* MainView.swift in Sources */,
728 | 022B6262285A481D0088A1D2 /* BaseAPI.swift in Sources */,
729 | 026BB034285B056900445411 /* Dictionary + Extension.swift in Sources */,
730 | );
731 | runOnlyForDeploymentPostprocessing = 0;
732 | };
733 | 022B6221285A3FAA0088A1D2 /* Sources */ = {
734 | isa = PBXSourcesBuildPhase;
735 | buildActionMask = 2147483647;
736 | files = (
737 | A846F4AE285EEB4F00F027EC /* FakeEndPoint.swift in Sources */,
738 | A846F4C5285F3CDC00F027EC /* MockModels.swift in Sources */,
739 | A846F4BE285F010600F027EC /* FakeBuildConfig.swift in Sources */,
740 | 02324D15285CE15E0046BBFB /* MainViewModelTest.swift in Sources */,
741 | A846F4B1285EEBB100F027EC /* EndpointTest.swift in Sources */,
742 | A846F4C1285F019400F027EC /* BuildConfigTest.swift in Sources */,
743 | 02324D1B285CEA150046BBFB /* SearchListModelTest.swift in Sources */,
744 | 02324D18285CE20C0046BBFB /* MockServices.swift in Sources */,
745 | );
746 | runOnlyForDeploymentPostprocessing = 0;
747 | };
748 | 022B622B285A3FAA0088A1D2 /* Sources */ = {
749 | isa = PBXSourcesBuildPhase;
750 | buildActionMask = 2147483647;
751 | files = (
752 | 022B6234285A3FAA0088A1D2 /* GithubRepositoryUITests.swift in Sources */,
753 | 022B6236285A3FAA0088A1D2 /* GithubRepositoryUITestsLaunchTests.swift in Sources */,
754 | );
755 | runOnlyForDeploymentPostprocessing = 0;
756 | };
757 | /* End PBXSourcesBuildPhase section */
758 |
759 | /* Begin PBXTargetDependency section */
760 | 022B6227285A3FAA0088A1D2 /* PBXTargetDependency */ = {
761 | isa = PBXTargetDependency;
762 | target = 022B620E285A3FA80088A1D2 /* GithubRepository */;
763 | targetProxy = 022B6226285A3FAA0088A1D2 /* PBXContainerItemProxy */;
764 | };
765 | 022B6231285A3FAA0088A1D2 /* PBXTargetDependency */ = {
766 | isa = PBXTargetDependency;
767 | target = 022B620E285A3FA80088A1D2 /* GithubRepository */;
768 | targetProxy = 022B6230285A3FAA0088A1D2 /* PBXContainerItemProxy */;
769 | };
770 | /* End PBXTargetDependency section */
771 |
772 | /* Begin PBXVariantGroup section */
773 | 022B621D285A3FAA0088A1D2 /* LaunchScreen.storyboard */ = {
774 | isa = PBXVariantGroup;
775 | children = (
776 | 022B621E285A3FAA0088A1D2 /* Base */,
777 | );
778 | name = LaunchScreen.storyboard;
779 | sourceTree = "";
780 | };
781 | /* End PBXVariantGroup section */
782 |
783 | /* Begin XCBuildConfiguration section */
784 | 022B6237285A3FAA0088A1D2 /* Debug */ = {
785 | isa = XCBuildConfiguration;
786 | buildSettings = {
787 | ALWAYS_SEARCH_USER_PATHS = NO;
788 | CLANG_ANALYZER_NONNULL = YES;
789 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
790 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
791 | CLANG_ENABLE_MODULES = YES;
792 | CLANG_ENABLE_OBJC_ARC = YES;
793 | CLANG_ENABLE_OBJC_WEAK = YES;
794 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
795 | CLANG_WARN_BOOL_CONVERSION = YES;
796 | CLANG_WARN_COMMA = YES;
797 | CLANG_WARN_CONSTANT_CONVERSION = YES;
798 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
799 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
800 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
801 | CLANG_WARN_EMPTY_BODY = YES;
802 | CLANG_WARN_ENUM_CONVERSION = YES;
803 | CLANG_WARN_INFINITE_RECURSION = YES;
804 | CLANG_WARN_INT_CONVERSION = YES;
805 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
806 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
807 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
808 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
809 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
810 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
811 | CLANG_WARN_STRICT_PROTOTYPES = YES;
812 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
813 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
814 | CLANG_WARN_UNREACHABLE_CODE = YES;
815 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
816 | COPY_PHASE_STRIP = NO;
817 | DEBUG_INFORMATION_FORMAT = dwarf;
818 | ENABLE_STRICT_OBJC_MSGSEND = YES;
819 | ENABLE_TESTABILITY = YES;
820 | GCC_C_LANGUAGE_STANDARD = gnu11;
821 | GCC_DYNAMIC_NO_PIC = NO;
822 | GCC_NO_COMMON_BLOCKS = YES;
823 | GCC_OPTIMIZATION_LEVEL = 0;
824 | GCC_PREPROCESSOR_DEFINITIONS = (
825 | "DEBUG=1",
826 | "$(inherited)",
827 | );
828 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
829 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
830 | GCC_WARN_UNDECLARED_SELECTOR = YES;
831 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
832 | GCC_WARN_UNUSED_FUNCTION = YES;
833 | GCC_WARN_UNUSED_VARIABLE = YES;
834 | IPHONEOS_DEPLOYMENT_TARGET = 15.4;
835 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
836 | MTL_FAST_MATH = YES;
837 | ONLY_ACTIVE_ARCH = YES;
838 | SDKROOT = iphoneos;
839 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
840 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
841 | };
842 | name = Debug;
843 | };
844 | 022B6238285A3FAA0088A1D2 /* Release */ = {
845 | isa = XCBuildConfiguration;
846 | buildSettings = {
847 | ALWAYS_SEARCH_USER_PATHS = NO;
848 | CLANG_ANALYZER_NONNULL = YES;
849 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
850 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
851 | CLANG_ENABLE_MODULES = YES;
852 | CLANG_ENABLE_OBJC_ARC = YES;
853 | CLANG_ENABLE_OBJC_WEAK = YES;
854 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
855 | CLANG_WARN_BOOL_CONVERSION = YES;
856 | CLANG_WARN_COMMA = YES;
857 | CLANG_WARN_CONSTANT_CONVERSION = YES;
858 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
859 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
860 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
861 | CLANG_WARN_EMPTY_BODY = YES;
862 | CLANG_WARN_ENUM_CONVERSION = YES;
863 | CLANG_WARN_INFINITE_RECURSION = YES;
864 | CLANG_WARN_INT_CONVERSION = YES;
865 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
866 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
867 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
868 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
869 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
870 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
871 | CLANG_WARN_STRICT_PROTOTYPES = YES;
872 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
873 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
874 | CLANG_WARN_UNREACHABLE_CODE = YES;
875 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
876 | COPY_PHASE_STRIP = NO;
877 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
878 | ENABLE_NS_ASSERTIONS = NO;
879 | ENABLE_STRICT_OBJC_MSGSEND = YES;
880 | GCC_C_LANGUAGE_STANDARD = gnu11;
881 | GCC_NO_COMMON_BLOCKS = YES;
882 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
883 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
884 | GCC_WARN_UNDECLARED_SELECTOR = YES;
885 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
886 | GCC_WARN_UNUSED_FUNCTION = YES;
887 | GCC_WARN_UNUSED_VARIABLE = YES;
888 | IPHONEOS_DEPLOYMENT_TARGET = 15.4;
889 | MTL_ENABLE_DEBUG_INFO = NO;
890 | MTL_FAST_MATH = YES;
891 | SDKROOT = iphoneos;
892 | SWIFT_COMPILATION_MODE = wholemodule;
893 | SWIFT_OPTIMIZATION_LEVEL = "-O";
894 | VALIDATE_PRODUCT = YES;
895 | };
896 | name = Release;
897 | };
898 | 022B623A285A3FAA0088A1D2 /* Debug */ = {
899 | isa = XCBuildConfiguration;
900 | buildSettings = {
901 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
902 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
903 | CODE_SIGN_STYLE = Automatic;
904 | CURRENT_PROJECT_VERSION = 1;
905 | DEVELOPMENT_TEAM = 3KZWVBAJSN;
906 | EXCLUDED_ARCHS = "";
907 | GENERATE_INFOPLIST_FILE = YES;
908 | INFOPLIST_FILE = GithubRepository/Info.plist;
909 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
910 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
911 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
912 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
913 | IPHONEOS_DEPLOYMENT_TARGET = 15.4;
914 | LD_RUNPATH_SEARCH_PATHS = (
915 | "$(inherited)",
916 | "@executable_path/Frameworks",
917 | );
918 | MARKETING_VERSION = 1.0;
919 | PRODUCT_BUNDLE_IDENTIFIER = gravityapp.GithubRepository;
920 | PRODUCT_NAME = "$(TARGET_NAME)";
921 | SWIFT_EMIT_LOC_STRINGS = YES;
922 | SWIFT_VERSION = 5.0;
923 | TARGETED_DEVICE_FAMILY = 1;
924 | };
925 | name = Debug;
926 | };
927 | 022B623B285A3FAA0088A1D2 /* Release */ = {
928 | isa = XCBuildConfiguration;
929 | buildSettings = {
930 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
931 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
932 | CODE_SIGN_STYLE = Automatic;
933 | CURRENT_PROJECT_VERSION = 1;
934 | DEVELOPMENT_TEAM = 3KZWVBAJSN;
935 | EXCLUDED_ARCHS = "";
936 | GENERATE_INFOPLIST_FILE = YES;
937 | INFOPLIST_FILE = GithubRepository/Info.plist;
938 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
939 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
940 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
941 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
942 | IPHONEOS_DEPLOYMENT_TARGET = 15.4;
943 | LD_RUNPATH_SEARCH_PATHS = (
944 | "$(inherited)",
945 | "@executable_path/Frameworks",
946 | );
947 | MARKETING_VERSION = 1.0;
948 | PRODUCT_BUNDLE_IDENTIFIER = gravityapp.GithubRepository;
949 | PRODUCT_NAME = "$(TARGET_NAME)";
950 | SWIFT_EMIT_LOC_STRINGS = YES;
951 | SWIFT_VERSION = 5.0;
952 | TARGETED_DEVICE_FAMILY = 1;
953 | };
954 | name = Release;
955 | };
956 | 022B623D285A3FAA0088A1D2 /* Debug */ = {
957 | isa = XCBuildConfiguration;
958 | buildSettings = {
959 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
960 | BUNDLE_LOADER = "$(TEST_HOST)";
961 | CLANG_ENABLE_MODULES = YES;
962 | CODE_SIGN_STYLE = Automatic;
963 | CURRENT_PROJECT_VERSION = 1;
964 | DEVELOPMENT_TEAM = 3KZWVBAJSN;
965 | GENERATE_INFOPLIST_FILE = YES;
966 | IPHONEOS_DEPLOYMENT_TARGET = 15.4;
967 | LD_RUNPATH_SEARCH_PATHS = (
968 | "$(inherited)",
969 | "@executable_path/Frameworks",
970 | "@loader_path/Frameworks",
971 | );
972 | MARKETING_VERSION = 1.0;
973 | PRODUCT_BUNDLE_IDENTIFIER = gravityapp.GithubRepositoryTests;
974 | PRODUCT_NAME = "$(TARGET_NAME)";
975 | SWIFT_EMIT_LOC_STRINGS = NO;
976 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
977 | SWIFT_VERSION = 5.0;
978 | TARGETED_DEVICE_FAMILY = "1,2";
979 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GithubRepository.app/GithubRepository";
980 | };
981 | name = Debug;
982 | };
983 | 022B623E285A3FAA0088A1D2 /* Release */ = {
984 | isa = XCBuildConfiguration;
985 | buildSettings = {
986 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
987 | BUNDLE_LOADER = "$(TEST_HOST)";
988 | CLANG_ENABLE_MODULES = YES;
989 | CODE_SIGN_STYLE = Automatic;
990 | CURRENT_PROJECT_VERSION = 1;
991 | DEVELOPMENT_TEAM = 3KZWVBAJSN;
992 | GENERATE_INFOPLIST_FILE = YES;
993 | IPHONEOS_DEPLOYMENT_TARGET = 15.4;
994 | LD_RUNPATH_SEARCH_PATHS = (
995 | "$(inherited)",
996 | "@executable_path/Frameworks",
997 | "@loader_path/Frameworks",
998 | );
999 | MARKETING_VERSION = 1.0;
1000 | PRODUCT_BUNDLE_IDENTIFIER = gravityapp.GithubRepositoryTests;
1001 | PRODUCT_NAME = "$(TARGET_NAME)";
1002 | SWIFT_EMIT_LOC_STRINGS = NO;
1003 | SWIFT_VERSION = 5.0;
1004 | TARGETED_DEVICE_FAMILY = "1,2";
1005 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GithubRepository.app/GithubRepository";
1006 | };
1007 | name = Release;
1008 | };
1009 | 022B6240285A3FAA0088A1D2 /* Debug */ = {
1010 | isa = XCBuildConfiguration;
1011 | buildSettings = {
1012 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
1013 | CODE_SIGN_STYLE = Automatic;
1014 | CURRENT_PROJECT_VERSION = 1;
1015 | DEVELOPMENT_TEAM = 3KZWVBAJSN;
1016 | GENERATE_INFOPLIST_FILE = YES;
1017 | MARKETING_VERSION = 1.0;
1018 | PRODUCT_BUNDLE_IDENTIFIER = gravityapp.GithubRepositoryUITests;
1019 | PRODUCT_NAME = "$(TARGET_NAME)";
1020 | SWIFT_EMIT_LOC_STRINGS = NO;
1021 | SWIFT_VERSION = 5.0;
1022 | TARGETED_DEVICE_FAMILY = "1,2";
1023 | TEST_TARGET_NAME = GithubRepository;
1024 | };
1025 | name = Debug;
1026 | };
1027 | 022B6241285A3FAA0088A1D2 /* Release */ = {
1028 | isa = XCBuildConfiguration;
1029 | buildSettings = {
1030 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
1031 | CODE_SIGN_STYLE = Automatic;
1032 | CURRENT_PROJECT_VERSION = 1;
1033 | DEVELOPMENT_TEAM = 3KZWVBAJSN;
1034 | GENERATE_INFOPLIST_FILE = YES;
1035 | MARKETING_VERSION = 1.0;
1036 | PRODUCT_BUNDLE_IDENTIFIER = gravityapp.GithubRepositoryUITests;
1037 | PRODUCT_NAME = "$(TARGET_NAME)";
1038 | SWIFT_EMIT_LOC_STRINGS = NO;
1039 | SWIFT_VERSION = 5.0;
1040 | TARGETED_DEVICE_FAMILY = "1,2";
1041 | TEST_TARGET_NAME = GithubRepository;
1042 | };
1043 | name = Release;
1044 | };
1045 | /* End XCBuildConfiguration section */
1046 |
1047 | /* Begin XCConfigurationList section */
1048 | 022B620A285A3FA80088A1D2 /* Build configuration list for PBXProject "GithubRepository" */ = {
1049 | isa = XCConfigurationList;
1050 | buildConfigurations = (
1051 | 022B6237285A3FAA0088A1D2 /* Debug */,
1052 | 022B6238285A3FAA0088A1D2 /* Release */,
1053 | );
1054 | defaultConfigurationIsVisible = 0;
1055 | defaultConfigurationName = Release;
1056 | };
1057 | 022B6239285A3FAA0088A1D2 /* Build configuration list for PBXNativeTarget "GithubRepository" */ = {
1058 | isa = XCConfigurationList;
1059 | buildConfigurations = (
1060 | 022B623A285A3FAA0088A1D2 /* Debug */,
1061 | 022B623B285A3FAA0088A1D2 /* Release */,
1062 | );
1063 | defaultConfigurationIsVisible = 0;
1064 | defaultConfigurationName = Release;
1065 | };
1066 | 022B623C285A3FAA0088A1D2 /* Build configuration list for PBXNativeTarget "GithubRepositoryTests" */ = {
1067 | isa = XCConfigurationList;
1068 | buildConfigurations = (
1069 | 022B623D285A3FAA0088A1D2 /* Debug */,
1070 | 022B623E285A3FAA0088A1D2 /* Release */,
1071 | );
1072 | defaultConfigurationIsVisible = 0;
1073 | defaultConfigurationName = Release;
1074 | };
1075 | 022B623F285A3FAA0088A1D2 /* Build configuration list for PBXNativeTarget "GithubRepositoryUITests" */ = {
1076 | isa = XCConfigurationList;
1077 | buildConfigurations = (
1078 | 022B6240285A3FAA0088A1D2 /* Debug */,
1079 | 022B6241285A3FAA0088A1D2 /* Release */,
1080 | );
1081 | defaultConfigurationIsVisible = 0;
1082 | defaultConfigurationName = Release;
1083 | };
1084 | /* End XCConfigurationList section */
1085 |
1086 | /* Begin XCRemoteSwiftPackageReference section */
1087 | 022039F6285B0FEB009E2B98 /* XCRemoteSwiftPackageReference "Alamofire" */ = {
1088 | isa = XCRemoteSwiftPackageReference;
1089 | repositoryURL = "https://github.com/Alamofire/Alamofire";
1090 | requirement = {
1091 | branch = master;
1092 | kind = branch;
1093 | };
1094 | };
1095 | /* End XCRemoteSwiftPackageReference section */
1096 |
1097 | /* Begin XCSwiftPackageProductDependency section */
1098 | 022039F7285B0FEB009E2B98 /* Alamofire */ = {
1099 | isa = XCSwiftPackageProductDependency;
1100 | package = 022039F6285B0FEB009E2B98 /* XCRemoteSwiftPackageReference "Alamofire" */;
1101 | productName = Alamofire;
1102 | };
1103 | /* End XCSwiftPackageProductDependency section */
1104 | };
1105 | rootObject = 022B6207285A3FA80088A1D2 /* Project object */;
1106 | }
1107 |
--------------------------------------------------------------------------------