├── 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 | --------------------------------------------------------------------------------