├── README.md └── SolutionExample ├── SolutionExample.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcuserdata │ │ └── nikita.xcuserdatad │ │ └── UserInterfaceState.xcuserstate └── xcuserdata │ └── nikita.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ └── xcschememanagement.plist └── SolutionExample ├── App ├── AppDelegate.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ └── LaunchScreen.storyboard ├── Info.plist └── SceneDelegate.swift ├── Dialogs ├── ContentPlaceholderDisplayable │ ├── ContentPlaceholderDisplayable.swift │ └── WeakContentPlaceholderDisplayable.swift ├── LoaderDisplayable │ ├── LoaderDisplayable.swift │ └── WeakDelayLoaderDisplayable.swift └── MessageDisplayable │ ├── MessageDisplayable.swift │ └── WeakMessageDisplayable.swift ├── Modules └── MainPage │ ├── Assembly │ └── MainPageAssembly.swift │ ├── Models │ └── Employee.swift │ ├── Presenter │ └── MainPagePresenter.swift │ └── View │ ├── Cells │ └── EmployeeCell │ │ ├── EmployeeCell.swift │ │ └── EmployeeCellData.swift │ ├── MainPageViewController.swift │ └── MainPageViewViewInput.swift ├── Networking ├── CachePolice.swift ├── Client │ ├── NetworkClient.swift │ └── NetworkClientImpl.swift ├── Converter │ ├── DecodingNetworkResponseConverter.swift │ ├── NetworkResponseConverter.swift │ └── NetworkResponseConverterOf.swift ├── HttpMethod.swift ├── NetworkError.swift ├── NetworkRequest.swift └── RequestBuilder.swift ├── Presentation ├── BaseViewController.swift └── MutableViewLifecycleObserver.swift ├── Services └── MainPageService │ ├── MainPageService.swift │ ├── MainPageServiceImpl.swift │ └── Requests │ ├── CompanyRequest.swift │ └── CompanyResponse.swift ├── UIComponents ├── ContentPlaceholder │ ├── ContentPlaceholder.swift │ └── ContentPlaceholderModel.swift ├── Loader │ └── Loader.swift ├── TableView │ ├── TableView.swift │ ├── TableViewCellInput.swift │ └── TableViewModel.swift └── Toast │ └── ToastController.swift └── Utils ├── Decodable+Helpers.swift ├── GenericCodingKey.swift └── Task+Extensions.swift /README.md: -------------------------------------------------------------------------------- 1 | # Тестовое задание на позицию стажёра в iOS + разбор (информация ниже) 2 | 3 | ### Общее описание задания 4 | Написать приложение для iOS. Приложение должно состоять из одного экрана со списком. Список данных в формате JSON приложение загружает из интернета по [ссылке](https://run.mocky.io/v3/1d1cb4ec-73db-4762-8c4b-0b8aa3cecd4c), необходимо распарсить эти данные и отобразить их в списке. 5 | 6 | [Пример](https://github.com/avito-tech/ios-trainee-problem-2021/blob/main/response_example.json) возвращаемых данных. 7 | 8 | ### Требование к реализации: 9 | - Приложение работает на iOS 13 и выше 10 | - Реализована поддержка iPhone и iPad 11 | - Список отсортирован по алфавиту 12 | - Кэширование ответа на 1 час 13 | - Обработаны случаи потери сети / отсутствия соединения 14 | 15 | Внешний вид приложения: по возможности, лаконичный, но, в целом, на усмотрение кандидата. 16 | 17 | ### Требования к коду: 18 | - Приложение написано на языке Swift 19 | - Пользовательский интерфейс приложения настроен в InterfaceBuilder (в Storiboard или Xib файлы) или кодом без использования SwiftUI 20 | - Для отображения списка используется UITableView, либо UICollectionView 21 | - Для запроса данных используется URLSession 22 | 23 | ### Требования к передаче результатов: 24 | - Код должен быть выложен в git-репозиторий на [github.com](http://github.com/) и отправлен нам. 25 | 26 | ### Разбор тестового задания: 27 | Пример решения тестового задания можно найти в папке `SolutionExample`. Чтобы его запустить достаточно вызвать `.xcodeproj`. 28 | 29 | Чуть подробнее про само решение: 30 | - Для отображения списка используется UITableView (см. `Modules/MainPage`) 31 | - Кэширование данных реализовано с помощью связки `URLCache` + `UserDefaults` (см. `Networking/Client/NetworkClientImpl.swift`) 32 | - Обработка случаем потери сети / отсутствия соединения так же находится в файле `NetworkClientImpl.swift` 33 | - Работа с многопоточкой происходит через `async\await` 34 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 114E6C8229C8ABB900FCCD52 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 114E6C8129C8ABB900FCCD52 /* AppDelegate.swift */; }; 11 | 114E6C8429C8ABB900FCCD52 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 114E6C8329C8ABB900FCCD52 /* SceneDelegate.swift */; }; 12 | 114E6C8B29C8ABBB00FCCD52 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 114E6C8A29C8ABBB00FCCD52 /* Assets.xcassets */; }; 13 | 114E6C8E29C8ABBB00FCCD52 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 114E6C8C29C8ABBB00FCCD52 /* LaunchScreen.storyboard */; }; 14 | 114E90D229C9B50000FCCD52 /* NetworkRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 114E90D129C9B50000FCCD52 /* NetworkRequest.swift */; }; 15 | 114E90D429C9B71000FCCD52 /* NetworkClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 114E90D329C9B71000FCCD52 /* NetworkClient.swift */; }; 16 | 114E90D629C9BC5800FCCD52 /* RequestBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 114E90D529C9BC5800FCCD52 /* RequestBuilder.swift */; }; 17 | 114E90D929C9BDFA00FCCD52 /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 114E90D829C9BDFA00FCCD52 /* NetworkError.swift */; }; 18 | 114E90DD29C9CF7B00FCCD52 /* HttpMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 114E90DC29C9CF7B00FCCD52 /* HttpMethod.swift */; }; 19 | 114E90DF29C9D02F00FCCD52 /* NetworkResponseConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 114E90DE29C9D02F00FCCD52 /* NetworkResponseConverter.swift */; }; 20 | 114E90E229C9D61800FCCD52 /* DecodingNetworkResponseConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 114E90E129C9D61800FCCD52 /* DecodingNetworkResponseConverter.swift */; }; 21 | 114E90E429C9D68400FCCD52 /* NetworkResponseConverterOf.swift in Sources */ = {isa = PBXBuildFile; fileRef = 114E90E329C9D68400FCCD52 /* NetworkResponseConverterOf.swift */; }; 22 | 114E90E729C9D70700FCCD52 /* NetworkClientImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 114E90E629C9D70700FCCD52 /* NetworkClientImpl.swift */; }; 23 | 114E90E929C9D7A600FCCD52 /* CachePolice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 114E90E829C9D7A500FCCD52 /* CachePolice.swift */; }; 24 | 114E90F429C9EF8400FCCD52 /* MainPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 114E90EC29C9EF8400FCCD52 /* MainPageViewController.swift */; }; 25 | 114E90F629C9EF8400FCCD52 /* MainPagePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 114E90EE29C9EF8400FCCD52 /* MainPagePresenter.swift */; }; 26 | 114E910129C9F07F00FCCD52 /* MainPageViewViewInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 114E910029C9F07F00FCCD52 /* MainPageViewViewInput.swift */; }; 27 | 114E910529C9F12400FCCD52 /* MainPageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 114E910429C9F12400FCCD52 /* MainPageService.swift */; }; 28 | 114E910829C9F18300FCCD52 /* CompanyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 114E910729C9F18300FCCD52 /* CompanyRequest.swift */; }; 29 | 114E910A29C9F18D00FCCD52 /* CompanyResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 114E910929C9F18D00FCCD52 /* CompanyResponse.swift */; }; 30 | 114E910C29C9F1F100FCCD52 /* MainPageServiceImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 114E910B29C9F1F100FCCD52 /* MainPageServiceImpl.swift */; }; 31 | 114E910E29C9F28300FCCD52 /* MainPageAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 114E910D29C9F28300FCCD52 /* MainPageAssembly.swift */; }; 32 | 114E911229C9F47600FCCD52 /* LoaderDisplayable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 114E911129C9F47600FCCD52 /* LoaderDisplayable.swift */; }; 33 | 114E911429C9F47E00FCCD52 /* WeakDelayLoaderDisplayable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 114E911329C9F47E00FCCD52 /* WeakDelayLoaderDisplayable.swift */; }; 34 | 114E911729C9F4F800FCCD52 /* ContentPlaceholderDisplayable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 114E911629C9F4F800FCCD52 /* ContentPlaceholderDisplayable.swift */; }; 35 | 114E911929C9F50000FCCD52 /* WeakContentPlaceholderDisplayable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 114E911829C9F50000FCCD52 /* WeakContentPlaceholderDisplayable.swift */; }; 36 | 114E911E29C9F6CD00FCCD52 /* ContentPlaceholder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 114E911D29C9F6CD00FCCD52 /* ContentPlaceholder.swift */; }; 37 | 114E912029C9F6D800FCCD52 /* ContentPlaceholderModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 114E911F29C9F6D800FCCD52 /* ContentPlaceholderModel.swift */; }; 38 | 114E912629C9F9FC00FCCD52 /* Employee.swift in Sources */ = {isa = PBXBuildFile; fileRef = 114E912529C9F9FC00FCCD52 /* Employee.swift */; }; 39 | 114E912829C9FD3D00FCCD52 /* Loader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 114E912729C9FD3D00FCCD52 /* Loader.swift */; }; 40 | 114E912B29C9FFB200FCCD52 /* TableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 114E912A29C9FFB200FCCD52 /* TableView.swift */; }; 41 | 114E912D29CA015400FCCD52 /* TableViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 114E912C29CA015400FCCD52 /* TableViewModel.swift */; }; 42 | 114E912F29CA032900FCCD52 /* TableViewCellInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 114E912E29CA032900FCCD52 /* TableViewCellInput.swift */; }; 43 | 114E913329CA053B00FCCD52 /* EmployeeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 114E913229CA053B00FCCD52 /* EmployeeCell.swift */; }; 44 | 114E913529CA054100FCCD52 /* EmployeeCellData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 114E913429CA054100FCCD52 /* EmployeeCellData.swift */; }; 45 | 114E913829CA076D00FCCD52 /* BaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 114E913729CA076D00FCCD52 /* BaseViewController.swift */; }; 46 | 114E913A29CA078100FCCD52 /* MutableViewLifecycleObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 114E913929CA078100FCCD52 /* MutableViewLifecycleObserver.swift */; }; 47 | 114E913D29CA0F2500FCCD52 /* GenericCodingKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 114E913C29CA0F2500FCCD52 /* GenericCodingKey.swift */; }; 48 | 114E913F29CA0FB900FCCD52 /* Decodable+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 114E913E29CA0FB900FCCD52 /* Decodable+Helpers.swift */; }; 49 | 114E914629CB1C9500FCCD52 /* MessageDisplayable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 114E914529CB1C9500FCCD52 /* MessageDisplayable.swift */; }; 50 | 114E914829CB1C9C00FCCD52 /* WeakMessageDisplayable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 114E914729CB1C9C00FCCD52 /* WeakMessageDisplayable.swift */; }; 51 | 114E914A29CB1D7D00FCCD52 /* Task+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 114E914929CB1D7D00FCCD52 /* Task+Extensions.swift */; }; 52 | 11765F2D29CB2DC800B2B279 /* ToastController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11765F2C29CB2DC800B2B279 /* ToastController.swift */; }; 53 | /* End PBXBuildFile section */ 54 | 55 | /* Begin PBXFileReference section */ 56 | 114E6C7E29C8ABB900FCCD52 /* SolutionExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SolutionExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 57 | 114E6C8129C8ABB900FCCD52 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 58 | 114E6C8329C8ABB900FCCD52 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 59 | 114E6C8A29C8ABBB00FCCD52 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 60 | 114E6C8D29C8ABBB00FCCD52 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 61 | 114E6C8F29C8ABBB00FCCD52 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 62 | 114E90D129C9B50000FCCD52 /* NetworkRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkRequest.swift; sourceTree = ""; }; 63 | 114E90D329C9B71000FCCD52 /* NetworkClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkClient.swift; sourceTree = ""; }; 64 | 114E90D529C9BC5800FCCD52 /* RequestBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestBuilder.swift; sourceTree = ""; }; 65 | 114E90D829C9BDFA00FCCD52 /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = ""; }; 66 | 114E90DC29C9CF7B00FCCD52 /* HttpMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HttpMethod.swift; sourceTree = ""; }; 67 | 114E90DE29C9D02F00FCCD52 /* NetworkResponseConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkResponseConverter.swift; sourceTree = ""; }; 68 | 114E90E129C9D61800FCCD52 /* DecodingNetworkResponseConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecodingNetworkResponseConverter.swift; sourceTree = ""; }; 69 | 114E90E329C9D68400FCCD52 /* NetworkResponseConverterOf.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkResponseConverterOf.swift; sourceTree = ""; }; 70 | 114E90E629C9D70700FCCD52 /* NetworkClientImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkClientImpl.swift; sourceTree = ""; }; 71 | 114E90E829C9D7A500FCCD52 /* CachePolice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachePolice.swift; sourceTree = ""; }; 72 | 114E90EC29C9EF8400FCCD52 /* MainPageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainPageViewController.swift; sourceTree = ""; }; 73 | 114E90EE29C9EF8400FCCD52 /* MainPagePresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainPagePresenter.swift; sourceTree = ""; }; 74 | 114E910029C9F07F00FCCD52 /* MainPageViewViewInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainPageViewViewInput.swift; sourceTree = ""; }; 75 | 114E910429C9F12400FCCD52 /* MainPageService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainPageService.swift; sourceTree = ""; }; 76 | 114E910729C9F18300FCCD52 /* CompanyRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompanyRequest.swift; sourceTree = ""; }; 77 | 114E910929C9F18D00FCCD52 /* CompanyResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompanyResponse.swift; sourceTree = ""; }; 78 | 114E910B29C9F1F100FCCD52 /* MainPageServiceImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainPageServiceImpl.swift; sourceTree = ""; }; 79 | 114E910D29C9F28300FCCD52 /* MainPageAssembly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainPageAssembly.swift; sourceTree = ""; }; 80 | 114E911129C9F47600FCCD52 /* LoaderDisplayable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoaderDisplayable.swift; sourceTree = ""; }; 81 | 114E911329C9F47E00FCCD52 /* WeakDelayLoaderDisplayable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakDelayLoaderDisplayable.swift; sourceTree = ""; }; 82 | 114E911629C9F4F800FCCD52 /* ContentPlaceholderDisplayable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentPlaceholderDisplayable.swift; sourceTree = ""; }; 83 | 114E911829C9F50000FCCD52 /* WeakContentPlaceholderDisplayable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakContentPlaceholderDisplayable.swift; sourceTree = ""; }; 84 | 114E911D29C9F6CD00FCCD52 /* ContentPlaceholder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentPlaceholder.swift; sourceTree = ""; }; 85 | 114E911F29C9F6D800FCCD52 /* ContentPlaceholderModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentPlaceholderModel.swift; sourceTree = ""; }; 86 | 114E912529C9F9FC00FCCD52 /* Employee.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Employee.swift; sourceTree = ""; }; 87 | 114E912729C9FD3D00FCCD52 /* Loader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Loader.swift; sourceTree = ""; }; 88 | 114E912A29C9FFB200FCCD52 /* TableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableView.swift; sourceTree = ""; }; 89 | 114E912C29CA015400FCCD52 /* TableViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewModel.swift; sourceTree = ""; }; 90 | 114E912E29CA032900FCCD52 /* TableViewCellInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewCellInput.swift; sourceTree = ""; }; 91 | 114E913229CA053B00FCCD52 /* EmployeeCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmployeeCell.swift; sourceTree = ""; }; 92 | 114E913429CA054100FCCD52 /* EmployeeCellData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmployeeCellData.swift; sourceTree = ""; }; 93 | 114E913729CA076D00FCCD52 /* BaseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseViewController.swift; sourceTree = ""; }; 94 | 114E913929CA078100FCCD52 /* MutableViewLifecycleObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutableViewLifecycleObserver.swift; sourceTree = ""; }; 95 | 114E913C29CA0F2500FCCD52 /* GenericCodingKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GenericCodingKey.swift; sourceTree = ""; }; 96 | 114E913E29CA0FB900FCCD52 /* Decodable+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Decodable+Helpers.swift"; sourceTree = ""; }; 97 | 114E914529CB1C9500FCCD52 /* MessageDisplayable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageDisplayable.swift; sourceTree = ""; }; 98 | 114E914729CB1C9C00FCCD52 /* WeakMessageDisplayable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakMessageDisplayable.swift; sourceTree = ""; }; 99 | 114E914929CB1D7D00FCCD52 /* Task+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Task+Extensions.swift"; sourceTree = ""; }; 100 | 11765F2C29CB2DC800B2B279 /* ToastController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastController.swift; sourceTree = ""; }; 101 | /* End PBXFileReference section */ 102 | 103 | /* Begin PBXFrameworksBuildPhase section */ 104 | 114E6C7B29C8ABB900FCCD52 /* Frameworks */ = { 105 | isa = PBXFrameworksBuildPhase; 106 | buildActionMask = 2147483647; 107 | files = ( 108 | ); 109 | runOnlyForDeploymentPostprocessing = 0; 110 | }; 111 | /* End PBXFrameworksBuildPhase section */ 112 | 113 | /* Begin PBXGroup section */ 114 | 114E6C7529C8ABB900FCCD52 = { 115 | isa = PBXGroup; 116 | children = ( 117 | 114E6C8029C8ABB900FCCD52 /* SolutionExample */, 118 | 114E6C7F29C8ABB900FCCD52 /* Products */, 119 | ); 120 | sourceTree = ""; 121 | }; 122 | 114E6C7F29C8ABB900FCCD52 /* Products */ = { 123 | isa = PBXGroup; 124 | children = ( 125 | 114E6C7E29C8ABB900FCCD52 /* SolutionExample.app */, 126 | ); 127 | name = Products; 128 | sourceTree = ""; 129 | }; 130 | 114E6C8029C8ABB900FCCD52 /* SolutionExample */ = { 131 | isa = PBXGroup; 132 | children = ( 133 | 114E913B29CA0EFC00FCCD52 /* Utils */, 134 | 114E913629CA076100FCCD52 /* Presentation */, 135 | 114E911A29C9F6A500FCCD52 /* UIComponents */, 136 | 114E910F29C9F45600FCCD52 /* Dialogs */, 137 | 114E910229C9F0F500FCCD52 /* Services */, 138 | 114E90EA29C9EF8400FCCD52 /* Modules */, 139 | 114E90D029C9B4CC00FCCD52 /* Networking */, 140 | 114E6C9629C8ADF800FCCD52 /* App */, 141 | ); 142 | path = SolutionExample; 143 | sourceTree = ""; 144 | }; 145 | 114E6C9629C8ADF800FCCD52 /* App */ = { 146 | isa = PBXGroup; 147 | children = ( 148 | 114E6C8A29C8ABBB00FCCD52 /* Assets.xcassets */, 149 | 114E6C8129C8ABB900FCCD52 /* AppDelegate.swift */, 150 | 114E6C8329C8ABB900FCCD52 /* SceneDelegate.swift */, 151 | 114E6C8C29C8ABBB00FCCD52 /* LaunchScreen.storyboard */, 152 | 114E6C8F29C8ABBB00FCCD52 /* Info.plist */, 153 | ); 154 | path = App; 155 | sourceTree = ""; 156 | }; 157 | 114E90D029C9B4CC00FCCD52 /* Networking */ = { 158 | isa = PBXGroup; 159 | children = ( 160 | 114E90E529C9D6E400FCCD52 /* Client */, 161 | 114E90E029C9D60300FCCD52 /* Converter */, 162 | 114E90D129C9B50000FCCD52 /* NetworkRequest.swift */, 163 | 114E90D529C9BC5800FCCD52 /* RequestBuilder.swift */, 164 | 114E90D829C9BDFA00FCCD52 /* NetworkError.swift */, 165 | 114E90DC29C9CF7B00FCCD52 /* HttpMethod.swift */, 166 | 114E90E829C9D7A500FCCD52 /* CachePolice.swift */, 167 | ); 168 | path = Networking; 169 | sourceTree = ""; 170 | }; 171 | 114E90E029C9D60300FCCD52 /* Converter */ = { 172 | isa = PBXGroup; 173 | children = ( 174 | 114E90DE29C9D02F00FCCD52 /* NetworkResponseConverter.swift */, 175 | 114E90E129C9D61800FCCD52 /* DecodingNetworkResponseConverter.swift */, 176 | 114E90E329C9D68400FCCD52 /* NetworkResponseConverterOf.swift */, 177 | ); 178 | path = Converter; 179 | sourceTree = ""; 180 | }; 181 | 114E90E529C9D6E400FCCD52 /* Client */ = { 182 | isa = PBXGroup; 183 | children = ( 184 | 114E90D329C9B71000FCCD52 /* NetworkClient.swift */, 185 | 114E90E629C9D70700FCCD52 /* NetworkClientImpl.swift */, 186 | ); 187 | path = Client; 188 | sourceTree = ""; 189 | }; 190 | 114E90EA29C9EF8400FCCD52 /* Modules */ = { 191 | isa = PBXGroup; 192 | children = ( 193 | 114E90EB29C9EF8400FCCD52 /* MainPage */, 194 | ); 195 | path = Modules; 196 | sourceTree = ""; 197 | }; 198 | 114E90EB29C9EF8400FCCD52 /* MainPage */ = { 199 | isa = PBXGroup; 200 | children = ( 201 | 114E912429C9F9F200FCCD52 /* Models */, 202 | 114E90FD29C9EFDD00FCCD52 /* Assembly */, 203 | 114E90FC29C9EFC600FCCD52 /* Presenter */, 204 | 114E90FB29C9EFBF00FCCD52 /* View */, 205 | ); 206 | path = MainPage; 207 | sourceTree = ""; 208 | }; 209 | 114E90FB29C9EFBF00FCCD52 /* View */ = { 210 | isa = PBXGroup; 211 | children = ( 212 | 114E913029CA052F00FCCD52 /* Cells */, 213 | 114E90EC29C9EF8400FCCD52 /* MainPageViewController.swift */, 214 | 114E910029C9F07F00FCCD52 /* MainPageViewViewInput.swift */, 215 | ); 216 | path = View; 217 | sourceTree = ""; 218 | }; 219 | 114E90FC29C9EFC600FCCD52 /* Presenter */ = { 220 | isa = PBXGroup; 221 | children = ( 222 | 114E90EE29C9EF8400FCCD52 /* MainPagePresenter.swift */, 223 | ); 224 | path = Presenter; 225 | sourceTree = ""; 226 | }; 227 | 114E90FD29C9EFDD00FCCD52 /* Assembly */ = { 228 | isa = PBXGroup; 229 | children = ( 230 | 114E910D29C9F28300FCCD52 /* MainPageAssembly.swift */, 231 | ); 232 | path = Assembly; 233 | sourceTree = ""; 234 | }; 235 | 114E910229C9F0F500FCCD52 /* Services */ = { 236 | isa = PBXGroup; 237 | children = ( 238 | 114E910329C9F10500FCCD52 /* MainPageService */, 239 | ); 240 | path = Services; 241 | sourceTree = ""; 242 | }; 243 | 114E910329C9F10500FCCD52 /* MainPageService */ = { 244 | isa = PBXGroup; 245 | children = ( 246 | 114E910629C9F17300FCCD52 /* Requests */, 247 | 114E910429C9F12400FCCD52 /* MainPageService.swift */, 248 | 114E910B29C9F1F100FCCD52 /* MainPageServiceImpl.swift */, 249 | ); 250 | path = MainPageService; 251 | sourceTree = ""; 252 | }; 253 | 114E910629C9F17300FCCD52 /* Requests */ = { 254 | isa = PBXGroup; 255 | children = ( 256 | 114E910729C9F18300FCCD52 /* CompanyRequest.swift */, 257 | 114E910929C9F18D00FCCD52 /* CompanyResponse.swift */, 258 | ); 259 | path = Requests; 260 | sourceTree = ""; 261 | }; 262 | 114E910F29C9F45600FCCD52 /* Dialogs */ = { 263 | isa = PBXGroup; 264 | children = ( 265 | 114E914429CB1C8700FCCD52 /* MessageDisplayable */, 266 | 114E911529C9F4E700FCCD52 /* ContentPlaceholderDisplayable */, 267 | 114E911029C9F46500FCCD52 /* LoaderDisplayable */, 268 | ); 269 | path = Dialogs; 270 | sourceTree = ""; 271 | }; 272 | 114E911029C9F46500FCCD52 /* LoaderDisplayable */ = { 273 | isa = PBXGroup; 274 | children = ( 275 | 114E911129C9F47600FCCD52 /* LoaderDisplayable.swift */, 276 | 114E911329C9F47E00FCCD52 /* WeakDelayLoaderDisplayable.swift */, 277 | ); 278 | path = LoaderDisplayable; 279 | sourceTree = ""; 280 | }; 281 | 114E911529C9F4E700FCCD52 /* ContentPlaceholderDisplayable */ = { 282 | isa = PBXGroup; 283 | children = ( 284 | 114E911629C9F4F800FCCD52 /* ContentPlaceholderDisplayable.swift */, 285 | 114E911829C9F50000FCCD52 /* WeakContentPlaceholderDisplayable.swift */, 286 | ); 287 | path = ContentPlaceholderDisplayable; 288 | sourceTree = ""; 289 | }; 290 | 114E911A29C9F6A500FCCD52 /* UIComponents */ = { 291 | isa = PBXGroup; 292 | children = ( 293 | 11765F2B29CB2DC000B2B279 /* Toast */, 294 | 114E912929C9FF9700FCCD52 /* TableView */, 295 | 114E911C29C9F6B900FCCD52 /* ContentPlaceholder */, 296 | 114E911B29C9F6B100FCCD52 /* Loader */, 297 | ); 298 | path = UIComponents; 299 | sourceTree = ""; 300 | }; 301 | 114E911B29C9F6B100FCCD52 /* Loader */ = { 302 | isa = PBXGroup; 303 | children = ( 304 | 114E912729C9FD3D00FCCD52 /* Loader.swift */, 305 | ); 306 | path = Loader; 307 | sourceTree = ""; 308 | }; 309 | 114E911C29C9F6B900FCCD52 /* ContentPlaceholder */ = { 310 | isa = PBXGroup; 311 | children = ( 312 | 114E911D29C9F6CD00FCCD52 /* ContentPlaceholder.swift */, 313 | 114E911F29C9F6D800FCCD52 /* ContentPlaceholderModel.swift */, 314 | ); 315 | path = ContentPlaceholder; 316 | sourceTree = ""; 317 | }; 318 | 114E912429C9F9F200FCCD52 /* Models */ = { 319 | isa = PBXGroup; 320 | children = ( 321 | 114E912529C9F9FC00FCCD52 /* Employee.swift */, 322 | ); 323 | path = Models; 324 | sourceTree = ""; 325 | }; 326 | 114E912929C9FF9700FCCD52 /* TableView */ = { 327 | isa = PBXGroup; 328 | children = ( 329 | 114E912A29C9FFB200FCCD52 /* TableView.swift */, 330 | 114E912C29CA015400FCCD52 /* TableViewModel.swift */, 331 | 114E912E29CA032900FCCD52 /* TableViewCellInput.swift */, 332 | ); 333 | path = TableView; 334 | sourceTree = ""; 335 | }; 336 | 114E913029CA052F00FCCD52 /* Cells */ = { 337 | isa = PBXGroup; 338 | children = ( 339 | 114E913129CA053400FCCD52 /* EmployeeCell */, 340 | ); 341 | path = Cells; 342 | sourceTree = ""; 343 | }; 344 | 114E913129CA053400FCCD52 /* EmployeeCell */ = { 345 | isa = PBXGroup; 346 | children = ( 347 | 114E913229CA053B00FCCD52 /* EmployeeCell.swift */, 348 | 114E913429CA054100FCCD52 /* EmployeeCellData.swift */, 349 | ); 350 | path = EmployeeCell; 351 | sourceTree = ""; 352 | }; 353 | 114E913629CA076100FCCD52 /* Presentation */ = { 354 | isa = PBXGroup; 355 | children = ( 356 | 114E913729CA076D00FCCD52 /* BaseViewController.swift */, 357 | 114E913929CA078100FCCD52 /* MutableViewLifecycleObserver.swift */, 358 | ); 359 | path = Presentation; 360 | sourceTree = ""; 361 | }; 362 | 114E913B29CA0EFC00FCCD52 /* Utils */ = { 363 | isa = PBXGroup; 364 | children = ( 365 | 114E913C29CA0F2500FCCD52 /* GenericCodingKey.swift */, 366 | 114E913E29CA0FB900FCCD52 /* Decodable+Helpers.swift */, 367 | 114E914929CB1D7D00FCCD52 /* Task+Extensions.swift */, 368 | ); 369 | path = Utils; 370 | sourceTree = ""; 371 | }; 372 | 114E914429CB1C8700FCCD52 /* MessageDisplayable */ = { 373 | isa = PBXGroup; 374 | children = ( 375 | 114E914529CB1C9500FCCD52 /* MessageDisplayable.swift */, 376 | 114E914729CB1C9C00FCCD52 /* WeakMessageDisplayable.swift */, 377 | ); 378 | path = MessageDisplayable; 379 | sourceTree = ""; 380 | }; 381 | 11765F2B29CB2DC000B2B279 /* Toast */ = { 382 | isa = PBXGroup; 383 | children = ( 384 | 11765F2C29CB2DC800B2B279 /* ToastController.swift */, 385 | ); 386 | path = Toast; 387 | sourceTree = ""; 388 | }; 389 | /* End PBXGroup section */ 390 | 391 | /* Begin PBXNativeTarget section */ 392 | 114E6C7D29C8ABB900FCCD52 /* SolutionExample */ = { 393 | isa = PBXNativeTarget; 394 | buildConfigurationList = 114E6C9229C8ABBB00FCCD52 /* Build configuration list for PBXNativeTarget "SolutionExample" */; 395 | buildPhases = ( 396 | 114E6C7A29C8ABB900FCCD52 /* Sources */, 397 | 114E6C7B29C8ABB900FCCD52 /* Frameworks */, 398 | 114E6C7C29C8ABB900FCCD52 /* Resources */, 399 | ); 400 | buildRules = ( 401 | ); 402 | dependencies = ( 403 | ); 404 | name = SolutionExample; 405 | productName = SolutionExample; 406 | productReference = 114E6C7E29C8ABB900FCCD52 /* SolutionExample.app */; 407 | productType = "com.apple.product-type.application"; 408 | }; 409 | /* End PBXNativeTarget section */ 410 | 411 | /* Begin PBXProject section */ 412 | 114E6C7629C8ABB900FCCD52 /* Project object */ = { 413 | isa = PBXProject; 414 | attributes = { 415 | BuildIndependentTargetsInParallel = 1; 416 | LastSwiftUpdateCheck = 1420; 417 | LastUpgradeCheck = 1420; 418 | TargetAttributes = { 419 | 114E6C7D29C8ABB900FCCD52 = { 420 | CreatedOnToolsVersion = 14.2; 421 | }; 422 | }; 423 | }; 424 | buildConfigurationList = 114E6C7929C8ABB900FCCD52 /* Build configuration list for PBXProject "SolutionExample" */; 425 | compatibilityVersion = "Xcode 14.0"; 426 | developmentRegion = en; 427 | hasScannedForEncodings = 0; 428 | knownRegions = ( 429 | en, 430 | Base, 431 | ); 432 | mainGroup = 114E6C7529C8ABB900FCCD52; 433 | productRefGroup = 114E6C7F29C8ABB900FCCD52 /* Products */; 434 | projectDirPath = ""; 435 | projectRoot = ""; 436 | targets = ( 437 | 114E6C7D29C8ABB900FCCD52 /* SolutionExample */, 438 | ); 439 | }; 440 | /* End PBXProject section */ 441 | 442 | /* Begin PBXResourcesBuildPhase section */ 443 | 114E6C7C29C8ABB900FCCD52 /* Resources */ = { 444 | isa = PBXResourcesBuildPhase; 445 | buildActionMask = 2147483647; 446 | files = ( 447 | 114E6C8E29C8ABBB00FCCD52 /* LaunchScreen.storyboard in Resources */, 448 | 114E6C8B29C8ABBB00FCCD52 /* Assets.xcassets in Resources */, 449 | ); 450 | runOnlyForDeploymentPostprocessing = 0; 451 | }; 452 | /* End PBXResourcesBuildPhase section */ 453 | 454 | /* Begin PBXSourcesBuildPhase section */ 455 | 114E6C7A29C8ABB900FCCD52 /* Sources */ = { 456 | isa = PBXSourcesBuildPhase; 457 | buildActionMask = 2147483647; 458 | files = ( 459 | 114E90D929C9BDFA00FCCD52 /* NetworkError.swift in Sources */, 460 | 114E90E429C9D68400FCCD52 /* NetworkResponseConverterOf.swift in Sources */, 461 | 114E914629CB1C9500FCCD52 /* MessageDisplayable.swift in Sources */, 462 | 114E90D629C9BC5800FCCD52 /* RequestBuilder.swift in Sources */, 463 | 114E6C8229C8ABB900FCCD52 /* AppDelegate.swift in Sources */, 464 | 114E90DD29C9CF7B00FCCD52 /* HttpMethod.swift in Sources */, 465 | 114E90D229C9B50000FCCD52 /* NetworkRequest.swift in Sources */, 466 | 114E912D29CA015400FCCD52 /* TableViewModel.swift in Sources */, 467 | 114E913D29CA0F2500FCCD52 /* GenericCodingKey.swift in Sources */, 468 | 114E90F429C9EF8400FCCD52 /* MainPageViewController.swift in Sources */, 469 | 114E913329CA053B00FCCD52 /* EmployeeCell.swift in Sources */, 470 | 114E914829CB1C9C00FCCD52 /* WeakMessageDisplayable.swift in Sources */, 471 | 114E90DF29C9D02F00FCCD52 /* NetworkResponseConverter.swift in Sources */, 472 | 114E913F29CA0FB900FCCD52 /* Decodable+Helpers.swift in Sources */, 473 | 114E913829CA076D00FCCD52 /* BaseViewController.swift in Sources */, 474 | 114E913529CA054100FCCD52 /* EmployeeCellData.swift in Sources */, 475 | 114E910129C9F07F00FCCD52 /* MainPageViewViewInput.swift in Sources */, 476 | 114E911429C9F47E00FCCD52 /* WeakDelayLoaderDisplayable.swift in Sources */, 477 | 11765F2D29CB2DC800B2B279 /* ToastController.swift in Sources */, 478 | 114E90E229C9D61800FCCD52 /* DecodingNetworkResponseConverter.swift in Sources */, 479 | 114E911229C9F47600FCCD52 /* LoaderDisplayable.swift in Sources */, 480 | 114E912B29C9FFB200FCCD52 /* TableView.swift in Sources */, 481 | 114E913A29CA078100FCCD52 /* MutableViewLifecycleObserver.swift in Sources */, 482 | 114E912629C9F9FC00FCCD52 /* Employee.swift in Sources */, 483 | 114E911E29C9F6CD00FCCD52 /* ContentPlaceholder.swift in Sources */, 484 | 114E912829C9FD3D00FCCD52 /* Loader.swift in Sources */, 485 | 114E912029C9F6D800FCCD52 /* ContentPlaceholderModel.swift in Sources */, 486 | 114E914A29CB1D7D00FCCD52 /* Task+Extensions.swift in Sources */, 487 | 114E910829C9F18300FCCD52 /* CompanyRequest.swift in Sources */, 488 | 114E90F629C9EF8400FCCD52 /* MainPagePresenter.swift in Sources */, 489 | 114E6C8429C8ABB900FCCD52 /* SceneDelegate.swift in Sources */, 490 | 114E90D429C9B71000FCCD52 /* NetworkClient.swift in Sources */, 491 | 114E910529C9F12400FCCD52 /* MainPageService.swift in Sources */, 492 | 114E911929C9F50000FCCD52 /* WeakContentPlaceholderDisplayable.swift in Sources */, 493 | 114E910E29C9F28300FCCD52 /* MainPageAssembly.swift in Sources */, 494 | 114E90E929C9D7A600FCCD52 /* CachePolice.swift in Sources */, 495 | 114E911729C9F4F800FCCD52 /* ContentPlaceholderDisplayable.swift in Sources */, 496 | 114E910A29C9F18D00FCCD52 /* CompanyResponse.swift in Sources */, 497 | 114E912F29CA032900FCCD52 /* TableViewCellInput.swift in Sources */, 498 | 114E90E729C9D70700FCCD52 /* NetworkClientImpl.swift in Sources */, 499 | 114E910C29C9F1F100FCCD52 /* MainPageServiceImpl.swift in Sources */, 500 | ); 501 | runOnlyForDeploymentPostprocessing = 0; 502 | }; 503 | /* End PBXSourcesBuildPhase section */ 504 | 505 | /* Begin PBXVariantGroup section */ 506 | 114E6C8C29C8ABBB00FCCD52 /* LaunchScreen.storyboard */ = { 507 | isa = PBXVariantGroup; 508 | children = ( 509 | 114E6C8D29C8ABBB00FCCD52 /* Base */, 510 | ); 511 | name = LaunchScreen.storyboard; 512 | sourceTree = ""; 513 | }; 514 | /* End PBXVariantGroup section */ 515 | 516 | /* Begin XCBuildConfiguration section */ 517 | 114E6C9029C8ABBB00FCCD52 /* Debug */ = { 518 | isa = XCBuildConfiguration; 519 | buildSettings = { 520 | ALWAYS_SEARCH_USER_PATHS = NO; 521 | CLANG_ANALYZER_NONNULL = YES; 522 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 523 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 524 | CLANG_ENABLE_MODULES = YES; 525 | CLANG_ENABLE_OBJC_ARC = YES; 526 | CLANG_ENABLE_OBJC_WEAK = YES; 527 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 528 | CLANG_WARN_BOOL_CONVERSION = YES; 529 | CLANG_WARN_COMMA = YES; 530 | CLANG_WARN_CONSTANT_CONVERSION = YES; 531 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 532 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 533 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 534 | CLANG_WARN_EMPTY_BODY = YES; 535 | CLANG_WARN_ENUM_CONVERSION = YES; 536 | CLANG_WARN_INFINITE_RECURSION = YES; 537 | CLANG_WARN_INT_CONVERSION = YES; 538 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 539 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 540 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 541 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 542 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 543 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 544 | CLANG_WARN_STRICT_PROTOTYPES = YES; 545 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 546 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 547 | CLANG_WARN_UNREACHABLE_CODE = YES; 548 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 549 | COPY_PHASE_STRIP = NO; 550 | DEBUG_INFORMATION_FORMAT = dwarf; 551 | ENABLE_STRICT_OBJC_MSGSEND = YES; 552 | ENABLE_TESTABILITY = YES; 553 | GCC_C_LANGUAGE_STANDARD = gnu11; 554 | GCC_DYNAMIC_NO_PIC = NO; 555 | GCC_NO_COMMON_BLOCKS = YES; 556 | GCC_OPTIMIZATION_LEVEL = 0; 557 | GCC_PREPROCESSOR_DEFINITIONS = ( 558 | "DEBUG=1", 559 | "$(inherited)", 560 | ); 561 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 562 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 563 | GCC_WARN_UNDECLARED_SELECTOR = YES; 564 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 565 | GCC_WARN_UNUSED_FUNCTION = YES; 566 | GCC_WARN_UNUSED_VARIABLE = YES; 567 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 568 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 569 | MTL_FAST_MATH = YES; 570 | ONLY_ACTIVE_ARCH = YES; 571 | SDKROOT = iphoneos; 572 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 573 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 574 | }; 575 | name = Debug; 576 | }; 577 | 114E6C9129C8ABBB00FCCD52 /* Release */ = { 578 | isa = XCBuildConfiguration; 579 | buildSettings = { 580 | ALWAYS_SEARCH_USER_PATHS = NO; 581 | CLANG_ANALYZER_NONNULL = YES; 582 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 583 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 584 | CLANG_ENABLE_MODULES = YES; 585 | CLANG_ENABLE_OBJC_ARC = YES; 586 | CLANG_ENABLE_OBJC_WEAK = YES; 587 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 588 | CLANG_WARN_BOOL_CONVERSION = YES; 589 | CLANG_WARN_COMMA = YES; 590 | CLANG_WARN_CONSTANT_CONVERSION = YES; 591 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 592 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 593 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 594 | CLANG_WARN_EMPTY_BODY = YES; 595 | CLANG_WARN_ENUM_CONVERSION = YES; 596 | CLANG_WARN_INFINITE_RECURSION = YES; 597 | CLANG_WARN_INT_CONVERSION = YES; 598 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 599 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 600 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 601 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 602 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 603 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 604 | CLANG_WARN_STRICT_PROTOTYPES = YES; 605 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 606 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 607 | CLANG_WARN_UNREACHABLE_CODE = YES; 608 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 609 | COPY_PHASE_STRIP = NO; 610 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 611 | ENABLE_NS_ASSERTIONS = NO; 612 | ENABLE_STRICT_OBJC_MSGSEND = YES; 613 | GCC_C_LANGUAGE_STANDARD = gnu11; 614 | GCC_NO_COMMON_BLOCKS = YES; 615 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 616 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 617 | GCC_WARN_UNDECLARED_SELECTOR = YES; 618 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 619 | GCC_WARN_UNUSED_FUNCTION = YES; 620 | GCC_WARN_UNUSED_VARIABLE = YES; 621 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 622 | MTL_ENABLE_DEBUG_INFO = NO; 623 | MTL_FAST_MATH = YES; 624 | SDKROOT = iphoneos; 625 | SWIFT_COMPILATION_MODE = wholemodule; 626 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 627 | VALIDATE_PRODUCT = YES; 628 | }; 629 | name = Release; 630 | }; 631 | 114E6C9329C8ABBB00FCCD52 /* Debug */ = { 632 | isa = XCBuildConfiguration; 633 | buildSettings = { 634 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 635 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 636 | CODE_SIGN_STYLE = Automatic; 637 | CURRENT_PROJECT_VERSION = 1; 638 | DEVELOPMENT_TEAM = MUV68JPJ7J; 639 | GENERATE_INFOPLIST_FILE = YES; 640 | INFOPLIST_FILE = SolutionExample/App/Info.plist; 641 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 642 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 643 | INFOPLIST_KEY_UIMainStoryboardFile = ""; 644 | INFOPLIST_KEY_UIRequiresFullScreen = NO; 645 | INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent; 646 | INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; 647 | INFOPLIST_KEY_UIUserInterfaceStyle = Light; 648 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 649 | LD_RUNPATH_SEARCH_PATHS = ( 650 | "$(inherited)", 651 | "@executable_path/Frameworks", 652 | ); 653 | MARKETING_VERSION = 1.0; 654 | PRODUCT_BUNDLE_IDENTIFIER = Avito.SolutionExample; 655 | PRODUCT_NAME = "$(TARGET_NAME)"; 656 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 657 | SUPPORTS_MACCATALYST = NO; 658 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 659 | SWIFT_EMIT_LOC_STRINGS = YES; 660 | SWIFT_VERSION = 5.0; 661 | TARGETED_DEVICE_FAMILY = "1,2"; 662 | }; 663 | name = Debug; 664 | }; 665 | 114E6C9429C8ABBB00FCCD52 /* Release */ = { 666 | isa = XCBuildConfiguration; 667 | buildSettings = { 668 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 669 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 670 | CODE_SIGN_STYLE = Automatic; 671 | CURRENT_PROJECT_VERSION = 1; 672 | DEVELOPMENT_TEAM = MUV68JPJ7J; 673 | GENERATE_INFOPLIST_FILE = YES; 674 | INFOPLIST_FILE = SolutionExample/App/Info.plist; 675 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 676 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 677 | INFOPLIST_KEY_UIMainStoryboardFile = ""; 678 | INFOPLIST_KEY_UIRequiresFullScreen = NO; 679 | INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleLightContent; 680 | INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; 681 | INFOPLIST_KEY_UIUserInterfaceStyle = Light; 682 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 683 | LD_RUNPATH_SEARCH_PATHS = ( 684 | "$(inherited)", 685 | "@executable_path/Frameworks", 686 | ); 687 | MARKETING_VERSION = 1.0; 688 | PRODUCT_BUNDLE_IDENTIFIER = Avito.SolutionExample; 689 | PRODUCT_NAME = "$(TARGET_NAME)"; 690 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 691 | SUPPORTS_MACCATALYST = NO; 692 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 693 | SWIFT_EMIT_LOC_STRINGS = YES; 694 | SWIFT_VERSION = 5.0; 695 | TARGETED_DEVICE_FAMILY = "1,2"; 696 | }; 697 | name = Release; 698 | }; 699 | /* End XCBuildConfiguration section */ 700 | 701 | /* Begin XCConfigurationList section */ 702 | 114E6C7929C8ABB900FCCD52 /* Build configuration list for PBXProject "SolutionExample" */ = { 703 | isa = XCConfigurationList; 704 | buildConfigurations = ( 705 | 114E6C9029C8ABBB00FCCD52 /* Debug */, 706 | 114E6C9129C8ABBB00FCCD52 /* Release */, 707 | ); 708 | defaultConfigurationIsVisible = 0; 709 | defaultConfigurationName = Release; 710 | }; 711 | 114E6C9229C8ABBB00FCCD52 /* Build configuration list for PBXNativeTarget "SolutionExample" */ = { 712 | isa = XCConfigurationList; 713 | buildConfigurations = ( 714 | 114E6C9329C8ABBB00FCCD52 /* Debug */, 715 | 114E6C9429C8ABBB00FCCD52 /* Release */, 716 | ); 717 | defaultConfigurationIsVisible = 0; 718 | defaultConfigurationName = Release; 719 | }; 720 | /* End XCConfigurationList section */ 721 | }; 722 | rootObject = 114E6C7629C8ABB900FCCD52 /* Project object */; 723 | } 724 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample.xcodeproj/project.xcworkspace/xcuserdata/nikita.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avito-tech/internship_ios_2022/0b279ffa3df7fb9c5f8e9a1747be11d9c6550eef/SolutionExample/SolutionExample.xcodeproj/project.xcworkspace/xcuserdata/nikita.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /SolutionExample/SolutionExample.xcodeproj/xcuserdata/nikita.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample.xcodeproj/xcuserdata/nikita.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | SolutionExample.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/App/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @main 4 | final class AppDelegate: UIResponder, UIApplicationDelegate { 5 | 6 | func application( 7 | _ application: UIApplication, 8 | configurationForConnecting connectingSceneSession: UISceneSession, 9 | options: UIScene.ConnectionOptions 10 | ) -> UISceneConfiguration { 11 | UISceneConfiguration( 12 | name: "Default Configuration", 13 | sessionRole: connectingSceneSession.role 14 | ) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/App/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 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/App/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/App/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/App/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/App/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIApplicationSceneManifest 6 | 7 | UIApplicationSupportsMultipleScenes 8 | 9 | UISceneConfigurations 10 | 11 | UIWindowSceneSessionRoleApplication 12 | 13 | 14 | UISceneConfigurationName 15 | Default Configuration 16 | UISceneDelegateClassName 17 | $(PRODUCT_MODULE_NAME).SceneDelegate 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/App/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class SceneDelegate: UIResponder, UIWindowSceneDelegate { 4 | 5 | // MARK: - Properties 6 | var window: UIWindow? 7 | 8 | // MARK: - UIWindowSceneDelegate 9 | func scene( 10 | _ scene: UIScene, 11 | willConnectTo session: UISceneSession, 12 | options connectionOptions: UIScene.ConnectionOptions 13 | ) { 14 | guard let scene = (scene as? UIWindowScene) else { return } 15 | 16 | let viewController = MainPageAssembly().viewController() 17 | let navigationController = UINavigationController(rootViewController: viewController) 18 | 19 | window = UIWindow(windowScene: scene) 20 | window?.overrideUserInterfaceStyle = .light 21 | window?.rootViewController = navigationController 22 | window?.makeKeyAndVisible() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/Dialogs/ContentPlaceholderDisplayable/ContentPlaceholderDisplayable.swift: -------------------------------------------------------------------------------- 1 | @MainActor protocol ContentPlaceholderDisplayable: AnyObject { 2 | func showPlaceholder(model: ContentPlaceholderModel) 3 | func hidePlaceholder() 4 | } 5 | 6 | extension ContentPlaceholderDisplayable { 7 | func showPlaceholder(error: NetworkError, onTap: @escaping () -> ()) { 8 | switch error { 9 | case .noInternetConnection, .timeout: 10 | showPlaceholder(model: .init( 11 | title: "Нет интернета", 12 | button: .init( 13 | title: "Повторить", 14 | onTap: { [weak self] in 15 | self?.hidePlaceholder() 16 | onTap() 17 | } 18 | ) 19 | )) 20 | default: 21 | showPlaceholder(model: .init( 22 | title: "Неизвестная ошибка", 23 | button: .init( 24 | title: "Повторить", 25 | onTap: { [weak self] in 26 | self?.hidePlaceholder() 27 | onTap() 28 | } 29 | ) 30 | )) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/Dialogs/ContentPlaceholderDisplayable/WeakContentPlaceholderDisplayable.swift: -------------------------------------------------------------------------------- 1 | final class WeakContentPlaceholderDisplayable: ContentPlaceholderDisplayable { 2 | 3 | // MARK: - Properties 4 | private weak var contentPlaceholderDisplayable: ContentPlaceholderDisplayable? 5 | 6 | // MARK: - Init 7 | init(contentPlaceholderDisplayable: ContentPlaceholderDisplayable) { 8 | self.contentPlaceholderDisplayable = contentPlaceholderDisplayable 9 | } 10 | 11 | // MARK: - ContentPlaceholderDisplayable 12 | func showPlaceholder(model: ContentPlaceholderModel) { 13 | contentPlaceholderDisplayable?.showPlaceholder(model: model) 14 | } 15 | 16 | func hidePlaceholder() { 17 | contentPlaceholderDisplayable?.hidePlaceholder() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/Dialogs/LoaderDisplayable/LoaderDisplayable.swift: -------------------------------------------------------------------------------- 1 | @MainActor protocol LoaderDisplayable: AnyObject { 2 | func showLoader() 3 | func hideLoader() 4 | } 5 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/Dialogs/LoaderDisplayable/WeakDelayLoaderDisplayable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class WeakDelayLoaderDisplayable: LoaderDisplayable { 4 | 5 | // MARK: - Properties 6 | private weak var loaderDisplayable: LoaderDisplayable? 7 | 8 | // MARK: - Init 9 | init(loaderDisplayable: LoaderDisplayable) { 10 | self.loaderDisplayable = loaderDisplayable 11 | } 12 | 13 | // MARK: - LoaderDisplayable 14 | func showLoader() { 15 | loaderDisplayable?.showLoader() 16 | } 17 | 18 | func hideLoader() { 19 | loaderDisplayable?.hideLoader() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/Dialogs/MessageDisplayable/MessageDisplayable.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @MainActor protocol MessageDisplayable: AnyObject { 4 | func showMessage(_ message: String) 5 | } 6 | 7 | extension MessageDisplayable { 8 | func showMessage(error: NetworkError) { 9 | switch error { 10 | case .noInternetConnection, .timeout: 11 | showMessage("Нет интернета") 12 | default: 13 | showMessage("Неизвестная ошибка") 14 | } 15 | } 16 | 17 | } 18 | 19 | extension MessageDisplayable where Self: UIViewController { 20 | func showMessage(_ message: String) { 21 | ToastController(title: nil, message: message, preferredStyle: .actionSheet).show() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/Dialogs/MessageDisplayable/WeakMessageDisplayable.swift: -------------------------------------------------------------------------------- 1 | final class WeakMessageDisplayable: MessageDisplayable { 2 | 3 | // MARK: - Properties 4 | private weak var messageDisplayable: MessageDisplayable? 5 | 6 | // MARK: - Init 7 | init(messageDisplayable: MessageDisplayable) { 8 | self.messageDisplayable = messageDisplayable 9 | } 10 | 11 | // MARK: - MessageDisplayable 12 | func showMessage(_ message: String) { 13 | messageDisplayable?.showMessage(message) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/Modules/MainPage/Assembly/MainPageAssembly.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | @MainActor final class MainPageAssembly { 4 | func viewController() -> UIViewController { 5 | let service = MainPageServiceImpl(networkClient: NetworkClientImpl()) 6 | let viewController = MainViewController() 7 | 8 | let loaderDisplayable = WeakDelayLoaderDisplayable( 9 | loaderDisplayable: viewController 10 | ) 11 | 12 | let messageDisplayable = WeakMessageDisplayable( 13 | messageDisplayable: viewController 14 | ) 15 | 16 | let contentPlaceholderDisplayable = WeakContentPlaceholderDisplayable( 17 | contentPlaceholderDisplayable: viewController 18 | ) 19 | 20 | let presenter = MainPresenter( 21 | service: service, 22 | loaderDisplayable: loaderDisplayable, 23 | messageDisplayable: messageDisplayable, 24 | contentPlaceholderDisplayable: contentPlaceholderDisplayable 25 | ) 26 | 27 | viewController.addDisposeBag(presenter) 28 | presenter.view = viewController 29 | 30 | return viewController 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/Modules/MainPage/Models/Employee.swift: -------------------------------------------------------------------------------- 1 | struct Employee: Codable { 2 | 3 | let name: String 4 | let skills: [String] 5 | let phoneNumber: String 6 | 7 | enum CodingKeys: String, CodingKey { 8 | case name 9 | case skills 10 | case phoneNumber = "phone_number" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/Modules/MainPage/Presenter/MainPagePresenter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class MainPresenter { 4 | 5 | // MARK: - Weak properties 6 | @MainActor weak var view: MainViewViewInput? { 7 | didSet { 8 | setUpView() 9 | } 10 | } 11 | 12 | // MARK: - Dependencies 13 | private let service: MainPageService 14 | private let loaderDisplayable: LoaderDisplayable 15 | private let messageDisplayable: MessageDisplayable 16 | private let contentPlaceholderDisplayable: ContentPlaceholderDisplayable 17 | 18 | // MARK: - Init 19 | init( 20 | service: MainPageService, 21 | loaderDisplayable: LoaderDisplayable, 22 | messageDisplayable: MessageDisplayable, 23 | contentPlaceholderDisplayable: ContentPlaceholderDisplayable 24 | ) { 25 | self.service = service 26 | self.loaderDisplayable = loaderDisplayable 27 | self.messageDisplayable = messageDisplayable 28 | self.contentPlaceholderDisplayable = contentPlaceholderDisplayable 29 | } 30 | 31 | // MARK: - SetUp 32 | @MainActor private func setUpView() { 33 | view?.onTopRefresh = { [weak self] in 34 | self?.proceedToLoadCompany(isRefreshing: true) 35 | } 36 | 37 | view?.onViewDidLoad = { [weak self] in 38 | self?.proceedToLoadCompany() 39 | } 40 | } 41 | 42 | // MARK: - Load company 43 | private func proceedToLoadCompany(isRefreshing: Bool = false) { 44 | Task { 45 | await proceedToLoadCompany(isRefreshing: isRefreshing) 46 | } 47 | } 48 | 49 | private func proceedToLoadCompany(isRefreshing: Bool = false) async { 50 | let loaderDisplayable = isRefreshing ? nil : loaderDisplayable 51 | 52 | await loaderDisplayable?.showLoader() 53 | let result = await service.company() 54 | await loaderDisplayable?.hideLoader() 55 | await view?.endRefreshing() 56 | 57 | switch result { 58 | case let .success(response): 59 | await handleCompanyLoading(response) 60 | case let .failure(error): 61 | await handleCompanyLoading(error, isRefreshing: isRefreshing) 62 | } 63 | } 64 | 65 | private func handleCompanyLoading(_ response: CompanyResponse) async { 66 | await view?.setTitle(response.name) 67 | await view?.display(models: response.employees 68 | .sorted { $0.name < $1.name } 69 | .enumerated() 70 | .map { 71 | TableViewModel( 72 | id: "\($0)", 73 | data: EmployeeCellData( 74 | name: $1.name, 75 | skills: $1.skills, 76 | phoneNumber: $1.phoneNumber 77 | ), 78 | cellType: EmployeeCell.self 79 | ) 80 | } 81 | ) 82 | } 83 | 84 | private func handleCompanyLoading(_ error: NetworkError, isRefreshing: Bool) async { 85 | if isRefreshing { 86 | await messageDisplayable.showMessage(error: error) 87 | return 88 | } 89 | 90 | await contentPlaceholderDisplayable.showPlaceholder(error: error) { [weak self] in 91 | self?.proceedToLoadCompany() 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/Modules/MainPage/View/Cells/EmployeeCell/EmployeeCell.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class EmployeeCell: UITableViewCell, TableViewCellInput { 4 | 5 | // MARK: - Subviews 6 | private let nameLabel = UILabel() 7 | private let phoneNumberLabel = UILabel() 8 | private let skillsLabel = UILabel() 9 | 10 | // MARK: - Init 11 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 12 | super.init(style: style, reuseIdentifier: reuseIdentifier) 13 | 14 | contentView.addSubview(nameLabel) 15 | contentView.addSubview(phoneNumberLabel) 16 | contentView.addSubview(skillsLabel) 17 | } 18 | 19 | required init?(coder: NSCoder) { 20 | fatalError("init(coder:) has not been implemented") 21 | } 22 | 23 | // MARK: - TableViewCellInput 24 | func update(with data: Any) { 25 | guard let data = data as? EmployeeCellData else { return } 26 | 27 | nameLabel.text = "Name: \(data.name)" 28 | phoneNumberLabel.text = "Phone: \(data.phoneNumber)" 29 | skillsLabel.text = "Skills: \(data.skills.joined(separator: ", "))" 30 | } 31 | 32 | // MARK: - Layout 33 | override func sizeThatFits(_ size: CGSize) -> CGSize { 34 | Spec.layout( 35 | size: size, 36 | nameLabel: nameLabel, 37 | phoneNumberLabel: phoneNumberLabel, 38 | skillsLabel: skillsLabel 39 | ).preferredSize 40 | } 41 | 42 | override func layoutSubviews() { 43 | super.layoutSubviews() 44 | 45 | let layout = Spec.layout( 46 | size: bounds.size, 47 | nameLabel: nameLabel, 48 | phoneNumberLabel: phoneNumberLabel, 49 | skillsLabel: skillsLabel 50 | ) 51 | 52 | nameLabel.frame = layout.nameLabelFrame 53 | phoneNumberLabel.frame = layout.phoneNumberLabelFrame 54 | skillsLabel.frame = layout.skillsLabelFrame 55 | } 56 | } 57 | 58 | // MARK: - Spec 59 | fileprivate enum Spec { 60 | 61 | static let contentInsets = UIEdgeInsets(top: 10, left: 24, bottom: 10, right: 24) 62 | static let interItemSpacing: CGFloat = 10 63 | 64 | // MARK: - Layout 65 | typealias Layout = ( 66 | nameLabelFrame: CGRect, 67 | phoneNumberLabelFrame: CGRect, 68 | skillsLabelFrame: CGRect, 69 | preferredSize: CGSize 70 | ) 71 | 72 | static func layout( 73 | size: CGSize, 74 | nameLabel: UILabel, 75 | phoneNumberLabel: UILabel, 76 | skillsLabel: UILabel 77 | ) -> Layout { 78 | let contentSize = CGSize( 79 | width: size.width - contentInsets.left - contentInsets.right, 80 | height: size.height 81 | ) 82 | 83 | let nameLabelSize = nameLabel.sizeThatFits(contentSize) 84 | let nameLabelFrame = CGRect( 85 | x: contentInsets.left, 86 | y: contentInsets.top, 87 | width: nameLabelSize.width, 88 | height: nameLabelSize.height 89 | ) 90 | 91 | let phoneNumberLabelSize = phoneNumberLabel.sizeThatFits(contentSize) 92 | let phoneNumberLabelFrame = CGRect( 93 | x: contentInsets.left, 94 | y: nameLabelFrame.maxY + interItemSpacing, 95 | width: phoneNumberLabelSize.width, 96 | height: phoneNumberLabelSize.height 97 | ) 98 | 99 | let skillsLabelSize = skillsLabel.sizeThatFits(contentSize) 100 | let skillsLabelFrame = CGRect( 101 | x: contentInsets.left, 102 | y: phoneNumberLabelFrame.maxY + interItemSpacing, 103 | width: skillsLabelSize.width, 104 | height: skillsLabelSize.height 105 | ) 106 | 107 | let customBackgroundViewFrame = CGRect( 108 | x: .zero, 109 | y: .zero, 110 | width: size.width, 111 | height: skillsLabelFrame.maxY + contentInsets.bottom 112 | ) 113 | 114 | let preferredSize = CGSize( 115 | width: size.width, 116 | height: skillsLabelFrame.maxY + contentInsets.bottom 117 | ) 118 | 119 | return ( 120 | nameLabelFrame, 121 | phoneNumberLabelFrame, 122 | skillsLabelFrame, 123 | preferredSize 124 | ) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/Modules/MainPage/View/Cells/EmployeeCell/EmployeeCellData.swift: -------------------------------------------------------------------------------- 1 | struct EmployeeCellData: Hashable { 2 | let name: String 3 | let skills: [String] 4 | let phoneNumber: String 5 | } 6 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/Modules/MainPage/View/MainPageViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class MainViewController: 4 | BaseViewController, 5 | MainViewViewInput, 6 | LoaderDisplayable, 7 | ContentPlaceholderDisplayable 8 | { 9 | // MARK: - Subviews 10 | private let loader = Loader() 11 | private let tableView = TableView() 12 | private let contentPlaceholder = ContentPlaceholder() 13 | 14 | // MARK: - Lifecycle 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | 18 | view.addSubview(loader) 19 | view.addSubview(contentPlaceholder) 20 | view.addSubview(tableView) 21 | } 22 | 23 | // MARK: - MainViewViewInput 24 | var onTopRefresh: (() -> ())? { 25 | get { tableView.onTopRefresh } 26 | set { tableView.onTopRefresh = newValue } 27 | } 28 | 29 | func setTitle(_ title: String) { 30 | self.title = title 31 | } 32 | 33 | func display(models: [TableViewModel]) { 34 | tableView.display(models: models) 35 | } 36 | 37 | func endRefreshing() { 38 | tableView.endRefreshing() 39 | } 40 | 41 | // MARK: - LoaderDisplayable 42 | func showLoader() { 43 | loader.isHidden = false 44 | loader.start() 45 | view.bringSubviewToFront(loader) 46 | } 47 | 48 | func hideLoader() { 49 | loader.isHidden = true 50 | loader.stop() 51 | view.sendSubviewToBack(loader) 52 | } 53 | 54 | // MARK: - ContentPlaceholderDisplayable 55 | func showPlaceholder(model: ContentPlaceholderModel) { 56 | contentPlaceholder.update(with: model) 57 | contentPlaceholder.isHidden = false 58 | view.bringSubviewToFront(contentPlaceholder) 59 | } 60 | 61 | func hidePlaceholder() { 62 | contentPlaceholder.isHidden = true 63 | view.sendSubviewToBack(contentPlaceholder) 64 | } 65 | 66 | // MARK: - Layout 67 | override func viewWillLayoutSubviews() { 68 | super.viewWillLayoutSubviews() 69 | 70 | loader.frame = view.bounds 71 | contentPlaceholder.frame = view.bounds 72 | tableView.frame = view.bounds 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/Modules/MainPage/View/MainPageViewViewInput.swift: -------------------------------------------------------------------------------- 1 | @MainActor protocol MainViewViewInput: MutableViewLifecycleObserver { 2 | 3 | var onTopRefresh: (() -> ())? { get set } 4 | 5 | func setTitle(_ title: String) 6 | func display(models: [TableViewModel]) 7 | func endRefreshing() 8 | } 9 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/Networking/CachePolice.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum CachePolice { 4 | case cacheToDisk(CacheTime) 5 | } 6 | 7 | enum CacheTime: TimeInterval { 8 | case oneHour = 3600 9 | } 10 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/Networking/Client/NetworkClient.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | protocol NetworkClient: AnyObject { 4 | func send( 5 | request: Request 6 | ) async -> Result 7 | } 8 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/Networking/Client/NetworkClientImpl.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class NetworkClientImpl: NetworkClient { 4 | 5 | // MARK: - Properties 6 | private let urlSession: URLSession = URLSession(configuration: .default) 7 | 8 | // MARK: - Dependencies 9 | private let urlCache: URLCache 10 | private let userDefaults: UserDefaults 11 | private let requestBuilder: RequestBuilder 12 | 13 | // MARK: - Init 14 | init( 15 | urlCache: URLCache = URLCache(), 16 | userDefaults: UserDefaults = UserDefaults.standard, 17 | requestBuilder: RequestBuilder = RequestBuilderImpl() 18 | ) { 19 | self.urlCache = urlCache 20 | self.userDefaults = userDefaults 21 | self.requestBuilder = requestBuilder 22 | } 23 | 24 | // MARK: - NetworkClient 25 | func send( 26 | request: Request 27 | ) async -> Result { 28 | 29 | switch requestBuilder.build(request: request) { 30 | case let .success(urlRequest): 31 | return await send( 32 | urlRequest: urlRequest, 33 | cachePolice: request.cachePolice, 34 | responseConverter: request.responseConverter 35 | ) 36 | 37 | case let .failure(error): 38 | return .failure(error) 39 | 40 | } 41 | } 42 | 43 | // MARK: - Private methods - Send methods 44 | private func send( 45 | urlRequest: URLRequest, 46 | cachePolice: CachePolice, 47 | responseConverter: Converter 48 | ) async -> Result { 49 | if let cachedData = cachedData(urlRequest: urlRequest) { 50 | return decodeResponse(from: cachedData, responseConverter: responseConverter) 51 | } 52 | 53 | do { 54 | let (data, response) = try await urlSession.data(for: urlRequest) 55 | 56 | cacheDataIfNeeded( 57 | urlRequest: urlRequest, 58 | cachePolice: cachePolice, 59 | cachedURLResponse: CachedURLResponse(response: response, data: data) 60 | ) 61 | 62 | return decodeResponse(from: data, responseConverter: responseConverter) 63 | } catch { 64 | switch (error as? URLError)?.code { 65 | case .some(.notConnectedToInternet): 66 | return .failure(.noInternetConnection) 67 | case .some(.timedOut): 68 | return .failure(.timeout) 69 | default: 70 | return .failure(.networkError) 71 | } 72 | } 73 | } 74 | 75 | // MARK: - Private methods - Decode methods 76 | func decodeResponse( 77 | from data: Data, 78 | responseConverter: Converter 79 | ) -> Result { 80 | if let response = responseConverter.decodeResponse(from: data) { 81 | return .success(response) 82 | } 83 | return .failure(.parsingFailure) 84 | } 85 | 86 | // MARK: - Private methods - Cache methods 87 | private func cacheDataIfNeeded( 88 | urlRequest: URLRequest, 89 | cachePolice: CachePolice, 90 | cachedURLResponse: CachedURLResponse 91 | ) { 92 | switch cachePolice { 93 | case let .cacheToDisk(cacheTime): 94 | guard let urlString = urlRequest.url?.absoluteString else { return } 95 | 96 | userDefaults.set( 97 | cacheTime.rawValue + Date.timeIntervalSinceReferenceDate, 98 | forKey: Spec.responseCacheKey(urlString: urlString) 99 | ) 100 | urlCache.storeCachedResponse(cachedURLResponse, for: urlRequest) 101 | } 102 | } 103 | 104 | private func cachedData(urlRequest: URLRequest) -> Data? { 105 | guard let urlString = urlRequest.url?.absoluteString, 106 | let storedData = userDefaults.object(forKey: Spec.responseCacheKey(urlString: urlString)), 107 | let expirationTimestamp = storedData as? TimeInterval 108 | else { return nil } 109 | 110 | guard expirationTimestamp > Date.timeIntervalSinceReferenceDate else { 111 | userDefaults.removeObject(forKey: Spec.responseCacheKey(urlString: urlString)) 112 | return nil 113 | } 114 | 115 | return urlCache.cachedResponse(for: urlRequest)?.data 116 | } 117 | } 118 | 119 | // MARK: - Spec 120 | fileprivate enum Spec { 121 | static func responseCacheKey(urlString: String) -> String { 122 | "ExpirationTimestamp_\(urlString)" 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/Networking/Converter/DecodingNetworkResponseConverter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class DecodingNetworkResponseConverter: NetworkResponseConverter where Response: Decodable { 4 | 5 | func decodeResponse(from data: Data) -> Response? { 6 | try? JSONDecoder().decode(Response.self, from: data) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/Networking/Converter/NetworkResponseConverter.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | protocol NetworkResponseConverter: AnyObject { 4 | associatedtype Response 5 | 6 | func decodeResponse(from data: Data) -> Response? 7 | } 8 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/Networking/Converter/NetworkResponseConverterOf.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class NetworkResponseConverterOf: NetworkResponseConverter { 4 | 5 | // MARK: - Callbacks 6 | private let decodeResponse: (Data) -> Response? 7 | 8 | // MARK: - Init 9 | init( 10 | converter: Converter 11 | ) where Converter.Response == Response { 12 | decodeResponse = { data in 13 | converter.decodeResponse(from: data) 14 | } 15 | } 16 | 17 | // MARK: - NetworkResponseConverter 18 | func decodeResponse(from data: Data) -> Response? { 19 | decodeResponse(data) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/Networking/HttpMethod.swift: -------------------------------------------------------------------------------- 1 | enum HttpMethod: String { 2 | case GET 3 | } 4 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/Networking/NetworkError.swift: -------------------------------------------------------------------------------- 1 | enum NetworkError: Error { 2 | case cantBuildUrlFromRequest 3 | case noInternetConnection 4 | case parsingFailure 5 | case networkError 6 | case timeout 7 | } 8 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/Networking/NetworkRequest.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | protocol NetworkRequest: AnyObject { 4 | associatedtype Response 5 | 6 | var path: String { get } 7 | var httpMethod: HttpMethod { get } 8 | var cachePolice: CachePolice { get } 9 | var responseConverter: NetworkResponseConverterOf { get } 10 | } 11 | 12 | extension NetworkRequest where Response: Decodable { 13 | var responseConverter: NetworkResponseConverterOf { 14 | NetworkResponseConverterOf(converter: DecodingNetworkResponseConverter()) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/Networking/RequestBuilder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | protocol RequestBuilder: AnyObject { 4 | func build(request: any NetworkRequest) -> Result 5 | } 6 | 7 | final class RequestBuilderImpl: RequestBuilder { 8 | 9 | // MARK: - Data 10 | private let host: String 11 | 12 | // MARK: - Init 13 | init(host: String = "run.mocky.io") { 14 | self.host = host 15 | } 16 | 17 | // MARK: - RequestBuilder 18 | func build(request: any NetworkRequest) -> Result { 19 | guard let url = URL(string: "https://\(host)/\(request.path)") else { 20 | return .failure(.cantBuildUrlFromRequest) 21 | } 22 | 23 | var urlRequest = URLRequest(url: url, timeoutInterval: 15) 24 | urlRequest.httpMethod = request.httpMethod.rawValue 25 | 26 | return .success(urlRequest) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/Presentation/BaseViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class BaseViewController: 4 | UIViewController, 5 | MessageDisplayable, 6 | MutableViewLifecycleObserver 7 | { 8 | // MARK: - Data 9 | private var disposeBag: Any? 10 | 11 | // MARK: - MutableViewLifecycleObserver 12 | var onViewDidLoad: (() -> ())? 13 | 14 | // MARK: - Lifecycle 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | onViewDidLoad?() 18 | } 19 | 20 | // MARK: - Methods 21 | func addDisposeBag(_ disposeBag: Any) { 22 | self.disposeBag = disposeBag 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/Presentation/MutableViewLifecycleObserver.swift: -------------------------------------------------------------------------------- 1 | @MainActor protocol MutableViewLifecycleObserver: AnyObject { 2 | var onViewDidLoad: (() -> ())? { get set } 3 | } 4 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/Services/MainPageService/MainPageService.swift: -------------------------------------------------------------------------------- 1 | protocol MainPageService: AnyObject { 2 | func company() async -> Result 3 | } 4 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/Services/MainPageService/MainPageServiceImpl.swift: -------------------------------------------------------------------------------- 1 | final class MainPageServiceImpl: MainPageService { 2 | 3 | // MARK: - Properties 4 | private let networkClient: NetworkClient 5 | 6 | // MARK: - Dependencies 7 | init(networkClient: NetworkClient) { 8 | self.networkClient = networkClient 9 | } 10 | 11 | // MARK: - MainPageService 12 | func company() async -> Result { 13 | await networkClient.send(request: CompanyRequest()) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/Services/MainPageService/Requests/CompanyRequest.swift: -------------------------------------------------------------------------------- 1 | final class CompanyRequest: NetworkRequest { 2 | typealias Response = CompanyResponse 3 | 4 | let path = "v3/1d1cb4ec-73db-4762-8c4b-0b8aa3cecd4c" 5 | let httpMethod: HttpMethod = .GET 6 | let cachePolice: CachePolice = .cacheToDisk(.oneHour) 7 | } 8 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/Services/MainPageService/Requests/CompanyResponse.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct CompanyResponse: Decodable { 4 | let name: String 5 | let employees: [Employee] 6 | 7 | init(from decoder: Decoder) throws { 8 | let container = try decoder.container() 9 | let nestedContainer = try container.nestedContainer(key: "company") 10 | 11 | name = try nestedContainer.decode(key: "name") 12 | employees = try nestedContainer.decode(key: "employees") 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/UIComponents/ContentPlaceholder/ContentPlaceholder.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class ContentPlaceholder: UIView { 4 | 5 | // MARK: - Callbacks 6 | private var onButtonTap: (() -> ())? 7 | 8 | // MARK: - Subviews 9 | private let title = UILabel() 10 | private let button = UIButton() 11 | 12 | // MARK: - Init 13 | init() { 14 | super.init(frame: .zero) 15 | 16 | addSubview(title) 17 | addSubview(button) 18 | 19 | setUpContent() 20 | } 21 | 22 | required init?(coder: NSCoder) { 23 | fatalError("init(coder:) has not been implemented") 24 | } 25 | 26 | // MARK: - SetUp 27 | private func setUpContent() { 28 | backgroundColor = Spec.backgroundColor 29 | 30 | button.layer.cornerRadius = Spec.Button.cornerRadius 31 | button.backgroundColor = Spec.Button.backgroundColor 32 | button.setTitleColor(Spec.Button.titleColor, for: .normal) 33 | button.addTarget(self, action: #selector(onButtonTap(_:)), for: .touchUpInside) 34 | } 35 | 36 | // MARK: - Update content 37 | func update(with model: ContentPlaceholderModel) { 38 | title.text = model.title 39 | button.setTitle(model.button.title, for: .normal) 40 | 41 | onButtonTap = { 42 | model.button.onTap() 43 | } 44 | 45 | setNeedsLayout() 46 | } 47 | 48 | // MARK: - Layout 49 | override func layoutSubviews() { 50 | super.layoutSubviews() 51 | 52 | let titleSize = title.sizeThatFits(bounds.size) 53 | title.frame = CGRect( 54 | x: (bounds.width - titleSize.width) / 2, 55 | y: (bounds.height - Spec.interItemSpacing) / 2 - titleSize.height, 56 | width: titleSize.width, 57 | height: titleSize.height 58 | ) 59 | 60 | let buttonSize = button.sizeThatFits(bounds.size) 61 | let updatedButtonSize = CGSize( 62 | width: buttonSize.width 63 | + Spec.Button.backgroundContentInset.left 64 | + Spec.Button.backgroundContentInset.right, 65 | height: buttonSize.height 66 | + Spec.Button.backgroundContentInset.top 67 | + Spec.Button.backgroundContentInset.bottom 68 | ) 69 | 70 | button.frame = CGRect( 71 | x: (bounds.width - updatedButtonSize.width) / 2, 72 | y: (bounds.height + Spec.interItemSpacing) / 2, 73 | width: updatedButtonSize.width, 74 | height: updatedButtonSize.height 75 | ) 76 | } 77 | 78 | // MARK: - Touch handlers 79 | @objc private func onButtonTap(_ sender: UIButton) { 80 | onButtonTap?() 81 | } 82 | } 83 | 84 | // MARK: - Spec 85 | fileprivate enum Spec { 86 | static let backgroundColor = UIColor.white 87 | static let interItemSpacing: CGFloat = 20 88 | 89 | enum Button { 90 | static let titleColor = UIColor.black 91 | static let backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.2) 92 | 93 | static let cornerRadius: CGFloat = 5 94 | static let backgroundContentInset = UIEdgeInsets(top: 5, left: 10, bottom: 5, right: 10) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/UIComponents/ContentPlaceholder/ContentPlaceholderModel.swift: -------------------------------------------------------------------------------- 1 | struct ContentPlaceholderModel { 2 | 3 | let title: String 4 | let button: Button 5 | 6 | struct Button { 7 | let title: String 8 | let onTap: () -> () 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/UIComponents/Loader/Loader.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class Loader: UIView { 4 | 5 | // MARK: - Subviews 6 | private let activityIndicator = UIActivityIndicatorView(style: .large) 7 | 8 | // MARK: - Init 9 | init() { 10 | super.init(frame: .zero) 11 | 12 | addSubview(activityIndicator) 13 | setUpContent() 14 | } 15 | 16 | required init?(coder: NSCoder) { 17 | fatalError("init(coder:) has not been implemented") 18 | } 19 | 20 | // MARK: - SetUp 21 | private func setUpContent() { 22 | backgroundColor = Spec.backgroundColor 23 | } 24 | 25 | // MARK: - Methods 26 | func start() { 27 | activityIndicator.startAnimating() 28 | } 29 | 30 | func stop() { 31 | activityIndicator.stopAnimating() 32 | } 33 | 34 | // MARK: - Layout 35 | override func layoutSubviews() { 36 | super.layoutSubviews() 37 | 38 | let activityIndicatorSize = activityIndicator.sizeThatFits(bounds.size) 39 | activityIndicator.frame = CGRect( 40 | x: (bounds.width - activityIndicatorSize.width) / 2, 41 | y: (bounds.height - activityIndicatorSize.height) / 2, 42 | width: activityIndicatorSize.width, 43 | height: activityIndicatorSize.height 44 | ) 45 | } 46 | } 47 | 48 | // MARK: - Spec 49 | fileprivate enum Spec { 50 | static let backgroundColor = UIColor.white 51 | } 52 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/UIComponents/TableView/TableView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class TableView: UIView, UITableViewDataSource { 4 | 5 | // MARK: - Callbacks 6 | var onTopRefresh: (() -> ())? 7 | 8 | // MARK: - Data 9 | private var models: [TableViewModel] = [] 10 | private var registerIds: Set = [] 11 | 12 | // MARK: - Subviews 13 | private let tableView = UITableView() 14 | private let refreshControl = UIRefreshControl() 15 | 16 | // MARK: - Init 17 | init() { 18 | super.init(frame: .zero) 19 | 20 | addSubview(tableView) 21 | tableView.addSubview(refreshControl) 22 | setUpContent() 23 | } 24 | 25 | required init?(coder: NSCoder) { 26 | fatalError("init(coder:) has not been implemented") 27 | } 28 | 29 | // MARK: - SetUp 30 | private func setUpContent() { 31 | tableView.dataSource = self 32 | 33 | tableView.separatorStyle = .singleLine 34 | tableView.allowsSelection = false 35 | tableView.backgroundColor = Spec.backgroundColor 36 | 37 | refreshControl.addTarget(self, action: #selector(onTopRefresh(_:)), for: .valueChanged) 38 | } 39 | 40 | // MARK: - Methods 41 | func display(models: [TableViewModel]) { 42 | guard self.models != models else { return } 43 | 44 | self.models = models 45 | tableView.reloadData() 46 | } 47 | 48 | func endRefreshing() { 49 | refreshControl.endRefreshing() 50 | } 51 | 52 | // MARK: - UITableViewDataSource 53 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 54 | models.count 55 | } 56 | 57 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 58 | guard indexPath.item < models.count else { return UITableViewCell() } 59 | 60 | let model = models[indexPath.item] 61 | let cell = cell(indexPath: indexPath, model: model) 62 | 63 | if let cell = cell as? TableViewCellInput { 64 | cell.update(with: model.data) 65 | } 66 | 67 | return cell 68 | } 69 | 70 | // MARK: - Layout 71 | override func layoutSubviews() { 72 | super.layoutSubviews() 73 | tableView.frame = bounds 74 | } 75 | 76 | // MARK: - Private methods 77 | func cell(indexPath: IndexPath, model: TableViewModel) -> UITableViewCell { 78 | if !registerIds.contains(model.id) { 79 | registerIds.insert(model.id) 80 | 81 | tableView.register( 82 | model.cellType, 83 | forCellReuseIdentifier: model.id 84 | ) 85 | } 86 | 87 | return tableView.dequeueReusableCell( 88 | withIdentifier: model.id, 89 | for: indexPath 90 | ) 91 | } 92 | 93 | @objc private func onTopRefresh(_ sender: UIRefreshControl) { 94 | onTopRefresh?() 95 | } 96 | } 97 | 98 | // MARK: - Spec 99 | fileprivate enum Spec { 100 | static let backgroundColor = UIColor.white 101 | } 102 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/UIComponents/TableView/TableViewCellInput.swift: -------------------------------------------------------------------------------- 1 | protocol TableViewCellInput { 2 | func update(with data: Any) 3 | } 4 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/UIComponents/TableView/TableViewModel.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | typealias TableViewCell = UITableViewCell & TableViewCellInput 4 | 5 | struct TableViewModel: Equatable { 6 | 7 | // MARK: - Properties 8 | let id: String 9 | let data: AnyHashable 10 | let cellType: TableViewCell.Type 11 | 12 | // MARK: - Equatable 13 | static func == (lhs: TableViewModel, rhs: TableViewModel) -> Bool { 14 | lhs.id == rhs.id && 15 | lhs.data == rhs.data 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/UIComponents/Toast/ToastController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class ToastController: UIAlertController { 4 | 5 | // MARK: - Properties 6 | let window: UIWindow? = { 7 | let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene 8 | let window = windowScene.map(UIWindow.init(windowScene: )) 9 | 10 | window?.isUserInteractionEnabled = false 11 | window?.rootViewController = UIViewController() 12 | window?.windowLevel = .alert + 1 13 | window?.makeKeyAndVisible() 14 | 15 | return window 16 | }() 17 | 18 | // MARK: - Methods 19 | func show() { 20 | window?.rootViewController?.present(self, animated: false) 21 | 22 | Task { 23 | await Task.sleep(seconds: 2) 24 | dismiss(animated: false) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/Utils/Decodable+Helpers.swift: -------------------------------------------------------------------------------- 1 | extension Decoder { 2 | 3 | func container() throws -> KeyedDecodingContainer { 4 | try container(keyedBy: GenericCodingKey.self) 5 | } 6 | } 7 | 8 | extension KeyedDecodingContainer where Key == GenericCodingKey { 9 | func nestedContainer(key: String) throws -> KeyedDecodingContainer { 10 | try nestedContainer(keyedBy: GenericCodingKey.self, forKey: GenericCodingKey(key)) 11 | } 12 | 13 | func decode(key: String) throws -> T { 14 | try decode(T.self, forKey: GenericCodingKey(key)) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/Utils/GenericCodingKey.swift: -------------------------------------------------------------------------------- 1 | final class GenericCodingKey: CodingKey { 2 | 3 | // MARK: - Properties 4 | private let value: String 5 | 6 | // MARK: - CodingKey 7 | var intValue: Int? { nil } 8 | var stringValue: String { value } 9 | 10 | // MARK: - Init 11 | init?(stringValue value: String) { nil } 12 | init?(intValue: Int) { nil } 13 | 14 | init(_ value: String) { 15 | self.value = value 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /SolutionExample/SolutionExample/Utils/Task+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Task where Success == Never, Failure == Never { 4 | static func sleep(seconds: TimeInterval) async { 5 | try? await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000)) 6 | } 7 | } 8 | --------------------------------------------------------------------------------