├── .gitignore ├── LICENSE.md ├── README.md ├── SwiftMVVMDemo.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcuserdata │ │ └── rushisangani.xcuserdatad │ │ ├── IDEFindNavigatorScopes.plist │ │ └── UserInterfaceState.xcuserstate ├── xcshareddata │ └── xcschemes │ │ └── SwiftMVVMDemo.xcscheme └── xcuserdata │ └── rushisangani.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ └── xcschememanagement.plist ├── SwiftMVVMDemo ├── AppCoordinator.swift ├── AppDelegate.swift ├── Assets.xcassets │ ├── .DS_Store │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ └── sample.imageset │ │ ├── Contents.json │ │ └── sample.png ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Constants.swift ├── Extensions │ ├── UIStoryboard+Extension.swift │ ├── UITableView+Extension.swift │ └── UIView+Constraints.swift ├── Info.plist ├── Modules │ ├── Comments │ │ └── Comment.swift │ ├── Photos │ │ ├── Image Loader │ │ │ ├── AsyncImageLoader.swift │ │ │ └── CacheManager.swift │ │ ├── Photo Row │ │ │ ├── PhotoRowViewModel.swift │ │ │ └── PhotoTableViewCell.swift │ │ ├── Photo.swift │ │ ├── PhotoService.swift │ │ ├── PhotosViewModel.swift │ │ ├── SwiftUI Views │ │ │ ├── PhotosAsyncImageView.swift │ │ │ └── PhotosCustomAsyncImageView.swift │ │ └── UIKit Views │ │ │ ├── PhotosCoordinator.swift │ │ │ └── PhotosViewController.swift │ └── Posts │ │ ├── Post.swift │ │ ├── PostListViewModel.swift │ │ ├── PostService.swift │ │ ├── SwiftUI Views │ │ └── PostListView.swift │ │ └── UIKit Views │ │ ├── PostCoordinator.swift │ │ └── PostListViewController.swift └── ViewController.swift ├── SwiftMVVMDemoTests ├── PhotosTests │ ├── ImageDownloadingTests │ │ ├── AsyncImageLoaderTests.swift │ │ └── CacheManagerTests.swift │ └── PhotoRowViewModelTests.swift ├── PostTests │ ├── PostCoordinatorTests.swift │ ├── PostListViewControllerTests.swift │ ├── PostListViewModelTests.swift │ ├── PostListViewTests.swift │ └── PostServiceTests.swift └── Resources │ ├── Bundle.swift │ ├── comments.json │ └── posts.json └── SwiftMVVMDemoUITests ├── SwiftMVVMDemoUITests.swift └── SwiftMVVMDemoUITestsLaunchTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | */*.xcuserstate 3 | */*.xcbkptlist 4 | .DS_Store 5 | SwiftMVVMDemo.xcodeproj/project.xcworkspace/xcuserdata/rushisangani.xcuserdatad/UserInterfaceState.xcuserstate 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Rushi Sangani 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swift + MVVM + Swift Package 2 | 3 | Welcome to the SwiftMVVMDemo repository! This sample app is designed to showcase various programming concepts, including SOLID principles, MVVM architecture, design patterns like singleton, and observer, and the integration of modern Swift features. 4 | 5 | ## Purpose 6 | 7 | The primary goal of this repository is to provide a comprehensive understanding of essential programming principles and demonstrate their implementation within a Swift project. Through this demo, you'll explore: 8 | 9 | - **MVVM Architecture**: Understand the Model-View-ViewModel architecture and its implementation in Swift. 10 | - **SOLID Principles**: Learn about writing maintainable and scalable code by applying SOLID principles. 11 | - **Design Patterns**: Explore various design patterns and their utilization in the context of this project. 12 | - **Swift Package**: Understand how you can create SwiftPackage and use it in your project. 13 | - **UIKit and SwiftUI**: See how a single ViewModel can be utilized in both UIKit and SwiftUI frameworks. 14 | - **Swift Concurrency**: Examples showcasing async-await usage for concurrency. 15 | - **Combine Framework**: Understanding and using the Combine framework for reactive programming. 16 | - **Unit Tests**: Learn about writing unit tests to ensure code quality and reliability. 17 | 18 | --- 19 | 20 | ## Contents 21 | 22 | ### MVVM + Routing through Coordinator 23 | 24 | | MVVM | Coordinator | 25 | |-------------|--------------------------------------------------------------------------------------------| 26 | | ![MVVM](https://miro.medium.com/v2/resize:fit:1400/1*SWQ5UQ1XU8wSykwXnWpiNg.png) | ![Coordinator](https://www.tpisoftware.com/tpu/File/onlineResource/articles/1110/titlePageImg.png) 27 | 28 | | Component | Class | Unit Test | 29 | |-------------|--------------------------------------------------------------------------------------------|------------------------------------------------------------------------| 30 | | Model | [Post](./SwiftMVVMDemo/Models/Post.swift) | - | 31 | | View | [PostListViewController](./SwiftMVVMDemo/Views/PostList/UIKit/PostListViewController.swift) (UIKit)
[PostListView](./SwiftMVVMDemo/Views/PostList/SwiftUI/PostListView.swift) (SwiftUI) | [PostListViewControllerTests](./SwiftMVVMDemoTests/PostTests/PostListViewControllerTests.swift)
[PostListViewTests](./SwiftMVVMDemoTests/PostTests/PostListViewTests.swift) | 32 | | ViewModel `Business logic` | [PostListViewModel](./SwiftMVVMDemo/Views/PostList/PostListViewModel.swift) | [PostListViewModelTests](./SwiftMVVMDemoTests/PostTests/PostListViewModelTests.swift) | 33 | | Service `Get data from DB or Network` | [PostService](./SwiftMVVMDemo/Services/PostService.swift) | [PostServiceTests](./SwiftMVVMDemoTests/PostTests/PostServiceTests.swift) | 34 | | Coordinator `Routing` | [PostCoordinator](./SwiftMVVMDemo/Views/PostList/UIKit/PostCoordinator.swift) | - | 35 | 36 | --- 37 | 38 | ### SOLID Principles 39 | 40 | | Principle | Meaning | ![Checkmark](https://img.shields.io/badge/-✔-green) Example | 41 | |-----------------------------------|------------------------------------------------------|-------------------------------------------------------------| 42 | | S - Single Responsibility | A class should have a single responsibility only. | [APIRequestHandler](https://github.com/rushisangani/NetworkKit/blob/main/Sources/NetworkKit/APIRequestHandler.swift) class only handling network requests.

[APIResponseHandler](https://github.com/rushisangani/NetworkKit/blob/main/Sources/NetworkKit/APIResponseHandler.swift) class only for parsing network response. | 43 | | O - Open/Closed | The classes and functions should be open for extension and closed for internal modification. | Leveraging `generics` within the [NetworkHandler](https://github.com/rushisangani/NetworkKit/blob/main/Sources/NetworkKit/NetworkManager.swift) fetch functionality empowers us to execute a wide range of requests, not limited to posts alone. | 44 | | L - Liskov Substitution | The code should be maintainable and reusable; objects should be replaced with instances of their subclasses without altering the behavior. LSP is closely related to `Polymorphism` | Typecasting of [PostCoordinator](./SwiftMVVMDemo/Views/PostList/UIKit/PostCoordinator.swift) to [Coordinator](./SwiftMVVMDemo/Coordinator/AppCoordinator.swift) protocol or calling method of `viewDidLoad` on **UINavigationController** will call `UIViewController.viewDidLoad` | 45 | | I - Interface Segregation | A class should not be forced to implement interfaces that they do not use. | Breaking interfaces into smaller and more specific ones. | 46 | | D - Dependency Inversion | High-level modules should not depend on low-level modules; rather, both should depend on abstractions. | Using protocols in [NetworkManager](https://github.com/rushisangani/NetworkKit/blob/main/Sources/NetworkKit/NetworkManager.swift) and [PostService](./SwiftMVVMDemo/Services/PhotoService.swift) allow us to remove direct dependencies and improve testability and mocking of individual components. | 47 | 48 | 49 | --- 50 | 51 | ### Design Patterns 52 | 53 | | Pattern | Meaning | ![Checkmark](https://img.shields.io/badge/-✔-green) Example | Tests | 54 | |------------|------------|--------------------|-------| 55 | | Singleton | The singleton pattern guarantees that only one instance of a class is instantiated. | [CacheManager](./SwiftMVVMDemo/Helpers/Image%20Downloading/CacheManager.swift) | [CacheManagerTests](./SwiftMVVMDemoTests/ImageDownloadingTests/CacheManagerTests.swift) | 56 | | Observer | The Observer pattern involves a subject maintaining a list of observers and automatically notifying them of any state changes, commonly used for implementing publish/subscribe systems. | [PhotoRowViewModel](./SwiftMVVMDemo/Views/Photos/PhotoRowViewModel.swift) uses `Combine` | [PhotoRowViewModelTests](./SwiftMVVMDemoTests/PhotosTests/PhotoRowViewModelTests.swift) | 57 | 58 | 59 | --- 60 | 61 | ### Other 62 | 63 | | Feature | Description | ![Checkmark](https://img.shields.io/badge/-✔-green) Example | 64 | |------------------------------|-------------------------------------------------------------------------------------|---------------------------------------------------------| 65 | | UIKit + SwiftUI | Provides examples showcasing the use of a single **ViewModel** in both UIKit and SwiftUI. | [PhotosViewModel](./SwiftMVVMDemo/Views/Photos/PhotosViewModel.swift)
[PhotosViewController](./SwiftMVVMDemo/Views/Photos/UIKit/PhotosViewController.swift)
[PhotosCustomAsyncImageView](./SwiftMVVMDemo/Views/Photos/SwiftUI/PhotosCustomAsyncImageView.swift) | 66 | | Swift Package | Create and distribute your own Swift Package and use it in your project. | [NetworkKit](https://github.com/rushisangani/NetworkKit) | 67 | | Async-Await Usage | Shows how to use async-await for asynchronous operations. | [APIRequestHandler](https://github.com/rushisangani/NetworkKit/blob/main/Sources/NetworkKit/APIRequestHandler.swift)
[PostListViewModel](./SwiftMVVMDemo/Views/PostList/PostListViewModel.swift) | 68 | | Combine Framework | Examples of using Combine for reactive programming. | [AsyncImageLoader](./SwiftMVVMDemo/Helpers/Image%20Downloading/AsyncImageLoader.swift)
[PhotoRowViewModel](./SwiftMVVMDemo/Views/Photos/PhotoRowViewModel.swift) | 69 | | Unit Tests | Comprehensive unit tests ensure code reliability and functionality. | [PostListViewControllerTests](./SwiftMVVMDemoTests/PostTests/PostListViewControllerTests.swift)
[APIRequestHandlerTests](https://github.com/rushisangani/NetworkKit/blob/main/Tests/NetworkKitTests/APIRequestHandlerTests.swift)
[PostServiceTests](./SwiftMVVMDemoTests/PostTests/PostServiceTests.swift) | 70 | 71 | 72 | ## Getting Started 73 | 74 | To get started with this demo project, follow these steps: 75 | 76 | 1. **Clone the Repository**: Clone this repository to your local machine using `git clone https://github.com/rushisangani/SwiftMVVMDemo.git`. 77 | 2. **Explore the Code**: Dive into the codebase to explore the various concepts implemented. 78 | 3. **Run the Project**: Run the project in Xcode to see the sample app in action. 79 | 4. **Check Unit Tests**: Review the unit tests to understand how different functionalities are tested. 80 | 81 | ## Contribution 82 | 83 | Contributions to enhance or expand this project are welcome! Feel free to submit issues, propose enhancements, or create pull requests to collaborate on improving this sample app further. 84 | 85 | ## Connect 86 | 87 | Connect with me on [LinkedIn](https://www.linkedin.com/in/rushisangani/) or follow me on [Medium](https://medium.com/@rushisangani). 88 | -------------------------------------------------------------------------------- /SwiftMVVMDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | A27997A02B4D9DCA0036C648 /* CacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A279979F2B4D9DCA0036C648 /* CacheManager.swift */; }; 11 | A27997AB2B4E97470036C648 /* Comment.swift in Sources */ = {isa = PBXBuildFile; fileRef = A27997AA2B4E97470036C648 /* Comment.swift */; }; 12 | A27997B12B4EB0900036C648 /* CacheManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A27997B02B4EB0900036C648 /* CacheManagerTests.swift */; }; 13 | A27997B32B4EC1940036C648 /* AsyncImageLoaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A27997B22B4EC1940036C648 /* AsyncImageLoaderTests.swift */; }; 14 | A27997C22B4F08F30036C648 /* PostListViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A27997C12B4F08F30036C648 /* PostListViewTests.swift */; }; 15 | A27997C72B500D780036C648 /* PhotoRowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A27997C62B500D780036C648 /* PhotoRowViewModel.swift */; }; 16 | A27997C92B5069010036C648 /* PhotoRowViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A27997C82B5069010036C648 /* PhotoRowViewModelTests.swift */; }; 17 | A279982D2B56B4E00036C648 /* PostCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A279982C2B56B4E00036C648 /* PostCoordinatorTests.swift */; }; 18 | A27998332B581F030036C648 /* NetworkKit in Frameworks */ = {isa = PBXBuildFile; productRef = A27998322B581F030036C648 /* NetworkKit */; }; 19 | A27998352B58200D0036C648 /* posts.json in Resources */ = {isa = PBXBuildFile; fileRef = A27998342B58200D0036C648 /* posts.json */; }; 20 | A27998372B5823160036C648 /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = A27998362B5823160036C648 /* Bundle.swift */; }; 21 | A279983A2B5829FE0036C648 /* NetworkKit in Frameworks */ = {isa = PBXBuildFile; productRef = A27998392B5829FE0036C648 /* NetworkKit */; }; 22 | A28F6B962B1F490900F858E4 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A28F6B952B1F490900F858E4 /* AppDelegate.swift */; }; 23 | A28F6B9D2B1F490900F858E4 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A28F6B9B2B1F490900F858E4 /* Main.storyboard */; }; 24 | A28F6B9F2B1F490B00F858E4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A28F6B9E2B1F490B00F858E4 /* Assets.xcassets */; }; 25 | A28F6BA22B1F490B00F858E4 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A28F6BA02B1F490B00F858E4 /* LaunchScreen.storyboard */; }; 26 | A28F6BB72B1F490B00F858E4 /* SwiftMVVMDemoUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A28F6BB62B1F490B00F858E4 /* SwiftMVVMDemoUITests.swift */; }; 27 | A28F6BB92B1F490B00F858E4 /* SwiftMVVMDemoUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A28F6BB82B1F490B00F858E4 /* SwiftMVVMDemoUITestsLaunchTests.swift */; }; 28 | A28F6BEE2B1F499300F858E4 /* Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = A28F6BDE2B1F499300F858E4 /* Post.swift */; }; 29 | A28F6BF72B1F499300F858E4 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = A28F6BE92B1F499300F858E4 /* Constants.swift */; }; 30 | A28F6C0A2B1F7C4300F858E4 /* PostListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A28F6C092B1F7C4300F858E4 /* PostListViewModel.swift */; }; 31 | A28F6C0C2B1F7C6600F858E4 /* PostListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A28F6C0B2B1F7C6600F858E4 /* PostListViewController.swift */; }; 32 | A28F6C0E2B1FA9FB00F858E4 /* PostServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A28F6C0D2B1FA9FB00F858E4 /* PostServiceTests.swift */; }; 33 | A28F6C102B1FAAB500F858E4 /* PostService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A28F6C0F2B1FAAB500F858E4 /* PostService.swift */; }; 34 | A28F6C1E2B20BA5800F858E4 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A28F6C1D2B20BA5800F858E4 /* ViewController.swift */; }; 35 | A28F6C212B20BE1500F858E4 /* UITableView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A28F6C202B20BE1500F858E4 /* UITableView+Extension.swift */; }; 36 | A28F6C232B20BE9800F858E4 /* UIView+Constraints.swift in Sources */ = {isa = PBXBuildFile; fileRef = A28F6C222B20BE9800F858E4 /* UIView+Constraints.swift */; }; 37 | A28F6C252B20C1A100F858E4 /* PostListViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A28F6C242B20C1A100F858E4 /* PostListViewModelTests.swift */; }; 38 | A28F6C292B20CC4300F858E4 /* PostListViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A28F6C282B20CC4300F858E4 /* PostListViewControllerTests.swift */; }; 39 | A28F6C2B2B21F82E00F858E4 /* PostListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A28F6C2A2B21F82E00F858E4 /* PostListView.swift */; }; 40 | A28F6C312B2310D600F858E4 /* Photo.swift in Sources */ = {isa = PBXBuildFile; fileRef = A28F6C302B2310D600F858E4 /* Photo.swift */; }; 41 | A28F6C332B23111200F858E4 /* PhotoService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A28F6C322B23111200F858E4 /* PhotoService.swift */; }; 42 | A28F6C362B23124800F858E4 /* PhotosViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A28F6C352B23124800F858E4 /* PhotosViewModel.swift */; }; 43 | A28F6C382B23125300F858E4 /* PhotosViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A28F6C372B23125300F858E4 /* PhotosViewController.swift */; }; 44 | A28F6C3A2B23154C00F858E4 /* PhotoTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A28F6C392B23154C00F858E4 /* PhotoTableViewCell.swift */; }; 45 | A28F6C402B2363ED00F858E4 /* PhotosAsyncImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A28F6C3F2B2363ED00F858E4 /* PhotosAsyncImageView.swift */; }; 46 | A28F6C422B236B3A00F858E4 /* PhotosCustomAsyncImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A28F6C412B236B3A00F858E4 /* PhotosCustomAsyncImageView.swift */; }; 47 | A28F6C442B236B8400F858E4 /* AsyncImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A28F6C432B236B8400F858E4 /* AsyncImageLoader.swift */; }; 48 | A2A63D392B289426002C8DAC /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A63D382B289426002C8DAC /* AppCoordinator.swift */; }; 49 | A2A63D3B2B2895A7002C8DAC /* UIStoryboard+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A63D3A2B2895A7002C8DAC /* UIStoryboard+Extension.swift */; }; 50 | A2A63D3D2B289980002C8DAC /* PostCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A63D3C2B289980002C8DAC /* PostCoordinator.swift */; }; 51 | A2A63D422B28B559002C8DAC /* PhotosCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A63D412B28B559002C8DAC /* PhotosCoordinator.swift */; }; 52 | /* End PBXBuildFile section */ 53 | 54 | /* Begin PBXContainerItemProxy section */ 55 | A28F6BA92B1F490B00F858E4 /* PBXContainerItemProxy */ = { 56 | isa = PBXContainerItemProxy; 57 | containerPortal = A28F6B8A2B1F490900F858E4 /* Project object */; 58 | proxyType = 1; 59 | remoteGlobalIDString = A28F6B912B1F490900F858E4; 60 | remoteInfo = SwiftMVVMDemo; 61 | }; 62 | A28F6BB32B1F490B00F858E4 /* PBXContainerItemProxy */ = { 63 | isa = PBXContainerItemProxy; 64 | containerPortal = A28F6B8A2B1F490900F858E4 /* Project object */; 65 | proxyType = 1; 66 | remoteGlobalIDString = A28F6B912B1F490900F858E4; 67 | remoteInfo = SwiftMVVMDemo; 68 | }; 69 | /* End PBXContainerItemProxy section */ 70 | 71 | /* Begin PBXFileReference section */ 72 | A279979F2B4D9DCA0036C648 /* CacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheManager.swift; sourceTree = ""; }; 73 | A27997AA2B4E97470036C648 /* Comment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Comment.swift; sourceTree = ""; }; 74 | A27997B02B4EB0900036C648 /* CacheManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheManagerTests.swift; sourceTree = ""; }; 75 | A27997B22B4EC1940036C648 /* AsyncImageLoaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncImageLoaderTests.swift; sourceTree = ""; }; 76 | A27997C12B4F08F30036C648 /* PostListViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostListViewTests.swift; sourceTree = ""; }; 77 | A27997C62B500D780036C648 /* PhotoRowViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoRowViewModel.swift; sourceTree = ""; }; 78 | A27997C82B5069010036C648 /* PhotoRowViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoRowViewModelTests.swift; sourceTree = ""; }; 79 | A279982C2B56B4E00036C648 /* PostCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostCoordinatorTests.swift; sourceTree = ""; }; 80 | A27998342B58200D0036C648 /* posts.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = posts.json; sourceTree = ""; }; 81 | A27998362B5823160036C648 /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = ""; }; 82 | A28F6B922B1F490900F858E4 /* SwiftMVVMDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftMVVMDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 83 | A28F6B952B1F490900F858E4 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 84 | A28F6B9C2B1F490900F858E4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 85 | A28F6B9E2B1F490B00F858E4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 86 | A28F6BA12B1F490B00F858E4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 87 | A28F6BA32B1F490B00F858E4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 88 | A28F6BA82B1F490B00F858E4 /* SwiftMVVMDemoTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftMVVMDemoTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 89 | A28F6BB22B1F490B00F858E4 /* SwiftMVVMDemoUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftMVVMDemoUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 90 | A28F6BB62B1F490B00F858E4 /* SwiftMVVMDemoUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftMVVMDemoUITests.swift; sourceTree = ""; }; 91 | A28F6BB82B1F490B00F858E4 /* SwiftMVVMDemoUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftMVVMDemoUITestsLaunchTests.swift; sourceTree = ""; }; 92 | A28F6BDE2B1F499300F858E4 /* Post.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Post.swift; sourceTree = ""; }; 93 | A28F6BE92B1F499300F858E4 /* Constants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 94 | A28F6C092B1F7C4300F858E4 /* PostListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostListViewModel.swift; sourceTree = ""; }; 95 | A28F6C0B2B1F7C6600F858E4 /* PostListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostListViewController.swift; sourceTree = ""; }; 96 | A28F6C0D2B1FA9FB00F858E4 /* PostServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostServiceTests.swift; sourceTree = ""; }; 97 | A28F6C0F2B1FAAB500F858E4 /* PostService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostService.swift; sourceTree = ""; }; 98 | A28F6C1D2B20BA5800F858E4 /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 99 | A28F6C202B20BE1500F858E4 /* UITableView+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableView+Extension.swift"; sourceTree = ""; }; 100 | A28F6C222B20BE9800F858E4 /* UIView+Constraints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Constraints.swift"; sourceTree = ""; }; 101 | A28F6C242B20C1A100F858E4 /* PostListViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostListViewModelTests.swift; sourceTree = ""; }; 102 | A28F6C282B20CC4300F858E4 /* PostListViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostListViewControllerTests.swift; sourceTree = ""; }; 103 | A28F6C2A2B21F82E00F858E4 /* PostListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostListView.swift; sourceTree = ""; }; 104 | A28F6C302B2310D600F858E4 /* Photo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Photo.swift; sourceTree = ""; }; 105 | A28F6C322B23111200F858E4 /* PhotoService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoService.swift; sourceTree = ""; }; 106 | A28F6C352B23124800F858E4 /* PhotosViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotosViewModel.swift; sourceTree = ""; }; 107 | A28F6C372B23125300F858E4 /* PhotosViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotosViewController.swift; sourceTree = ""; }; 108 | A28F6C392B23154C00F858E4 /* PhotoTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoTableViewCell.swift; sourceTree = ""; }; 109 | A28F6C3F2B2363ED00F858E4 /* PhotosAsyncImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotosAsyncImageView.swift; sourceTree = ""; }; 110 | A28F6C412B236B3A00F858E4 /* PhotosCustomAsyncImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotosCustomAsyncImageView.swift; sourceTree = ""; }; 111 | A28F6C432B236B8400F858E4 /* AsyncImageLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncImageLoader.swift; sourceTree = ""; }; 112 | A2A63D382B289426002C8DAC /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = ""; }; 113 | A2A63D3A2B2895A7002C8DAC /* UIStoryboard+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIStoryboard+Extension.swift"; sourceTree = ""; }; 114 | A2A63D3C2B289980002C8DAC /* PostCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostCoordinator.swift; sourceTree = ""; }; 115 | A2A63D412B28B559002C8DAC /* PhotosCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotosCoordinator.swift; sourceTree = ""; }; 116 | /* End PBXFileReference section */ 117 | 118 | /* Begin PBXFrameworksBuildPhase section */ 119 | A28F6B8F2B1F490900F858E4 /* Frameworks */ = { 120 | isa = PBXFrameworksBuildPhase; 121 | buildActionMask = 2147483647; 122 | files = ( 123 | A279983A2B5829FE0036C648 /* NetworkKit in Frameworks */, 124 | A27998332B581F030036C648 /* NetworkKit in Frameworks */, 125 | ); 126 | runOnlyForDeploymentPostprocessing = 0; 127 | }; 128 | A28F6BA52B1F490B00F858E4 /* Frameworks */ = { 129 | isa = PBXFrameworksBuildPhase; 130 | buildActionMask = 2147483647; 131 | files = ( 132 | ); 133 | runOnlyForDeploymentPostprocessing = 0; 134 | }; 135 | A28F6BAF2B1F490B00F858E4 /* Frameworks */ = { 136 | isa = PBXFrameworksBuildPhase; 137 | buildActionMask = 2147483647; 138 | files = ( 139 | ); 140 | runOnlyForDeploymentPostprocessing = 0; 141 | }; 142 | /* End PBXFrameworksBuildPhase section */ 143 | 144 | /* Begin PBXGroup section */ 145 | A27997A62B4E95050036C648 /* UIKit Views */ = { 146 | isa = PBXGroup; 147 | children = ( 148 | A28F6C372B23125300F858E4 /* PhotosViewController.swift */, 149 | A2A63D412B28B559002C8DAC /* PhotosCoordinator.swift */, 150 | ); 151 | path = "UIKit Views"; 152 | sourceTree = ""; 153 | }; 154 | A27997A72B4E950C0036C648 /* SwiftUI Views */ = { 155 | isa = PBXGroup; 156 | children = ( 157 | A28F6C3F2B2363ED00F858E4 /* PhotosAsyncImageView.swift */, 158 | A28F6C412B236B3A00F858E4 /* PhotosCustomAsyncImageView.swift */, 159 | ); 160 | path = "SwiftUI Views"; 161 | sourceTree = ""; 162 | }; 163 | A27997A82B4E95440036C648 /* UIKit Views */ = { 164 | isa = PBXGroup; 165 | children = ( 166 | A28F6C0B2B1F7C6600F858E4 /* PostListViewController.swift */, 167 | A2A63D3C2B289980002C8DAC /* PostCoordinator.swift */, 168 | ); 169 | path = "UIKit Views"; 170 | sourceTree = ""; 171 | }; 172 | A27997A92B4E954B0036C648 /* SwiftUI Views */ = { 173 | isa = PBXGroup; 174 | children = ( 175 | A28F6C2A2B21F82E00F858E4 /* PostListView.swift */, 176 | ); 177 | path = "SwiftUI Views"; 178 | sourceTree = ""; 179 | }; 180 | A27997AC2B4E978C0036C648 /* PhotosTests */ = { 181 | isa = PBXGroup; 182 | children = ( 183 | A27997C82B5069010036C648 /* PhotoRowViewModelTests.swift */, 184 | A27997AD2B4E9A610036C648 /* ImageDownloadingTests */, 185 | ); 186 | path = PhotosTests; 187 | sourceTree = ""; 188 | }; 189 | A27997AD2B4E9A610036C648 /* ImageDownloadingTests */ = { 190 | isa = PBXGroup; 191 | children = ( 192 | A27997B22B4EC1940036C648 /* AsyncImageLoaderTests.swift */, 193 | A27997B02B4EB0900036C648 /* CacheManagerTests.swift */, 194 | ); 195 | path = ImageDownloadingTests; 196 | sourceTree = ""; 197 | }; 198 | A279B8B72B6E25D10069E16C /* Modules */ = { 199 | isa = PBXGroup; 200 | children = ( 201 | A279B8B82B6E25DB0069E16C /* Posts */, 202 | A279B8B92B6E25E00069E16C /* Photos */, 203 | A279B8BC2B6E26C20069E16C /* Comments */, 204 | ); 205 | path = Modules; 206 | sourceTree = ""; 207 | }; 208 | A279B8B82B6E25DB0069E16C /* Posts */ = { 209 | isa = PBXGroup; 210 | children = ( 211 | A28F6BDE2B1F499300F858E4 /* Post.swift */, 212 | A28F6C0F2B1FAAB500F858E4 /* PostService.swift */, 213 | A28F6C092B1F7C4300F858E4 /* PostListViewModel.swift */, 214 | A27997A82B4E95440036C648 /* UIKit Views */, 215 | A27997A92B4E954B0036C648 /* SwiftUI Views */, 216 | ); 217 | path = Posts; 218 | sourceTree = ""; 219 | }; 220 | A279B8B92B6E25E00069E16C /* Photos */ = { 221 | isa = PBXGroup; 222 | children = ( 223 | A28F6C302B2310D600F858E4 /* Photo.swift */, 224 | A28F6C322B23111200F858E4 /* PhotoService.swift */, 225 | A28F6C352B23124800F858E4 /* PhotosViewModel.swift */, 226 | A279B8BA2B6E26410069E16C /* Photo Row */, 227 | A27997A62B4E95050036C648 /* UIKit Views */, 228 | A27997A72B4E950C0036C648 /* SwiftUI Views */, 229 | A279B8BD2B6E27330069E16C /* Image Loader */, 230 | ); 231 | path = Photos; 232 | sourceTree = ""; 233 | }; 234 | A279B8BA2B6E26410069E16C /* Photo Row */ = { 235 | isa = PBXGroup; 236 | children = ( 237 | A28F6C392B23154C00F858E4 /* PhotoTableViewCell.swift */, 238 | A27997C62B500D780036C648 /* PhotoRowViewModel.swift */, 239 | ); 240 | path = "Photo Row"; 241 | sourceTree = ""; 242 | }; 243 | A279B8BB2B6E26B50069E16C /* Extensions */ = { 244 | isa = PBXGroup; 245 | children = ( 246 | A28F6C202B20BE1500F858E4 /* UITableView+Extension.swift */, 247 | A28F6C222B20BE9800F858E4 /* UIView+Constraints.swift */, 248 | A2A63D3A2B2895A7002C8DAC /* UIStoryboard+Extension.swift */, 249 | ); 250 | path = Extensions; 251 | sourceTree = ""; 252 | }; 253 | A279B8BC2B6E26C20069E16C /* Comments */ = { 254 | isa = PBXGroup; 255 | children = ( 256 | A27997AA2B4E97470036C648 /* Comment.swift */, 257 | ); 258 | path = Comments; 259 | sourceTree = ""; 260 | }; 261 | A279B8BD2B6E27330069E16C /* Image Loader */ = { 262 | isa = PBXGroup; 263 | children = ( 264 | A28F6C432B236B8400F858E4 /* AsyncImageLoader.swift */, 265 | A279979F2B4D9DCA0036C648 /* CacheManager.swift */, 266 | ); 267 | path = "Image Loader"; 268 | sourceTree = ""; 269 | }; 270 | A28F6B892B1F490900F858E4 = { 271 | isa = PBXGroup; 272 | children = ( 273 | A28F6B942B1F490900F858E4 /* SwiftMVVMDemo */, 274 | A28F6BAB2B1F490B00F858E4 /* SwiftMVVMDemoTests */, 275 | A28F6BB52B1F490B00F858E4 /* SwiftMVVMDemoUITests */, 276 | A28F6B932B1F490900F858E4 /* Products */, 277 | ); 278 | sourceTree = ""; 279 | }; 280 | A28F6B932B1F490900F858E4 /* Products */ = { 281 | isa = PBXGroup; 282 | children = ( 283 | A28F6B922B1F490900F858E4 /* SwiftMVVMDemo.app */, 284 | A28F6BA82B1F490B00F858E4 /* SwiftMVVMDemoTests.xctest */, 285 | A28F6BB22B1F490B00F858E4 /* SwiftMVVMDemoUITests.xctest */, 286 | ); 287 | name = Products; 288 | sourceTree = ""; 289 | }; 290 | A28F6B942B1F490900F858E4 /* SwiftMVVMDemo */ = { 291 | isa = PBXGroup; 292 | children = ( 293 | A28F6B952B1F490900F858E4 /* AppDelegate.swift */, 294 | A28F6BE92B1F499300F858E4 /* Constants.swift */, 295 | A2A63D382B289426002C8DAC /* AppCoordinator.swift */, 296 | A28F6C1D2B20BA5800F858E4 /* ViewController.swift */, 297 | A279B8B72B6E25D10069E16C /* Modules */, 298 | A279B8BB2B6E26B50069E16C /* Extensions */, 299 | A28F6B9B2B1F490900F858E4 /* Main.storyboard */, 300 | A28F6B9E2B1F490B00F858E4 /* Assets.xcassets */, 301 | A28F6BA02B1F490B00F858E4 /* LaunchScreen.storyboard */, 302 | A28F6BA32B1F490B00F858E4 /* Info.plist */, 303 | ); 304 | path = SwiftMVVMDemo; 305 | sourceTree = ""; 306 | }; 307 | A28F6BAB2B1F490B00F858E4 /* SwiftMVVMDemoTests */ = { 308 | isa = PBXGroup; 309 | children = ( 310 | A28F6C272B20CC2C00F858E4 /* PostTests */, 311 | A27997AC2B4E978C0036C648 /* PhotosTests */, 312 | A28F6BCE2B1F498000F858E4 /* Resources */, 313 | ); 314 | path = SwiftMVVMDemoTests; 315 | sourceTree = ""; 316 | }; 317 | A28F6BB52B1F490B00F858E4 /* SwiftMVVMDemoUITests */ = { 318 | isa = PBXGroup; 319 | children = ( 320 | A28F6BB62B1F490B00F858E4 /* SwiftMVVMDemoUITests.swift */, 321 | A28F6BB82B1F490B00F858E4 /* SwiftMVVMDemoUITestsLaunchTests.swift */, 322 | ); 323 | path = SwiftMVVMDemoUITests; 324 | sourceTree = ""; 325 | }; 326 | A28F6BCE2B1F498000F858E4 /* Resources */ = { 327 | isa = PBXGroup; 328 | children = ( 329 | A27998342B58200D0036C648 /* posts.json */, 330 | A27998362B5823160036C648 /* Bundle.swift */, 331 | ); 332 | path = Resources; 333 | sourceTree = ""; 334 | }; 335 | A28F6C272B20CC2C00F858E4 /* PostTests */ = { 336 | isa = PBXGroup; 337 | children = ( 338 | A28F6C0D2B1FA9FB00F858E4 /* PostServiceTests.swift */, 339 | A28F6C242B20C1A100F858E4 /* PostListViewModelTests.swift */, 340 | A28F6C282B20CC4300F858E4 /* PostListViewControllerTests.swift */, 341 | A27997C12B4F08F30036C648 /* PostListViewTests.swift */, 342 | A279982C2B56B4E00036C648 /* PostCoordinatorTests.swift */, 343 | ); 344 | path = PostTests; 345 | sourceTree = ""; 346 | }; 347 | /* End PBXGroup section */ 348 | 349 | /* Begin PBXNativeTarget section */ 350 | A28F6B912B1F490900F858E4 /* SwiftMVVMDemo */ = { 351 | isa = PBXNativeTarget; 352 | buildConfigurationList = A28F6BBC2B1F490B00F858E4 /* Build configuration list for PBXNativeTarget "SwiftMVVMDemo" */; 353 | buildPhases = ( 354 | A28F6B8E2B1F490900F858E4 /* Sources */, 355 | A28F6B8F2B1F490900F858E4 /* Frameworks */, 356 | A28F6B902B1F490900F858E4 /* Resources */, 357 | ); 358 | buildRules = ( 359 | ); 360 | dependencies = ( 361 | ); 362 | name = SwiftMVVMDemo; 363 | packageProductDependencies = ( 364 | A27998322B581F030036C648 /* NetworkKit */, 365 | A27998392B5829FE0036C648 /* NetworkKit */, 366 | ); 367 | productName = SwiftMVVMDemo; 368 | productReference = A28F6B922B1F490900F858E4 /* SwiftMVVMDemo.app */; 369 | productType = "com.apple.product-type.application"; 370 | }; 371 | A28F6BA72B1F490B00F858E4 /* SwiftMVVMDemoTests */ = { 372 | isa = PBXNativeTarget; 373 | buildConfigurationList = A28F6BBF2B1F490B00F858E4 /* Build configuration list for PBXNativeTarget "SwiftMVVMDemoTests" */; 374 | buildPhases = ( 375 | A28F6BA42B1F490B00F858E4 /* Sources */, 376 | A28F6BA52B1F490B00F858E4 /* Frameworks */, 377 | A28F6BA62B1F490B00F858E4 /* Resources */, 378 | ); 379 | buildRules = ( 380 | ); 381 | dependencies = ( 382 | A28F6BAA2B1F490B00F858E4 /* PBXTargetDependency */, 383 | ); 384 | name = SwiftMVVMDemoTests; 385 | productName = SwiftMVVMDemoTests; 386 | productReference = A28F6BA82B1F490B00F858E4 /* SwiftMVVMDemoTests.xctest */; 387 | productType = "com.apple.product-type.bundle.unit-test"; 388 | }; 389 | A28F6BB12B1F490B00F858E4 /* SwiftMVVMDemoUITests */ = { 390 | isa = PBXNativeTarget; 391 | buildConfigurationList = A28F6BC22B1F490B00F858E4 /* Build configuration list for PBXNativeTarget "SwiftMVVMDemoUITests" */; 392 | buildPhases = ( 393 | A28F6BAE2B1F490B00F858E4 /* Sources */, 394 | A28F6BAF2B1F490B00F858E4 /* Frameworks */, 395 | A28F6BB02B1F490B00F858E4 /* Resources */, 396 | ); 397 | buildRules = ( 398 | ); 399 | dependencies = ( 400 | A28F6BB42B1F490B00F858E4 /* PBXTargetDependency */, 401 | ); 402 | name = SwiftMVVMDemoUITests; 403 | productName = SwiftMVVMDemoUITests; 404 | productReference = A28F6BB22B1F490B00F858E4 /* SwiftMVVMDemoUITests.xctest */; 405 | productType = "com.apple.product-type.bundle.ui-testing"; 406 | }; 407 | /* End PBXNativeTarget section */ 408 | 409 | /* Begin PBXProject section */ 410 | A28F6B8A2B1F490900F858E4 /* Project object */ = { 411 | isa = PBXProject; 412 | attributes = { 413 | BuildIndependentTargetsInParallel = 1; 414 | LastSwiftUpdateCheck = 1500; 415 | LastUpgradeCheck = 1500; 416 | TargetAttributes = { 417 | A28F6B912B1F490900F858E4 = { 418 | CreatedOnToolsVersion = 15.0; 419 | }; 420 | A28F6BA72B1F490B00F858E4 = { 421 | CreatedOnToolsVersion = 15.0; 422 | LastSwiftMigration = 1500; 423 | TestTargetID = A28F6B912B1F490900F858E4; 424 | }; 425 | A28F6BB12B1F490B00F858E4 = { 426 | CreatedOnToolsVersion = 15.0; 427 | TestTargetID = A28F6B912B1F490900F858E4; 428 | }; 429 | }; 430 | }; 431 | buildConfigurationList = A28F6B8D2B1F490900F858E4 /* Build configuration list for PBXProject "SwiftMVVMDemo" */; 432 | compatibilityVersion = "Xcode 14.0"; 433 | developmentRegion = en; 434 | hasScannedForEncodings = 0; 435 | knownRegions = ( 436 | en, 437 | Base, 438 | ); 439 | mainGroup = A28F6B892B1F490900F858E4; 440 | packageReferences = ( 441 | A27998382B5829FE0036C648 /* XCRemoteSwiftPackageReference "NetworkKit" */, 442 | ); 443 | productRefGroup = A28F6B932B1F490900F858E4 /* Products */; 444 | projectDirPath = ""; 445 | projectRoot = ""; 446 | targets = ( 447 | A28F6B912B1F490900F858E4 /* SwiftMVVMDemo */, 448 | A28F6BA72B1F490B00F858E4 /* SwiftMVVMDemoTests */, 449 | A28F6BB12B1F490B00F858E4 /* SwiftMVVMDemoUITests */, 450 | ); 451 | }; 452 | /* End PBXProject section */ 453 | 454 | /* Begin PBXResourcesBuildPhase section */ 455 | A28F6B902B1F490900F858E4 /* Resources */ = { 456 | isa = PBXResourcesBuildPhase; 457 | buildActionMask = 2147483647; 458 | files = ( 459 | A28F6BA22B1F490B00F858E4 /* LaunchScreen.storyboard in Resources */, 460 | A28F6B9F2B1F490B00F858E4 /* Assets.xcassets in Resources */, 461 | A28F6B9D2B1F490900F858E4 /* Main.storyboard in Resources */, 462 | ); 463 | runOnlyForDeploymentPostprocessing = 0; 464 | }; 465 | A28F6BA62B1F490B00F858E4 /* Resources */ = { 466 | isa = PBXResourcesBuildPhase; 467 | buildActionMask = 2147483647; 468 | files = ( 469 | A27998352B58200D0036C648 /* posts.json in Resources */, 470 | ); 471 | runOnlyForDeploymentPostprocessing = 0; 472 | }; 473 | A28F6BB02B1F490B00F858E4 /* Resources */ = { 474 | isa = PBXResourcesBuildPhase; 475 | buildActionMask = 2147483647; 476 | files = ( 477 | ); 478 | runOnlyForDeploymentPostprocessing = 0; 479 | }; 480 | /* End PBXResourcesBuildPhase section */ 481 | 482 | /* Begin PBXSourcesBuildPhase section */ 483 | A28F6B8E2B1F490900F858E4 /* Sources */ = { 484 | isa = PBXSourcesBuildPhase; 485 | buildActionMask = 2147483647; 486 | files = ( 487 | A2A63D3B2B2895A7002C8DAC /* UIStoryboard+Extension.swift in Sources */, 488 | A28F6C1E2B20BA5800F858E4 /* ViewController.swift in Sources */, 489 | A28F6C102B1FAAB500F858E4 /* PostService.swift in Sources */, 490 | A2A63D392B289426002C8DAC /* AppCoordinator.swift in Sources */, 491 | A28F6C0A2B1F7C4300F858E4 /* PostListViewModel.swift in Sources */, 492 | A28F6C0C2B1F7C6600F858E4 /* PostListViewController.swift in Sources */, 493 | A28F6B962B1F490900F858E4 /* AppDelegate.swift in Sources */, 494 | A28F6C232B20BE9800F858E4 /* UIView+Constraints.swift in Sources */, 495 | A2A63D3D2B289980002C8DAC /* PostCoordinator.swift in Sources */, 496 | A27997C72B500D780036C648 /* PhotoRowViewModel.swift in Sources */, 497 | A28F6BF72B1F499300F858E4 /* Constants.swift in Sources */, 498 | A28F6C3A2B23154C00F858E4 /* PhotoTableViewCell.swift in Sources */, 499 | A28F6C402B2363ED00F858E4 /* PhotosAsyncImageView.swift in Sources */, 500 | A28F6C442B236B8400F858E4 /* AsyncImageLoader.swift in Sources */, 501 | A28F6C312B2310D600F858E4 /* Photo.swift in Sources */, 502 | A28F6BEE2B1F499300F858E4 /* Post.swift in Sources */, 503 | A28F6C2B2B21F82E00F858E4 /* PostListView.swift in Sources */, 504 | A27997A02B4D9DCA0036C648 /* CacheManager.swift in Sources */, 505 | A28F6C362B23124800F858E4 /* PhotosViewModel.swift in Sources */, 506 | A28F6C382B23125300F858E4 /* PhotosViewController.swift in Sources */, 507 | A27997AB2B4E97470036C648 /* Comment.swift in Sources */, 508 | A28F6C332B23111200F858E4 /* PhotoService.swift in Sources */, 509 | A28F6C422B236B3A00F858E4 /* PhotosCustomAsyncImageView.swift in Sources */, 510 | A28F6C212B20BE1500F858E4 /* UITableView+Extension.swift in Sources */, 511 | A2A63D422B28B559002C8DAC /* PhotosCoordinator.swift in Sources */, 512 | ); 513 | runOnlyForDeploymentPostprocessing = 0; 514 | }; 515 | A28F6BA42B1F490B00F858E4 /* Sources */ = { 516 | isa = PBXSourcesBuildPhase; 517 | buildActionMask = 2147483647; 518 | files = ( 519 | A27997C22B4F08F30036C648 /* PostListViewTests.swift in Sources */, 520 | A27998372B5823160036C648 /* Bundle.swift in Sources */, 521 | A27997B32B4EC1940036C648 /* AsyncImageLoaderTests.swift in Sources */, 522 | A28F6C0E2B1FA9FB00F858E4 /* PostServiceTests.swift in Sources */, 523 | A28F6C292B20CC4300F858E4 /* PostListViewControllerTests.swift in Sources */, 524 | A27997C92B5069010036C648 /* PhotoRowViewModelTests.swift in Sources */, 525 | A279982D2B56B4E00036C648 /* PostCoordinatorTests.swift in Sources */, 526 | A28F6C252B20C1A100F858E4 /* PostListViewModelTests.swift in Sources */, 527 | A27997B12B4EB0900036C648 /* CacheManagerTests.swift in Sources */, 528 | ); 529 | runOnlyForDeploymentPostprocessing = 0; 530 | }; 531 | A28F6BAE2B1F490B00F858E4 /* Sources */ = { 532 | isa = PBXSourcesBuildPhase; 533 | buildActionMask = 2147483647; 534 | files = ( 535 | A28F6BB72B1F490B00F858E4 /* SwiftMVVMDemoUITests.swift in Sources */, 536 | A28F6BB92B1F490B00F858E4 /* SwiftMVVMDemoUITestsLaunchTests.swift in Sources */, 537 | ); 538 | runOnlyForDeploymentPostprocessing = 0; 539 | }; 540 | /* End PBXSourcesBuildPhase section */ 541 | 542 | /* Begin PBXTargetDependency section */ 543 | A28F6BAA2B1F490B00F858E4 /* PBXTargetDependency */ = { 544 | isa = PBXTargetDependency; 545 | target = A28F6B912B1F490900F858E4 /* SwiftMVVMDemo */; 546 | targetProxy = A28F6BA92B1F490B00F858E4 /* PBXContainerItemProxy */; 547 | }; 548 | A28F6BB42B1F490B00F858E4 /* PBXTargetDependency */ = { 549 | isa = PBXTargetDependency; 550 | target = A28F6B912B1F490900F858E4 /* SwiftMVVMDemo */; 551 | targetProxy = A28F6BB32B1F490B00F858E4 /* PBXContainerItemProxy */; 552 | }; 553 | /* End PBXTargetDependency section */ 554 | 555 | /* Begin PBXVariantGroup section */ 556 | A28F6B9B2B1F490900F858E4 /* Main.storyboard */ = { 557 | isa = PBXVariantGroup; 558 | children = ( 559 | A28F6B9C2B1F490900F858E4 /* Base */, 560 | ); 561 | name = Main.storyboard; 562 | sourceTree = ""; 563 | }; 564 | A28F6BA02B1F490B00F858E4 /* LaunchScreen.storyboard */ = { 565 | isa = PBXVariantGroup; 566 | children = ( 567 | A28F6BA12B1F490B00F858E4 /* Base */, 568 | ); 569 | name = LaunchScreen.storyboard; 570 | sourceTree = ""; 571 | }; 572 | /* End PBXVariantGroup section */ 573 | 574 | /* Begin XCBuildConfiguration section */ 575 | A28F6BBA2B1F490B00F858E4 /* Debug */ = { 576 | isa = XCBuildConfiguration; 577 | buildSettings = { 578 | ALWAYS_SEARCH_USER_PATHS = NO; 579 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 580 | CLANG_ANALYZER_NONNULL = YES; 581 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 582 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 583 | CLANG_ENABLE_MODULES = YES; 584 | CLANG_ENABLE_OBJC_ARC = YES; 585 | CLANG_ENABLE_OBJC_WEAK = YES; 586 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 587 | CLANG_WARN_BOOL_CONVERSION = YES; 588 | CLANG_WARN_COMMA = YES; 589 | CLANG_WARN_CONSTANT_CONVERSION = YES; 590 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 591 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 592 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 593 | CLANG_WARN_EMPTY_BODY = YES; 594 | CLANG_WARN_ENUM_CONVERSION = YES; 595 | CLANG_WARN_INFINITE_RECURSION = YES; 596 | CLANG_WARN_INT_CONVERSION = YES; 597 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 598 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 599 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 600 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 601 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 602 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 603 | CLANG_WARN_STRICT_PROTOTYPES = YES; 604 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 605 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 606 | CLANG_WARN_UNREACHABLE_CODE = YES; 607 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 608 | COPY_PHASE_STRIP = NO; 609 | DEBUG_INFORMATION_FORMAT = dwarf; 610 | ENABLE_STRICT_OBJC_MSGSEND = YES; 611 | ENABLE_TESTABILITY = YES; 612 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 613 | GCC_C_LANGUAGE_STANDARD = gnu17; 614 | GCC_DYNAMIC_NO_PIC = NO; 615 | GCC_NO_COMMON_BLOCKS = YES; 616 | GCC_OPTIMIZATION_LEVEL = 0; 617 | GCC_PREPROCESSOR_DEFINITIONS = ( 618 | "DEBUG=1", 619 | "$(inherited)", 620 | ); 621 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 622 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 623 | GCC_WARN_UNDECLARED_SELECTOR = YES; 624 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 625 | GCC_WARN_UNUSED_FUNCTION = YES; 626 | GCC_WARN_UNUSED_VARIABLE = YES; 627 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 628 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 629 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 630 | MTL_FAST_MATH = YES; 631 | ONLY_ACTIVE_ARCH = YES; 632 | SDKROOT = iphoneos; 633 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 634 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 635 | }; 636 | name = Debug; 637 | }; 638 | A28F6BBB2B1F490B00F858E4 /* Release */ = { 639 | isa = XCBuildConfiguration; 640 | buildSettings = { 641 | ALWAYS_SEARCH_USER_PATHS = NO; 642 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 643 | CLANG_ANALYZER_NONNULL = YES; 644 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 645 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 646 | CLANG_ENABLE_MODULES = YES; 647 | CLANG_ENABLE_OBJC_ARC = YES; 648 | CLANG_ENABLE_OBJC_WEAK = YES; 649 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 650 | CLANG_WARN_BOOL_CONVERSION = YES; 651 | CLANG_WARN_COMMA = YES; 652 | CLANG_WARN_CONSTANT_CONVERSION = YES; 653 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 654 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 655 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 656 | CLANG_WARN_EMPTY_BODY = YES; 657 | CLANG_WARN_ENUM_CONVERSION = YES; 658 | CLANG_WARN_INFINITE_RECURSION = YES; 659 | CLANG_WARN_INT_CONVERSION = YES; 660 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 661 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 662 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 663 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 664 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 665 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 666 | CLANG_WARN_STRICT_PROTOTYPES = YES; 667 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 668 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 669 | CLANG_WARN_UNREACHABLE_CODE = YES; 670 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 671 | COPY_PHASE_STRIP = NO; 672 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 673 | ENABLE_NS_ASSERTIONS = NO; 674 | ENABLE_STRICT_OBJC_MSGSEND = YES; 675 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 676 | GCC_C_LANGUAGE_STANDARD = gnu17; 677 | GCC_NO_COMMON_BLOCKS = YES; 678 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 679 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 680 | GCC_WARN_UNDECLARED_SELECTOR = YES; 681 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 682 | GCC_WARN_UNUSED_FUNCTION = YES; 683 | GCC_WARN_UNUSED_VARIABLE = YES; 684 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 685 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 686 | MTL_ENABLE_DEBUG_INFO = NO; 687 | MTL_FAST_MATH = YES; 688 | SDKROOT = iphoneos; 689 | SWIFT_COMPILATION_MODE = wholemodule; 690 | VALIDATE_PRODUCT = YES; 691 | }; 692 | name = Release; 693 | }; 694 | A28F6BBD2B1F490B00F858E4 /* Debug */ = { 695 | isa = XCBuildConfiguration; 696 | buildSettings = { 697 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 698 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 699 | CODE_SIGN_STYLE = Automatic; 700 | CURRENT_PROJECT_VERSION = 1; 701 | DEVELOPMENT_TEAM = H535R5Z6H3; 702 | GENERATE_INFOPLIST_FILE = YES; 703 | INFOPLIST_FILE = SwiftMVVMDemo/Info.plist; 704 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 705 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 706 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 707 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 708 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 709 | LD_RUNPATH_SEARCH_PATHS = ( 710 | "$(inherited)", 711 | "@executable_path/Frameworks", 712 | ); 713 | MARKETING_VERSION = 1.0; 714 | PRODUCT_BUNDLE_IDENTIFIER = com.rushi.SwiftMVVMDemo; 715 | PRODUCT_NAME = "$(TARGET_NAME)"; 716 | SWIFT_EMIT_LOC_STRINGS = YES; 717 | SWIFT_VERSION = 5.0; 718 | TARGETED_DEVICE_FAMILY = "1,2"; 719 | }; 720 | name = Debug; 721 | }; 722 | A28F6BBE2B1F490B00F858E4 /* Release */ = { 723 | isa = XCBuildConfiguration; 724 | buildSettings = { 725 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 726 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 727 | CODE_SIGN_STYLE = Automatic; 728 | CURRENT_PROJECT_VERSION = 1; 729 | DEVELOPMENT_TEAM = H535R5Z6H3; 730 | GENERATE_INFOPLIST_FILE = YES; 731 | INFOPLIST_FILE = SwiftMVVMDemo/Info.plist; 732 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 733 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 734 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 735 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 736 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 737 | LD_RUNPATH_SEARCH_PATHS = ( 738 | "$(inherited)", 739 | "@executable_path/Frameworks", 740 | ); 741 | MARKETING_VERSION = 1.0; 742 | PRODUCT_BUNDLE_IDENTIFIER = com.rushi.SwiftMVVMDemo; 743 | PRODUCT_NAME = "$(TARGET_NAME)"; 744 | SWIFT_EMIT_LOC_STRINGS = YES; 745 | SWIFT_VERSION = 5.0; 746 | TARGETED_DEVICE_FAMILY = "1,2"; 747 | }; 748 | name = Release; 749 | }; 750 | A28F6BC02B1F490B00F858E4 /* Debug */ = { 751 | isa = XCBuildConfiguration; 752 | buildSettings = { 753 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 754 | BUNDLE_LOADER = "$(TEST_HOST)"; 755 | CLANG_ENABLE_MODULES = YES; 756 | CODE_SIGN_STYLE = Automatic; 757 | CURRENT_PROJECT_VERSION = 1; 758 | DEVELOPMENT_TEAM = H535R5Z6H3; 759 | GENERATE_INFOPLIST_FILE = YES; 760 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 761 | MARKETING_VERSION = 1.0; 762 | PRODUCT_BUNDLE_IDENTIFIER = com.rushi.SwiftMVVMDemoTests; 763 | PRODUCT_NAME = "$(TARGET_NAME)"; 764 | SWIFT_EMIT_LOC_STRINGS = NO; 765 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 766 | SWIFT_VERSION = 5.0; 767 | TARGETED_DEVICE_FAMILY = "1,2"; 768 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftMVVMDemo.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/SwiftMVVMDemo"; 769 | }; 770 | name = Debug; 771 | }; 772 | A28F6BC12B1F490B00F858E4 /* Release */ = { 773 | isa = XCBuildConfiguration; 774 | buildSettings = { 775 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 776 | BUNDLE_LOADER = "$(TEST_HOST)"; 777 | CLANG_ENABLE_MODULES = YES; 778 | CODE_SIGN_STYLE = Automatic; 779 | CURRENT_PROJECT_VERSION = 1; 780 | DEVELOPMENT_TEAM = H535R5Z6H3; 781 | GENERATE_INFOPLIST_FILE = YES; 782 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 783 | MARKETING_VERSION = 1.0; 784 | PRODUCT_BUNDLE_IDENTIFIER = com.rushi.SwiftMVVMDemoTests; 785 | PRODUCT_NAME = "$(TARGET_NAME)"; 786 | SWIFT_EMIT_LOC_STRINGS = NO; 787 | SWIFT_VERSION = 5.0; 788 | TARGETED_DEVICE_FAMILY = "1,2"; 789 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftMVVMDemo.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/SwiftMVVMDemo"; 790 | }; 791 | name = Release; 792 | }; 793 | A28F6BC32B1F490B00F858E4 /* Debug */ = { 794 | isa = XCBuildConfiguration; 795 | buildSettings = { 796 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 797 | CODE_SIGN_STYLE = Automatic; 798 | CURRENT_PROJECT_VERSION = 1; 799 | DEVELOPMENT_TEAM = H535R5Z6H3; 800 | GENERATE_INFOPLIST_FILE = YES; 801 | MARKETING_VERSION = 1.0; 802 | PRODUCT_BUNDLE_IDENTIFIER = com.rushi.SwiftMVVMDemoUITests; 803 | PRODUCT_NAME = "$(TARGET_NAME)"; 804 | SWIFT_EMIT_LOC_STRINGS = NO; 805 | SWIFT_VERSION = 5.0; 806 | TARGETED_DEVICE_FAMILY = "1,2"; 807 | TEST_TARGET_NAME = SwiftMVVMDemo; 808 | }; 809 | name = Debug; 810 | }; 811 | A28F6BC42B1F490B00F858E4 /* Release */ = { 812 | isa = XCBuildConfiguration; 813 | buildSettings = { 814 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 815 | CODE_SIGN_STYLE = Automatic; 816 | CURRENT_PROJECT_VERSION = 1; 817 | DEVELOPMENT_TEAM = H535R5Z6H3; 818 | GENERATE_INFOPLIST_FILE = YES; 819 | MARKETING_VERSION = 1.0; 820 | PRODUCT_BUNDLE_IDENTIFIER = com.rushi.SwiftMVVMDemoUITests; 821 | PRODUCT_NAME = "$(TARGET_NAME)"; 822 | SWIFT_EMIT_LOC_STRINGS = NO; 823 | SWIFT_VERSION = 5.0; 824 | TARGETED_DEVICE_FAMILY = "1,2"; 825 | TEST_TARGET_NAME = SwiftMVVMDemo; 826 | }; 827 | name = Release; 828 | }; 829 | /* End XCBuildConfiguration section */ 830 | 831 | /* Begin XCConfigurationList section */ 832 | A28F6B8D2B1F490900F858E4 /* Build configuration list for PBXProject "SwiftMVVMDemo" */ = { 833 | isa = XCConfigurationList; 834 | buildConfigurations = ( 835 | A28F6BBA2B1F490B00F858E4 /* Debug */, 836 | A28F6BBB2B1F490B00F858E4 /* Release */, 837 | ); 838 | defaultConfigurationIsVisible = 0; 839 | defaultConfigurationName = Release; 840 | }; 841 | A28F6BBC2B1F490B00F858E4 /* Build configuration list for PBXNativeTarget "SwiftMVVMDemo" */ = { 842 | isa = XCConfigurationList; 843 | buildConfigurations = ( 844 | A28F6BBD2B1F490B00F858E4 /* Debug */, 845 | A28F6BBE2B1F490B00F858E4 /* Release */, 846 | ); 847 | defaultConfigurationIsVisible = 0; 848 | defaultConfigurationName = Release; 849 | }; 850 | A28F6BBF2B1F490B00F858E4 /* Build configuration list for PBXNativeTarget "SwiftMVVMDemoTests" */ = { 851 | isa = XCConfigurationList; 852 | buildConfigurations = ( 853 | A28F6BC02B1F490B00F858E4 /* Debug */, 854 | A28F6BC12B1F490B00F858E4 /* Release */, 855 | ); 856 | defaultConfigurationIsVisible = 0; 857 | defaultConfigurationName = Release; 858 | }; 859 | A28F6BC22B1F490B00F858E4 /* Build configuration list for PBXNativeTarget "SwiftMVVMDemoUITests" */ = { 860 | isa = XCConfigurationList; 861 | buildConfigurations = ( 862 | A28F6BC32B1F490B00F858E4 /* Debug */, 863 | A28F6BC42B1F490B00F858E4 /* Release */, 864 | ); 865 | defaultConfigurationIsVisible = 0; 866 | defaultConfigurationName = Release; 867 | }; 868 | /* End XCConfigurationList section */ 869 | 870 | /* Begin XCRemoteSwiftPackageReference section */ 871 | A27998382B5829FE0036C648 /* XCRemoteSwiftPackageReference "NetworkKit" */ = { 872 | isa = XCRemoteSwiftPackageReference; 873 | repositoryURL = "https://github.com/rushisangani/NetworkKit/"; 874 | requirement = { 875 | branch = main; 876 | kind = branch; 877 | }; 878 | }; 879 | /* End XCRemoteSwiftPackageReference section */ 880 | 881 | /* Begin XCSwiftPackageProductDependency section */ 882 | A27998322B581F030036C648 /* NetworkKit */ = { 883 | isa = XCSwiftPackageProductDependency; 884 | productName = NetworkKit; 885 | }; 886 | A27998392B5829FE0036C648 /* NetworkKit */ = { 887 | isa = XCSwiftPackageProductDependency; 888 | package = A27998382B5829FE0036C648 /* XCRemoteSwiftPackageReference "NetworkKit" */; 889 | productName = NetworkKit; 890 | }; 891 | /* End XCSwiftPackageProductDependency section */ 892 | }; 893 | rootObject = A28F6B8A2B1F490900F858E4 /* Project object */; 894 | } 895 | -------------------------------------------------------------------------------- /SwiftMVVMDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SwiftMVVMDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SwiftMVVMDemo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "networkkit", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/rushisangani/NetworkKit/", 7 | "state" : { 8 | "branch" : "main", 9 | "revision" : "505fd98eade8b54438dac441c078be402dfe88fb" 10 | } 11 | } 12 | ], 13 | "version" : 2 14 | } 15 | -------------------------------------------------------------------------------- /SwiftMVVMDemo.xcodeproj/project.xcworkspace/xcuserdata/rushisangani.xcuserdatad/IDEFindNavigatorScopes.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /SwiftMVVMDemo.xcodeproj/project.xcworkspace/xcuserdata/rushisangani.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rushisangani/SwiftMVVMDemo/09de1e99e890a5a672a87ee09fef026a25073cd1/SwiftMVVMDemo.xcodeproj/project.xcworkspace/xcuserdata/rushisangani.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /SwiftMVVMDemo.xcodeproj/xcshareddata/xcschemes/SwiftMVVMDemo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 35 | 41 | 42 | 43 | 46 | 52 | 53 | 54 | 55 | 56 | 66 | 68 | 74 | 75 | 76 | 77 | 83 | 85 | 91 | 92 | 93 | 94 | 96 | 97 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /SwiftMVVMDemo.xcodeproj/xcuserdata/rushisangani.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /SwiftMVVMDemo.xcodeproj/xcuserdata/rushisangani.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | SwiftMVVMDemo.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /SwiftMVVMDemo/AppCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppCoordinator.swift 3 | // SwiftMVVMDemo 4 | // 5 | // Created by Rushi Sangani on 12/12/2023. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | import SwiftUI 11 | 12 | protocol Coordinator: AnyObject { 13 | var parentCoordinator: Coordinator? { get set } 14 | var children: [Coordinator] { get set } 15 | var navigationController : UINavigationController { get set } 16 | 17 | func start() 18 | //func finish() 19 | } 20 | 21 | class AppCoordinator: Coordinator { 22 | 23 | weak var parentCoordinator: Coordinator? 24 | var children: [Coordinator] = [] 25 | var navigationController: UINavigationController 26 | 27 | init(navigationController: UINavigationController) { 28 | self.navigationController = navigationController 29 | } 30 | 31 | func start() { 32 | let mainStoryboard = UIStoryboard(name: "Main", bundle: .main) 33 | let vc = mainStoryboard.createViewController(ViewController.self) 34 | vc.coordinator = self 35 | navigationController.pushViewController(vc, animated: false) 36 | } 37 | 38 | func showPostsUIKit() { 39 | let child = PostCoordinator(navigationController: navigationController) 40 | child.start() 41 | } 42 | 43 | func showPhotosViewController() { 44 | let child = PhotosCoordinator(navigationController: navigationController) 45 | child.start() 46 | } 47 | 48 | // TODO: How to open SwiftUI Views using Coordinator? 49 | func showPostsSwiftUIView() { 50 | let hostController = UIHostingController(rootView: PostListView()) 51 | self.navigationController.pushViewController(hostController, animated: true) 52 | } 53 | 54 | func showPhotosAsyncImageView() { 55 | let hostController = UIHostingController(rootView: PhotosAsyncImageView()) 56 | self.navigationController.pushViewController(hostController, animated: true) 57 | } 58 | 59 | func showPhotosImageLoadingView() { 60 | let hostController = UIHostingController(rootView: PhotosCustomAsyncImageView()) 61 | self.navigationController.pushViewController(hostController, animated: true) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /SwiftMVVMDemo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // SwiftMVVMDemo 4 | // 5 | // Created by Rushi Sangani on 05/12/2023. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | var window: UIWindow? 14 | var coordinator: AppCoordinator? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | 18 | // create the main navigation controller to be used for our app 19 | let navController = UINavigationController() 20 | 21 | // send that into our coordinator so that it can display view controllers 22 | coordinator = AppCoordinator(navigationController: navController) 23 | 24 | // tell the coordinator to take over control 25 | coordinator?.start() 26 | 27 | // create a basic UIWindow and activate it 28 | window = UIWindow(frame: UIScreen.main.bounds) 29 | window?.rootViewController = navController 30 | window?.makeKeyAndVisible() 31 | 32 | return true 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /SwiftMVVMDemo/Assets.xcassets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rushisangani/SwiftMVVMDemo/09de1e99e890a5a672a87ee09fef026a25073cd1/SwiftMVVMDemo/Assets.xcassets/.DS_Store -------------------------------------------------------------------------------- /SwiftMVVMDemo/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 | -------------------------------------------------------------------------------- /SwiftMVVMDemo/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 | -------------------------------------------------------------------------------- /SwiftMVVMDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SwiftMVVMDemo/Assets.xcassets/sample.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "sample.png", 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 | -------------------------------------------------------------------------------- /SwiftMVVMDemo/Assets.xcassets/sample.imageset/sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rushisangani/SwiftMVVMDemo/09de1e99e890a5a672a87ee09fef026a25073cd1/SwiftMVVMDemo/Assets.xcassets/sample.imageset/sample.png -------------------------------------------------------------------------------- /SwiftMVVMDemo/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 | -------------------------------------------------------------------------------- /SwiftMVVMDemo/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 32 | 40 | 50 | 60 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /SwiftMVVMDemo/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIConstants.swift 3 | // MVVMDemo 4 | // 5 | // Created by Rushi Sangani on 30/11/2023. 6 | // 7 | 8 | import Foundation 9 | import NetworkKit 10 | 11 | struct APIConstants { 12 | static let host = "jsonplaceholder.typicode.com" 13 | } 14 | 15 | extension Request { 16 | var host: String { 17 | APIConstants.host 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /SwiftMVVMDemo/Extensions/UIStoryboard+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIStoryboard+Extension.swift 3 | // SwiftMVVMDemo 4 | // 5 | // Created by Rushi Sangani on 12/12/2023. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | extension UIStoryboard { 12 | 13 | static func createViewController(_ type: T.Type) -> T { 14 | let storyboard = UIStoryboard(name: String(describing: type), bundle: nil) 15 | return storyboard.instantiateInitialViewController() as! T 16 | } 17 | 18 | func createViewController(_ type: T.Type) -> T { 19 | instantiateViewController(withIdentifier: String(describing: type)) as! T 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SwiftMVVMDemo/Extensions/UITableView+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UITableView+Extension.swift 3 | // SwiftMVVMDemo 4 | // 5 | // Created by Rushi Sangani on 06/12/2023. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | extension UITableView { 12 | func dequeCell(withIdentifier identifier: String, style: UITableViewCell.CellStyle = .default) -> UITableViewCell { 13 | dequeueReusableCell(withIdentifier: identifier) ?? UITableViewCell(style: style, reuseIdentifier: identifier) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /SwiftMVVMDemo/Extensions/UIView+Constraints.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+Constraints.swift 3 | // SwiftMVVMDemo 4 | // 5 | // Created by Rushi Sangani on 06/12/2023. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | extension UIView { 12 | func autoPinEdgesToSuperViewEdges() { 13 | guard let superview = superview else { return } 14 | translatesAutoresizingMaskIntoConstraints = false 15 | topAnchor.constraint(equalTo: superview.topAnchor).isActive = true 16 | leadingAnchor.constraint(equalTo: superview.leadingAnchor).isActive = true 17 | bottomAnchor.constraint(equalTo: superview.bottomAnchor).isActive = true 18 | trailingAnchor.constraint(equalTo: superview.trailingAnchor).isActive = true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /SwiftMVVMDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /SwiftMVVMDemo/Modules/Comments/Comment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Comment.swift 3 | // MVVMDemo 4 | // 5 | // Created by Rushi Sangani on 30/11/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Comment: Codable, Identifiable { 11 | let id: Int 12 | let postId: Int 13 | let name: String 14 | let email: String 15 | let body: String 16 | } 17 | -------------------------------------------------------------------------------- /SwiftMVVMDemo/Modules/Photos/Image Loader/AsyncImageLoader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncImageLoader.swift 3 | // SwiftMVVMDemo 4 | // 5 | // Created by Rushi Sangani on 08/12/2023. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | import Combine 11 | 12 | protocol AsyncImageLoading { 13 | func downloadWithCombine(url: String) -> AnyPublisher 14 | func downloadWithAsync(url: String) async throws -> UIImage 15 | } 16 | 17 | class AsyncImageLoader: ObservableObject, AsyncImageLoading { 18 | 19 | // MARK: - Properties 20 | 21 | private static let queue = DispatchQueue(label: "Image Download Queue") 22 | private let urlSession: URLSession 23 | 24 | // MARK: - Init 25 | 26 | init(urlSession: URLSession = .shared) { 27 | self.urlSession = urlSession 28 | } 29 | 30 | /// Using Combine 31 | func downloadWithCombine(url: String) -> AnyPublisher { 32 | guard let imageURL = URL(string: url) else { 33 | return Fail(error: URLError(.badURL, userInfo: [NSURLErrorFailingURLErrorKey: url])) 34 | .eraseToAnyPublisher() 35 | } 36 | let request = URLRequest(url: imageURL, cachePolicy: .returnCacheDataElseLoad) 37 | 38 | return urlSession 39 | .dataTaskPublisher(for: request) 40 | .subscribe(on: Self.queue) 41 | .map(\.data) 42 | .tryMap { data in 43 | guard let image = UIImage(data: data) else { 44 | throw URLError(.badServerResponse, userInfo: [NSURLErrorFailingURLErrorKey: url]) 45 | } 46 | return image 47 | } 48 | .receive(on: DispatchQueue.main) 49 | .eraseToAnyPublisher() 50 | } 51 | 52 | /// Using Async-Await 53 | func downloadWithAsync(url: String) async throws -> UIImage { 54 | guard let imageURL = URL(string: url) else { 55 | throw URLError(.badURL) 56 | } 57 | 58 | let request = URLRequest(url: imageURL, cachePolicy: .returnCacheDataElseLoad) 59 | let (data, _) = try await urlSession.data(for: request) 60 | 61 | guard let image = UIImage(data: data) else { 62 | throw URLError(.badServerResponse, userInfo: [NSURLErrorFailingURLErrorKey: url]) 63 | } 64 | return image 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /SwiftMVVMDemo/Modules/Photos/Image Loader/CacheManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CacheManager.swift 3 | // SwiftMVVMDemo 4 | // 5 | // Created by Rushi Sangani on 09/01/2024. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | protocol Cacheable { 12 | func get(for url: String) -> UIImage? 13 | func set(_ image: UIImage?, for url: String) 14 | subscript(key: String) -> UIImage? { get set } 15 | func clear() 16 | } 17 | 18 | extension Cacheable { 19 | subscript(key: String) -> UIImage? { 20 | get { get(for: key) } 21 | set { set(newValue, for: key) } 22 | } 23 | } 24 | 25 | final class CacheManager: Cacheable { 26 | 27 | // MARK: - Singleton 28 | 29 | public static let shared = CacheManager() 30 | private init() {} 31 | 32 | // MARK: - Properties 33 | 34 | private let cache: NSCache = { 35 | let cache = NSCache() 36 | cache.countLimit = 100 37 | cache.totalCostLimit = 1024 * 1024 * 100 // 100 MB 38 | return cache 39 | }() 40 | 41 | // MARK: - Cacheable 42 | 43 | func get(for url: String) -> UIImage? { 44 | cache.object(forKey: url as NSString) 45 | } 46 | 47 | func set(_ image: UIImage?, for url: String) { 48 | if image == nil { 49 | cache.removeObject(forKey: url as NSString) 50 | } else { 51 | cache.setObject(image!, forKey: url as NSString) 52 | } 53 | } 54 | 55 | func clear() { 56 | cache.removeAllObjects() 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /SwiftMVVMDemo/Modules/Photos/Photo Row/PhotoRowViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoRowViewModel.swift 3 | // SwiftMVVMDemo 4 | // 5 | // Created by Rushi Sangani on 11/01/2024. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | import UIKit 11 | 12 | protocol PhotoRowViewModelHandler { 13 | 14 | var image: CurrentValueSubject { get } 15 | func getImage(url: String) 16 | func downloadImage(url: String) 17 | } 18 | 19 | class PhotoRowViewModel: PhotoRowViewModelHandler { 20 | 21 | // MARK: - Properties 22 | 23 | var image = CurrentValueSubject(nil) 24 | private var cancellables = Set() 25 | 26 | private let imageLoader: AsyncImageLoading 27 | private let cacheManager: Cacheable 28 | 29 | init(imageLoader: AsyncImageLoading = AsyncImageLoader(), 30 | cacheManager: Cacheable = CacheManager.shared 31 | ) { 32 | self.imageLoader = imageLoader 33 | self.cacheManager = cacheManager 34 | } 35 | 36 | // MARK: - PhotoRowViewModelHandler 37 | 38 | func getImage(url: String) { 39 | if let image = cacheManager[url] { 40 | self.image.send(image) 41 | } else { 42 | downloadImage(url: url) 43 | } 44 | } 45 | 46 | func downloadImage(url: String) { 47 | imageLoader 48 | .downloadWithCombine(url: url) 49 | .sink { _ in 50 | } receiveValue: { [weak self] image in 51 | guard let self = self else { return } 52 | 53 | self.cacheManager.set(image, for: url) 54 | self.image.send(image) 55 | } 56 | .store(in: &cancellables) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /SwiftMVVMDemo/Modules/Photos/Photo Row/PhotoTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoTableViewCell.swift 3 | // SwiftMVVMDemo 4 | // 5 | // Created by Rushi Sangani on 08/12/2023. 6 | // 7 | 8 | import UIKit 9 | import Combine 10 | 11 | class PhotoTableViewCell: UITableViewCell { 12 | 13 | // MARK: - Properties 14 | 15 | private var viewModel: PhotoRowViewModelHandler = PhotoRowViewModel() 16 | private var cancellables = Set() 17 | 18 | // MARK: - UI Components 19 | 20 | static let identifier = "PhotoTableViewCellId" 21 | 22 | let photoImageView: UIImageView = { 23 | let imageView = UIImageView(frame: .zero) 24 | imageView.clipsToBounds = true 25 | imageView.contentMode = .scaleAspectFill 26 | return imageView 27 | }() 28 | 29 | // MARK: - Init 30 | 31 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String!) { 32 | super.init(style: style, reuseIdentifier: reuseIdentifier) 33 | 34 | accessoryType = .none 35 | setupViewComponents() 36 | addObserver() 37 | } 38 | 39 | required init?(coder: NSCoder) { 40 | fatalError("init(coder:) has not been implemented") 41 | } 42 | 43 | override func prepareForReuse() { 44 | super.prepareForReuse() 45 | photoImageView.image = nil 46 | } 47 | 48 | func showPhoto(forUrl url: String) { 49 | viewModel.getImage(url: url) 50 | } 51 | } 52 | 53 | extension PhotoTableViewCell { 54 | 55 | func setupViewComponents() { 56 | contentView.addSubview(photoImageView) 57 | photoImageView.autoPinEdgesToSuperViewEdges() 58 | 59 | let heightConstraint = photoImageView.heightAnchor.constraint(equalToConstant: 300) 60 | heightConstraint.priority = .defaultHigh 61 | heightConstraint.isActive = true 62 | } 63 | 64 | func addObserver() { 65 | viewModel 66 | .image 67 | .sink { [weak self] image in 68 | guard let self = self else { return } 69 | self.photoImageView.image = image 70 | } 71 | .store(in: &cancellables) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /SwiftMVVMDemo/Modules/Photos/Photo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Photo.swift 3 | // SwiftMVVMDemo 4 | // 5 | // Created by Rushi Sangani on 08/12/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Photo: Codable, Identifiable { 11 | let albumId: Int 12 | let id: Int 13 | let title: String 14 | let url: String 15 | let thumbnailUrl: String 16 | } 17 | -------------------------------------------------------------------------------- /SwiftMVVMDemo/Modules/Photos/PhotoService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoService.swift 3 | // SwiftMVVMDemo 4 | // 5 | // Created by Rushi Sangani on 08/12/2023. 6 | // 7 | 8 | import Foundation 9 | import NetworkKit 10 | 11 | enum PhotoRequest: Request { 12 | case photosByAlbum(id: Int) 13 | 14 | var path: String { 15 | switch self { 16 | case .photosByAlbum(let id): 17 | return "/albums/\(id)/photos" 18 | } 19 | } 20 | } 21 | 22 | protocol PhotoRetrievalService { 23 | func getPhotos(albumId: Int) async throws -> [Photo] 24 | } 25 | 26 | // MARK: - PhotoService 27 | 28 | class PhotoService: PhotoRetrievalService { 29 | let networkManager: NetworkHandler 30 | 31 | init(networkManager: NetworkHandler = NetworkManager()) { 32 | self.networkManager = networkManager 33 | } 34 | 35 | func getPhotos(albumId: Int) async throws -> [Photo] { 36 | try await networkManager.fetch(request: PhotoRequest.photosByAlbum(id: albumId)) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /SwiftMVVMDemo/Modules/Photos/PhotosViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotosViewModel.swift 3 | // SwiftMVVMDemo 4 | // 5 | // Created by Rushi Sangani on 08/12/2023. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | final class PhotosViewModel: ObservableObject { 12 | 13 | // MARK: - Properties 14 | 15 | @Published var photos: [Photo] = [] 16 | private let service: PhotoRetrievalService 17 | 18 | init(service: PhotoRetrievalService = PhotoService()) { 19 | self.service = service 20 | } 21 | 22 | func getPhotos() async throws { 23 | let _photos = try await service.getPhotos(albumId: 1) 24 | await MainActor.run { 25 | self.photos = _photos 26 | } 27 | } 28 | 29 | func photoUrl(at indexPath: IndexPath) -> String { 30 | photos[indexPath.row].url 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /SwiftMVVMDemo/Modules/Photos/SwiftUI Views/PhotosAsyncImageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotosAsyncImageView.swift 3 | // SwiftMVVMDemo 4 | // 5 | // Created by Rushi Sangani on 08/12/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct PhotosAsyncImageView: View { 11 | @StateObject var viewModel: PhotosViewModel 12 | 13 | init(photoService: PhotoRetrievalService = PhotoService()) { 14 | self._viewModel = StateObject(wrappedValue: PhotosViewModel(service: photoService)) 15 | } 16 | 17 | var body: some View { 18 | ScrollView { 19 | LazyVStack { 20 | ForEach(viewModel.photos) { 21 | AsyncImage(url: URL(string: $0.url)) { phase in 22 | if let image = phase.image { 23 | image.resizable() 24 | } else { 25 | ProgressView() 26 | } 27 | } 28 | .frame(height: 300) 29 | } 30 | } 31 | } 32 | .navigationTitle(Text("SwiftUI Lazy Photos")) 33 | .task { 34 | try? await viewModel.getPhotos() 35 | } 36 | } 37 | } 38 | 39 | #Preview { 40 | PhotosAsyncImageView() 41 | } 42 | -------------------------------------------------------------------------------- /SwiftMVVMDemo/Modules/Photos/SwiftUI Views/PhotosCustomAsyncImageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotosCustomAsyncImageView.swift 3 | // SwiftMVVMDemo 4 | // 5 | // Created by Rushi Sangani on 08/12/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct PhotosCustomAsyncImageView: View { 11 | @StateObject var viewModel: PhotosViewModel 12 | 13 | init(photoService: PhotoRetrievalService = PhotoService()) { 14 | self._viewModel = StateObject(wrappedValue: PhotosViewModel(service: photoService)) 15 | } 16 | 17 | var body: some View { 18 | ScrollView { 19 | LazyVStack { 20 | ForEach(viewModel.photos) { 21 | CustomAsyncImageView(url: $0.url) 22 | } 23 | } 24 | } 25 | .navigationTitle(Text("SwiftUI Lazy Photos")) 26 | .task { 27 | try? await viewModel.getPhotos() 28 | } 29 | } 30 | } 31 | 32 | #Preview { 33 | PhotosCustomAsyncImageView() 34 | } 35 | 36 | struct CustomAsyncImageView: View { 37 | private let url: String 38 | 39 | @State private var image: UIImage? 40 | private let viewModel = PhotoRowViewModel() 41 | 42 | init(url: String) { 43 | self.url = url 44 | } 45 | 46 | var body: some View { 47 | Group { 48 | if let image = image { 49 | Image(uiImage: image) 50 | .resizable() 51 | .frame(height: 300) 52 | } else { 53 | ProgressView() 54 | .frame(height: 300) 55 | } 56 | } 57 | .onAppear() { 58 | viewModel.getImage(url: url) 59 | } 60 | .onReceive(viewModel.image) { image in 61 | self.image = image 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /SwiftMVVMDemo/Modules/Photos/UIKit Views/PhotosCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotosCoordinator.swift 3 | // SwiftMVVMDemo 4 | // 5 | // Created by Rushi Sangani on 12/12/2023. 6 | // 7 | 8 | import UIKit 9 | 10 | class PhotosCoordinator: Coordinator { 11 | 12 | weak var parentCoordinator: Coordinator? 13 | var children: [Coordinator] = [] 14 | var navigationController: UINavigationController 15 | 16 | init(navigationController: UINavigationController) { 17 | self.navigationController = navigationController 18 | } 19 | 20 | func start() { 21 | let viewController = PhotosViewController(viewModel: PhotosViewModel()) 22 | viewController.coordinator = self 23 | self.navigationController.pushViewController(viewController, animated: true) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /SwiftMVVMDemo/Modules/Photos/UIKit Views/PhotosViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotosViewController.swift 3 | // SwiftMVVMDemo 4 | // 5 | // Created by Rushi Sangani on 08/12/2023. 6 | // 7 | 8 | import UIKit 9 | import Combine 10 | 11 | class PhotosViewController: UIViewController { 12 | 13 | private(set) var viewModel: PhotosViewModel 14 | 15 | init(viewModel: PhotosViewModel) { 16 | self.viewModel = viewModel 17 | super.init(nibName: nil, bundle: nil) 18 | } 19 | 20 | required init?(coder: NSCoder) { 21 | fatalError("init(coder:) has not been implemented") 22 | } 23 | 24 | // MARK: - Properties 25 | 26 | weak var coordinator: PhotosCoordinator? 27 | private(set) var tableView = UITableView(frame: .zero, style: .plain) 28 | private var cancellables = Set() 29 | 30 | // MARK: - Life cycle 31 | 32 | override func loadView() { 33 | super.loadView() 34 | 35 | setupViews() 36 | } 37 | 38 | override func viewDidLoad() { 39 | super.viewDidLoad() 40 | 41 | setupObservers() 42 | getPhotos() 43 | } 44 | 45 | func getPhotos() { 46 | Task { 47 | try await viewModel.getPhotos() 48 | } 49 | } 50 | } 51 | 52 | // MARK: - Observables 53 | 54 | extension PhotosViewController { 55 | 56 | func setupObservers() { 57 | viewModel.$photos 58 | .receive(on: RunLoop.main) 59 | .sink { [weak self] posts in 60 | self?.tableView.reloadData() 61 | } 62 | .store(in: &cancellables) 63 | } 64 | } 65 | 66 | extension PhotosViewController { 67 | 68 | func showImage(_ image: UIImage, atIndexPath indexPath: IndexPath) { 69 | tableView.reloadRows(at: [indexPath], with: .none) 70 | } 71 | } 72 | 73 | // MARK: - UITableViewDataSource 74 | 75 | extension PhotosViewController: UITableViewDataSource { 76 | 77 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 78 | viewModel.photos.count 79 | } 80 | 81 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 82 | let cell = tableView.dequeueReusableCell(withIdentifier: PhotoTableViewCell.identifier, for: indexPath) as! PhotoTableViewCell 83 | cell.showPhoto(forUrl: viewModel.photoUrl(at: indexPath)) 84 | return cell 85 | } 86 | } 87 | 88 | //// MARK: - UITableViewDataSourcePrefetching 89 | // 90 | //extension PhotosViewController: UITableViewDataSourcePrefetching { 91 | // 92 | // func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { 93 | // // Perform image downloading for the indexPaths, only if not available in cache 94 | // for indexPath in indexPaths { 95 | //// if viewModel.image(atIndexPath: indexPath) == nil { 96 | //// viewModel.downloadImage(atIndexPath: indexPath) 97 | //// } 98 | // } 99 | // } 100 | // 101 | // func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { 102 | // // TODO: how to cancel prefetch? 103 | // } 104 | //} 105 | 106 | // MARK: - UISetup 107 | 108 | extension PhotosViewController { 109 | 110 | func setupViews() { 111 | view.addSubview(tableView) 112 | tableView.autoPinEdgesToSuperViewEdges() 113 | navigationItem.title = "Photos Prefetching" 114 | 115 | tableView.dataSource = self 116 | //tableView.prefetchDataSource = self 117 | tableView.estimatedRowHeight = 300 118 | tableView.rowHeight = UITableView.automaticDimension 119 | tableView.register(PhotoTableViewCell.self, forCellReuseIdentifier: PhotoTableViewCell.identifier) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /SwiftMVVMDemo/Modules/Posts/Post.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Post.swift 3 | // MVVMDemo 4 | // 5 | // Created by Rushi Sangani on 30/11/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Post: Codable, Identifiable { 11 | let userId: Int 12 | let id: Int 13 | let title: String 14 | let body: String 15 | } 16 | -------------------------------------------------------------------------------- /SwiftMVVMDemo/Modules/Posts/PostListViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PostListViewModel.swift 3 | // SwiftMVVMDemo 4 | // 5 | // Created by Rushi Sangani on 05/12/2023. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | // MARK: - PostListViewModel 12 | 13 | final class PostListViewModel: ObservableObject { 14 | private let postService: PostRetrievalService 15 | 16 | init(postService: PostRetrievalService = PostService()) { 17 | self.postService = postService 18 | } 19 | 20 | // MARK: - Properties 21 | 22 | @Published var posts: [Post] = [] 23 | 24 | func loadPosts() async throws { 25 | let _posts = try await postService.getPosts() 26 | await MainActor.run { 27 | self.posts = _posts 28 | } 29 | } 30 | 31 | func post(atIndexPath indexPath: IndexPath) -> Post { 32 | posts[indexPath.row] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /SwiftMVVMDemo/Modules/Posts/PostService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PostService.swift 3 | // SwiftMVVMDemo 4 | // 5 | // Created by Rushi Sangani on 05/12/2023. 6 | // 7 | 8 | import Foundation 9 | import NetworkKit 10 | 11 | // MARK: - PostRequest 12 | 13 | enum PostRequest: Request { 14 | case getPosts 15 | case getPost(id: Int) 16 | 17 | var path: String { 18 | switch self { 19 | case .getPosts: 20 | return "/posts" 21 | case .getPost(let id): 22 | return "/posts/\(id)" 23 | } 24 | } 25 | } 26 | 27 | protocol PostRetrievalService { 28 | func getPosts() async throws -> [Post] 29 | func getPostById(_ id: Int) async throws -> Post? 30 | } 31 | 32 | // MARK: - PostService 33 | 34 | class PostService: PostRetrievalService { 35 | let networkManager: NetworkHandler 36 | 37 | init(networkManager: NetworkHandler = NetworkManager()) { 38 | self.networkManager = networkManager 39 | } 40 | 41 | func getPosts() async throws -> [Post] { 42 | try await networkManager.fetch(request: PostRequest.getPosts) 43 | } 44 | 45 | func getPostById(_ id: Int) async throws -> Post? { 46 | try await networkManager.fetch(request: PostRequest.getPost(id: id)) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /SwiftMVVMDemo/Modules/Posts/SwiftUI Views/PostListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PostListView.swift 3 | // SwiftMVVMDemo 4 | // 5 | // Created by Rushi Sangani on 07/12/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct PostListView: View { 11 | @StateObject var viewModel: PostListViewModel 12 | 13 | init(postService: PostRetrievalService = PostService()) { 14 | self._viewModel = StateObject(wrappedValue: PostListViewModel(postService: postService)) 15 | } 16 | 17 | var body: some View { 18 | List { 19 | ForEach(viewModel.posts) { 20 | PostListRow(post: $0) 21 | } 22 | } 23 | .task { 24 | try? await viewModel.loadPosts() 25 | } 26 | .listStyle(.plain) 27 | .navigationTitle(Text("SwiftUI Posts")) 28 | } 29 | } 30 | 31 | struct PostListRow: View { 32 | let post: Post 33 | 34 | var body: some View { 35 | VStack(alignment: .leading, spacing: 5) { 36 | Text(post.title) 37 | .font(.body) 38 | .lineLimit(2) 39 | Text(post.body) 40 | .font(.caption) 41 | } 42 | } 43 | } 44 | 45 | #Preview { 46 | PostListView() 47 | } 48 | -------------------------------------------------------------------------------- /SwiftMVVMDemo/Modules/Posts/UIKit Views/PostCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PostCoordinator.swift 3 | // SwiftMVVMDemo 4 | // 5 | // Created by Rushi Sangani on 12/12/2023. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | import SwiftUI 11 | 12 | class PostCoordinator: Coordinator { 13 | 14 | weak var parentCoordinator: Coordinator? 15 | var children: [Coordinator] = [] 16 | var navigationController: UINavigationController 17 | 18 | init(navigationController: UINavigationController) { 19 | self.navigationController = navigationController 20 | } 21 | 22 | func start() { 23 | let viewController = PostListViewController(viewModel: PostListViewModel()) 24 | viewController.coordinator = self 25 | self.navigationController.pushViewController(viewController, animated: true) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /SwiftMVVMDemo/Modules/Posts/UIKit Views/PostListViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PostListViewController.swift 3 | // SwiftMVVMDemo 4 | // 5 | // Created by Rushi Sangani on 05/12/2023. 6 | // 7 | 8 | import UIKit 9 | import Combine 10 | 11 | class PostListViewController: UIViewController { 12 | private(set) var viewModel: PostListViewModel 13 | 14 | init(viewModel: PostListViewModel) { 15 | self.viewModel = viewModel 16 | super.init(nibName: nil, bundle: nil) 17 | } 18 | 19 | required init?(coder: NSCoder) { 20 | fatalError("init(coder:) has not been implemented") 21 | } 22 | 23 | // MARK: - Properties 24 | weak var coordinator: PostCoordinator? 25 | private(set) var tableView = UITableView(frame: .zero, style: .plain) 26 | private var cellIdentifier = "PostListTableViewCell" 27 | private var cancellables = Set() 28 | 29 | // MARK: - Life cycle 30 | 31 | override func loadView() { 32 | super.loadView() 33 | 34 | setupViews() 35 | } 36 | 37 | override func viewDidLoad() { 38 | super.viewDidLoad() 39 | 40 | setupObservers() 41 | getPosts() 42 | } 43 | 44 | func getPosts() { 45 | Task { 46 | try await viewModel.loadPosts() 47 | } 48 | } 49 | } 50 | 51 | // MARK: - Observables 52 | 53 | extension PostListViewController { 54 | 55 | func setupObservers() { 56 | viewModel.$posts 57 | .receive(on: RunLoop.main) 58 | .sink { [weak self] posts in 59 | self?.tableView.reloadData() 60 | } 61 | .store(in: &cancellables) 62 | } 63 | } 64 | 65 | // MARK: - UITableViewDataSource 66 | 67 | extension PostListViewController: UITableViewDataSource { 68 | 69 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 70 | viewModel.posts.count 71 | } 72 | 73 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 74 | let cell = tableView.dequeCell(withIdentifier: cellIdentifier, style: .subtitle) 75 | 76 | let post = viewModel.post(atIndexPath: indexPath) 77 | cell.textLabel?.text = post.title 78 | cell.detailTextLabel?.text = post.body 79 | cell.textLabel?.numberOfLines = 2 80 | cell.detailTextLabel?.numberOfLines = 0 81 | 82 | return cell 83 | } 84 | } 85 | 86 | // MARK: - UITableViewDelegate 87 | 88 | extension PostListViewController: UITableViewDelegate { 89 | 90 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 91 | 92 | } 93 | } 94 | 95 | // MARK: - UISetup 96 | 97 | extension PostListViewController { 98 | 99 | func setupViews() { 100 | view.addSubview(tableView) 101 | tableView.autoPinEdgesToSuperViewEdges() 102 | navigationItem.title = "UIKit Posts" 103 | 104 | tableView.dataSource = self 105 | tableView.delegate = self 106 | tableView.estimatedRowHeight = 80 107 | tableView.rowHeight = UITableView.automaticDimension 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /SwiftMVVMDemo/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // SwiftMVVMDemo 4 | // 5 | // Created by Rushi Sangani on 05/12/2023. 6 | // 7 | 8 | import UIKit 9 | import SwiftUI 10 | 11 | class ViewController: UIViewController { 12 | weak var coordinator: AppCoordinator? 13 | 14 | override func viewDidLoad() { 15 | super.viewDidLoad() 16 | } 17 | 18 | @IBAction func showPostsVCTapped(_ sender: Any) { 19 | coordinator?.showPostsUIKit() 20 | } 21 | 22 | @IBAction func showPostsViewTapped(_ sender: Any) { 23 | coordinator?.showPostsSwiftUIView() 24 | } 25 | 26 | @IBAction func photoUIKitTapped(_ sender: Any) { 27 | coordinator?.showPhotosViewController() 28 | } 29 | 30 | @IBAction func photosSwiftUIAsyncImageTapped(_ sender: Any) { 31 | coordinator?.showPhotosAsyncImageView() 32 | } 33 | 34 | @IBAction func photosSwiftUIImageLoadingTapped(_ sender: Any) { 35 | coordinator?.showPhotosImageLoadingView() 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /SwiftMVVMDemoTests/PhotosTests/ImageDownloadingTests/AsyncImageLoaderTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncImageLoaderTests.swift 3 | // SwiftMVVMDemoTests 4 | // 5 | // Created by Rushi Sangani on 10/01/2024. 6 | // 7 | 8 | import XCTest 9 | import Combine 10 | import NetworkKit 11 | @testable import SwiftMVVMDemo 12 | 13 | final class AsyncImageLoaderTests: XCTestCase { 14 | var asyncImageLoader: AsyncImageLoading! 15 | var imageUrl: String! 16 | var cancellables: Set! 17 | 18 | override func setUpWithError() throws { 19 | asyncImageLoader = AsyncImageLoader(urlSession: URLSession.mock) 20 | 21 | imageUrl = "https://via.placeholder.com/151/d32776" 22 | cancellables = [] 23 | } 24 | 25 | override func tearDownWithError() throws { 26 | asyncImageLoader = nil 27 | imageUrl = nil 28 | cancellables = nil 29 | } 30 | 31 | func testAsyncImageDownloadingWithCombine() { 32 | MockURLProtocol.requestHandler = { request in 33 | let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! 34 | return (MockImage().data, response) 35 | } 36 | 37 | var result: UIImage? 38 | let expectation = XCTestExpectation(description: "AsyncImageLoader Download Image") 39 | 40 | asyncImageLoader! 41 | .downloadWithCombine(url: imageUrl) 42 | .sink { completion in 43 | guard case .finished = completion else { return } 44 | expectation.fulfill() 45 | 46 | } receiveValue: { image in 47 | result = image 48 | } 49 | .store(in: &cancellables) 50 | 51 | wait(for: [expectation], timeout: 2) 52 | 53 | // verify image exists 54 | XCTAssertNotNil(result) 55 | } 56 | 57 | func testAsyncImageLoaderFailureWithCombine() { 58 | MockURLProtocol.requestHandler = { request in 59 | let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! 60 | return (Data(), response) 61 | } 62 | 63 | let expectation = XCTestExpectation(description: "AsyncImageLoader should throw error") 64 | var result: UIImage? 65 | var error: Error? 66 | 67 | asyncImageLoader! 68 | .downloadWithCombine(url: imageUrl) 69 | .sink { completion in 70 | guard case .failure(let err) = completion else { return } 71 | error = err 72 | expectation.fulfill() 73 | 74 | } receiveValue: { image in 75 | result = image 76 | } 77 | .store(in: &cancellables) 78 | 79 | wait(for: [expectation], timeout: 2) 80 | 81 | // verify image not retrieved 82 | XCTAssertNil(result) 83 | 84 | do { 85 | // verify error type is badServerResponse 86 | let urlError = try XCTUnwrap(error as? URLError) 87 | XCTAssertEqual(urlError.code, URLError.badServerResponse) 88 | 89 | } catch { 90 | XCTFail("AsyncImageLoader unexpected error: \(error)") 91 | } 92 | } 93 | 94 | func testAsyncImageLoaderReturnsImage() async throws { 95 | MockURLProtocol.requestHandler = { request in 96 | let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! 97 | return (MockImage().data, response) 98 | } 99 | 100 | let sampleImageData = MockImage().data 101 | let expectation = XCTestExpectation(description: "AsycImageLoader should return image") 102 | 103 | do { 104 | let image = try await asyncImageLoader!.downloadWithAsync(url: imageUrl!) 105 | expectation.fulfill() 106 | 107 | // verify the image is correct 108 | XCTAssertEqual(image.pngData()!.count, sampleImageData.count) 109 | } 110 | catch { 111 | XCTFail("AsycImageLoader should return valid image with correct url.") 112 | } 113 | } 114 | 115 | func testAyncImageLoaderFailure() async throws { 116 | MockURLProtocol.requestHandler = { request in 117 | let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! 118 | return (Data(), response) 119 | } 120 | 121 | let expectation = XCTestExpectation(description: "AsycImageLoader should throw badServerResponse error") 122 | do { 123 | let _ = try await asyncImageLoader!.downloadWithAsync(url: imageUrl) 124 | XCTFail("AsyncImageLoader should throw error") 125 | } 126 | catch URLError.badServerResponse { 127 | expectation.fulfill() 128 | } 129 | } 130 | } 131 | 132 | // MARK: - Mocks 133 | 134 | fileprivate struct MockImage { 135 | var image: UIImage { 136 | UIImage(named: "sample.png")! 137 | } 138 | var data: Data { 139 | image.pngData()! 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /SwiftMVVMDemoTests/PhotosTests/ImageDownloadingTests/CacheManagerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CacheManagerTests.swift 3 | // SwiftMVVMDemoTests 4 | // 5 | // Created by Rushi Sangani on 10/01/2024. 6 | // 7 | 8 | import XCTest 9 | @testable import SwiftMVVMDemo 10 | 11 | final class CacheManagerTests: XCTestCase { 12 | var cacheManager: Cacheable! 13 | 14 | override func setUpWithError() throws { 15 | cacheManager = CacheManager.shared 16 | } 17 | 18 | override func tearDownWithError() throws { 19 | cacheManager = nil 20 | } 21 | 22 | func testImageCacheStoreAndRetrieveImages() { 23 | let image = UIImage(named: "sample.png")! 24 | let url = "https://via.placeholder.com/150/d32776" 25 | 26 | // set 27 | cacheManager.set(image, for: url) 28 | // get 29 | let result = cacheManager.get(for: url) 30 | XCTAssertNotNil(result, "Expected image not nil") 31 | XCTAssertEqual(image, result) 32 | } 33 | 34 | func testCacheManagerClearCache() { 35 | let image = UIImage(named: "sample.png")! 36 | let url = "" 37 | 38 | // set 39 | cacheManager.set(image, for: url) 40 | 41 | // clear 42 | cacheManager.clear() 43 | 44 | let result = cacheManager[url] 45 | XCTAssertNil(result) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /SwiftMVVMDemoTests/PhotosTests/PhotoRowViewModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoRowViewModelTests.swift 3 | // SwiftMVVMDemoTests 4 | // 5 | // Created by Rushi Sangani on 11/01/2024. 6 | // 7 | 8 | import XCTest 9 | import Combine 10 | @testable import SwiftMVVMDemo 11 | 12 | final class PhotoRowViewModelTests: XCTestCase { 13 | var viewModel: PhotoRowViewModel! 14 | var cancellables: Set! 15 | 16 | override func setUpWithError() throws { 17 | viewModel = PhotoRowViewModel( 18 | imageLoader: MockAsyncImageLoader(), 19 | cacheManager: MockCacheManager() 20 | ) 21 | cancellables = [] 22 | } 23 | 24 | override func tearDownWithError() throws { 25 | viewModel = nil 26 | cancellables = nil 27 | } 28 | 29 | func testImageDownloadAndCaching() { 30 | let expectation1 = XCTestExpectation(description: "PhotoRowViewModel downloads image") 31 | let expectation2 = XCTestExpectation(description: "PhotoRowViewModel caches image") 32 | 33 | let imageUrl = "https://via.placeholder.com/150/d32776" 34 | 35 | // verify initial state 36 | XCTAssertNil(viewModel.image) 37 | XCTAssertNil(viewModel.cacheManager.get(for: imageUrl)) 38 | 39 | viewModel.$image 40 | .dropFirst() 41 | .sink(receiveValue: { image in 42 | expectation1.fulfill() 43 | expectation2.fulfill() 44 | }) 45 | .store(in: &cancellables) 46 | 47 | 48 | // download 49 | viewModel.downloadImage(url: imageUrl) 50 | 51 | wait(for: [expectation1, expectation2], timeout: 2) 52 | 53 | // verify 54 | XCTAssertNotNil(viewModel.image) 55 | XCTAssertNotNil(viewModel.cacheManager.get(for: imageUrl)) 56 | } 57 | } 58 | 59 | // MARK: - Mocks 60 | 61 | class MockAsyncImageLoader: AsyncImageLoading { 62 | private let sampleImage = UIImage(named: "sample.png")! 63 | 64 | func downloadWithCombine(url: String) -> AnyPublisher { 65 | Just(sampleImage) 66 | .setFailureType(to: Error.self) 67 | .eraseToAnyPublisher() 68 | } 69 | 70 | func downloadWithAsync(url: String) async throws -> UIImage { 71 | sampleImage 72 | } 73 | } 74 | 75 | class MockCacheManager: Cacheable { 76 | private var cache: Dictionary = [:] 77 | 78 | func get(for url: String) -> UIImage? { 79 | cache[url] 80 | } 81 | 82 | func set(_ image: UIImage?, for url: String) { 83 | cache[url] = image 84 | } 85 | 86 | func clear() { 87 | cache.removeAll() 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /SwiftMVVMDemoTests/PostTests/PostCoordinatorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PostCoordinatorTests.swift 3 | // SwiftMVVMDemoTests 4 | // 5 | // Created by Rushi Sangani on 16/01/2024. 6 | // 7 | 8 | import XCTest 9 | 10 | final class PostCoordinatorTests: 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 | 16 | override func tearDownWithError() throws { 17 | // Put teardown code here. This method is called after the invocation of each test method in the class. 18 | } 19 | 20 | func testExample() throws { 21 | // This is an example of a functional test case. 22 | // Use XCTAssert and related functions to verify your tests produce the correct results. 23 | // Any test you write for XCTest can be annotated as throws and async. 24 | // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. 25 | // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. 26 | } 27 | 28 | func testPostCoordinator() { 29 | // TODO: 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /SwiftMVVMDemoTests/PostTests/PostListViewControllerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PostListViewControllerTests.swift 3 | // SwiftMVVMDemoTests 4 | // 5 | // Created by Rushi Sangani on 06/12/2023. 6 | // 7 | 8 | import XCTest 9 | @testable import SwiftMVVMDemo 10 | 11 | final class PostListViewControllerTests: XCTestCase { 12 | var postListVC: PostListViewController! 13 | var viewModel: PostListViewModel! 14 | 15 | override func setUpWithError() throws { 16 | viewModel = PostListViewModel(postService: MockPostService()) 17 | postListVC = PostListViewController(viewModel: viewModel) 18 | } 19 | 20 | override func tearDownWithError() throws { 21 | postListVC = nil 22 | viewModel = nil 23 | } 24 | 25 | func testPostListVCShowPostsInTableView() throws { 26 | let expectation = XCTestExpectation(description: "TableView has data") 27 | 28 | postListVC.loadView() 29 | postListVC.setupObservers() 30 | 31 | Task { 32 | do { 33 | try await viewModel.loadPosts() 34 | expectation.fulfill() 35 | } 36 | catch { 37 | XCTFail("Expected viewmodel should load posts") 38 | } 39 | } 40 | 41 | wait(for: [expectation], timeout: 3) 42 | XCTAssertEqual(viewModel.posts.count, postListVC.tableView.numberOfRows(inSection: 0)) 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /SwiftMVVMDemoTests/PostTests/PostListViewModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PostListViewModelTests.swift 3 | // SwiftMVVMDemoTests 4 | // 5 | // Created by Rushi Sangani on 06/12/2023. 6 | // 7 | 8 | import XCTest 9 | import Combine 10 | import NetworkKit 11 | @testable import SwiftMVVMDemo 12 | 13 | final class PostListViewModelTests: XCTestCase { 14 | var postListViewModel: PostListViewModel! 15 | var postService: MockPostService! 16 | private var cancellables: Set! 17 | 18 | override func setUpWithError() throws { 19 | postService = MockPostService() 20 | postListViewModel = PostListViewModel(postService: postService) 21 | cancellables = [] 22 | } 23 | 24 | override func tearDownWithError() throws { 25 | postListViewModel = nil 26 | postService = nil 27 | cancellables = nil 28 | } 29 | 30 | func testPostViewModelReturnsPosts() async throws { 31 | try await postListViewModel!.loadPosts() 32 | let posts = postListViewModel!.posts 33 | 34 | // verify count 35 | XCTAssertEqual(posts.count, 100) 36 | 37 | let first = try XCTUnwrap(posts.first) 38 | XCTAssertEqual(first.userId, 1) 39 | XCTAssertEqual(first.id, 1) 40 | 41 | let last = try XCTUnwrap(posts.last) 42 | XCTAssertEqual(last.title, "at nam consequatur ea labore ea harum") 43 | XCTAssertEqual(last.body, "cupiditate quo est a modi nesciunt soluta\nipsa voluptas error itaque dicta in\nautem qui minus magnam et distinctio eum\naccusamus ratione error aut") 44 | } 45 | 46 | func testPostsProperty() throws { 47 | let expectation = XCTestExpectation(description: "@Published posts count") 48 | 49 | // initial state 50 | XCTAssertTrue(postListViewModel!.posts.isEmpty) 51 | 52 | postListViewModel! 53 | .$posts 54 | .dropFirst() 55 | .sink(receiveValue: { posts in 56 | // verify count and fullfil the expectation 57 | XCTAssertEqual(posts.count, 100) 58 | expectation.fulfill() 59 | }) 60 | .store(in: &cancellables) 61 | 62 | Task { 63 | try await postListViewModel!.loadPosts() 64 | } 65 | 66 | wait(for: [expectation], timeout: 3) 67 | } 68 | 69 | func testPostViewModelFailedGettingPosts() async { 70 | postService.shouldFail = true 71 | 72 | do { 73 | try await postListViewModel!.loadPosts() 74 | XCTFail("PostListViewModel should throw error.") 75 | } 76 | catch RequestError.failed(let error) { 77 | XCTAssertEqual(error, "No posts found.") 78 | } 79 | catch { 80 | XCTFail("PostListViewModel should throw RequestError.failed") 81 | } 82 | } 83 | } 84 | 85 | 86 | // MARK: - Mock 87 | 88 | class MockPostService: PostRetrievalService { 89 | var shouldFail: Bool = false 90 | 91 | func getPosts() async throws -> [Post] { 92 | guard !shouldFail else { 93 | throw RequestError.failed(description: "No posts found.") 94 | } 95 | return try Bundle.test.decodableObject(forResource: "posts", type: [Post].self) 96 | } 97 | 98 | func getPostById(_ id: Int) async throws -> Post? { 99 | nil 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /SwiftMVVMDemoTests/PostTests/PostListViewTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PostListViewTests.swift 3 | // SwiftMVVMDemoTests 4 | // 5 | // Created by Rushi Sangani on 10/01/2024. 6 | // 7 | 8 | import XCTest 9 | 10 | final class PostListViewTests: XCTestCase { 11 | 12 | // TODO: Write SwiftUI view tests 13 | 14 | override func setUpWithError() throws { 15 | // Put setup code here. This method is called before the invocation of each test method in the class. 16 | } 17 | 18 | override func tearDownWithError() throws { 19 | // Put teardown code here. This method is called after the invocation of each test method in the class. 20 | } 21 | 22 | func testExample() throws { 23 | // This is an example of a functional test case. 24 | // Use XCTAssert and related functions to verify your tests produce the correct results. 25 | // Any test you write for XCTest can be annotated as throws and async. 26 | // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. 27 | // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. 28 | } 29 | 30 | func testPerformanceExample() throws { 31 | // This is an example of a performance test case. 32 | self.measure { 33 | // Put the code you want to measure the time of here. 34 | } 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /SwiftMVVMDemoTests/PostTests/PostServiceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PostServiceTests.swift 3 | // SwiftMVVMDemoTests 4 | // 5 | // Created by Rushi Sangani on 05/12/2023. 6 | // 7 | 8 | import XCTest 9 | import NetworkKit 10 | import Combine 11 | @testable import SwiftMVVMDemo 12 | 13 | final class PostServiceTests: XCTestCase { 14 | var postService: PostService? 15 | 16 | override func setUpWithError() throws { 17 | postService = PostService(networkManager: MockNetworkManager()) 18 | } 19 | 20 | override func tearDownWithError() throws { 21 | postService = nil 22 | } 23 | 24 | func testPostServiceGetPosts() async throws { 25 | let posts = try await postService!.getPosts() 26 | 27 | let firstPost = try XCTUnwrap(posts.first) 28 | XCTAssertEqual(firstPost.id, 1) 29 | XCTAssertEqual(firstPost.userId, 1) 30 | XCTAssertEqual(firstPost.title, "sunt aut facere repellat provident occaecati excepturi optio reprehenderit") 31 | 32 | let lastPost = try XCTUnwrap(posts.last) 33 | XCTAssertEqual(lastPost.id, 100) 34 | XCTAssertEqual(lastPost.userId, 10) 35 | XCTAssertEqual(lastPost.title, "at nam consequatur ea labore ea harum") 36 | } 37 | 38 | func testPostServiceThrowsError() async throws { 39 | // TODO: How to implement this? 40 | } 41 | } 42 | 43 | // MARK: - Mock 44 | 45 | fileprivate class MockNetworkManager: NetworkHandler { 46 | 47 | func fetch(request: Request) async throws -> T where T : Decodable { 48 | try Bundle.test.decodableObject(forResource: "posts", type: T.self) 49 | } 50 | 51 | func fetch(request: Request) -> AnyPublisher where T : Decodable { 52 | do { 53 | let posts = try Bundle.test.decodableObject(forResource: "posts", type: T.self) 54 | return Just(posts) 55 | .setFailureType(to: RequestError.self) 56 | .eraseToAnyPublisher() 57 | 58 | } catch { 59 | return Fail(error: RequestError.failed(description: error.localizedDescription)) 60 | .eraseToAnyPublisher() 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /SwiftMVVMDemoTests/Resources/Bundle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bundle.swift 3 | // SwiftMVVMDemoTests 4 | // 5 | // Created by Rushi Sangani on 17/01/2024. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Bundle { 11 | static var test: Bundle { 12 | Bundle(for: PostServiceTests.self) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /SwiftMVVMDemoTests/Resources/comments.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "postId": 1, 4 | "id": 1, 5 | "name": "id labore ex et quam laborum", 6 | "email": "Eliseo@gardner.biz", 7 | "body": "laudantium enim quasi est quidem magnam voluptate ipsam eos\ntempora quo necessitatibus\ndolor quam autem quasi\nreiciendis et nam sapiente accusantium" 8 | }, 9 | { 10 | "postId": 1, 11 | "id": 2, 12 | "name": "quo vero reiciendis velit similique earum", 13 | "email": "Jayne_Kuhic@sydney.com", 14 | "body": "est natus enim nihil est dolore omnis voluptatem numquam\net omnis occaecati quod ullam at\nvoluptatem error expedita pariatur\nnihil sint nostrum voluptatem reiciendis et" 15 | }, 16 | { 17 | "postId": 1, 18 | "id": 3, 19 | "name": "odio adipisci rerum aut animi", 20 | "email": "Nikita@garfield.biz", 21 | "body": "quia molestiae reprehenderit quasi aspernatur\naut expedita occaecati aliquam eveniet laudantium\nomnis quibusdam delectus saepe quia accusamus maiores nam est\ncum et ducimus et vero voluptates excepturi deleniti ratione" 22 | }, 23 | { 24 | "postId": 1, 25 | "id": 4, 26 | "name": "alias odio sit", 27 | "email": "Lew@alysha.tv", 28 | "body": "non et atque\noccaecati deserunt quas accusantium unde odit nobis qui voluptatem\nquia voluptas consequuntur itaque dolor\net qui rerum deleniti ut occaecati" 29 | }, 30 | { 31 | "postId": 1, 32 | "id": 5, 33 | "name": "vero eaque aliquid doloribus et culpa", 34 | "email": "Hayden@althea.biz", 35 | "body": "harum non quasi et ratione\ntempore iure ex voluptates in ratione\nharum architecto fugit inventore cupiditate\nvoluptates magni quo et" 36 | }, 37 | { 38 | "postId": 2, 39 | "id": 6, 40 | "name": "et fugit eligendi deleniti quidem qui sint nihil autem", 41 | "email": "Presley.Mueller@myrl.com", 42 | "body": "doloribus at sed quis culpa deserunt consectetur qui praesentium\naccusamus fugiat dicta\nvoluptatem rerum ut voluptate autem\nvoluptatem repellendus aspernatur dolorem in" 43 | }, 44 | { 45 | "postId": 2, 46 | "id": 7, 47 | "name": "repellat consequatur praesentium vel minus molestias voluptatum", 48 | "email": "Dallas@ole.me", 49 | "body": "maiores sed dolores similique labore et inventore et\nquasi temporibus esse sunt id et\neos voluptatem aliquam\naliquid ratione corporis molestiae mollitia quia et magnam dolor" 50 | }, 51 | { 52 | "postId": 2, 53 | "id": 8, 54 | "name": "et omnis dolorem", 55 | "email": "Mallory_Kunze@marie.org", 56 | "body": "ut voluptatem corrupti velit\nad voluptatem maiores\net nisi velit vero accusamus maiores\nvoluptates quia aliquid ullam eaque" 57 | }, 58 | { 59 | "postId": 2, 60 | "id": 9, 61 | "name": "provident id voluptas", 62 | "email": "Meghan_Littel@rene.us", 63 | "body": "sapiente assumenda molestiae atque\nadipisci laborum distinctio aperiam et ab ut omnis\net occaecati aspernatur odit sit rem expedita\nquas enim ipsam minus" 64 | }, 65 | { 66 | "postId": 2, 67 | "id": 10, 68 | "name": "eaque et deleniti atque tenetur ut quo ut", 69 | "email": "Carmen_Keeling@caroline.name", 70 | "body": "voluptate iusto quis nobis reprehenderit ipsum amet nulla\nquia quas dolores velit et non\naut quia necessitatibus\nnostrum quaerat nulla et accusamus nisi facilis" 71 | }, 72 | { 73 | "postId": 3, 74 | "id": 11, 75 | "name": "fugit labore quia mollitia quas deserunt nostrum sunt", 76 | "email": "Veronica_Goodwin@timmothy.net", 77 | "body": "ut dolorum nostrum id quia aut est\nfuga est inventore vel eligendi explicabo quis consectetur\naut occaecati repellat id natus quo est\nut blanditiis quia ut vel ut maiores ea" 78 | }, 79 | { 80 | "postId": 3, 81 | "id": 12, 82 | "name": "modi ut eos dolores illum nam dolor", 83 | "email": "Oswald.Vandervort@leanne.org", 84 | "body": "expedita maiores dignissimos facilis\nipsum est rem est fugit velit sequi\neum odio dolores dolor totam\noccaecati ratione eius rem velit" 85 | }, 86 | { 87 | "postId": 3, 88 | "id": 13, 89 | "name": "aut inventore non pariatur sit vitae voluptatem sapiente", 90 | "email": "Kariane@jadyn.tv", 91 | "body": "fuga eos qui dolor rerum\ninventore corporis exercitationem\ncorporis cupiditate et deserunt recusandae est sed quis culpa\neum maiores corporis et" 92 | }, 93 | { 94 | "postId": 3, 95 | "id": 14, 96 | "name": "et officiis id praesentium hic aut ipsa dolorem repudiandae", 97 | "email": "Nathan@solon.io", 98 | "body": "vel quae voluptas qui exercitationem\nvoluptatibus unde sed\nminima et qui ipsam aspernatur\nexpedita magnam laudantium et et quaerat ut qui dolorum" 99 | }, 100 | { 101 | "postId": 3, 102 | "id": 15, 103 | "name": "debitis magnam hic odit aut ullam nostrum tenetur", 104 | "email": "Maynard.Hodkiewicz@roberta.com", 105 | "body": "nihil ut voluptates blanditiis autem odio dicta rerum\nquisquam saepe et est\nsunt quasi nemo laudantium deserunt\nmolestias tempora quo quia" 106 | }, 107 | { 108 | "postId": 4, 109 | "id": 16, 110 | "name": "perferendis temporibus delectus optio ea eum ratione dolorum", 111 | "email": "Christine@ayana.info", 112 | "body": "iste ut laborum aliquid velit facere itaque\nquo ut soluta dicta voluptate\nerror tempore aut et\nsequi reiciendis dignissimos expedita consequuntur libero sed fugiat facilis" 113 | }, 114 | { 115 | "postId": 4, 116 | "id": 17, 117 | "name": "eos est animi quis", 118 | "email": "Preston_Hudson@blaise.tv", 119 | "body": "consequatur necessitatibus totam sed sit dolorum\nrecusandae quae odio excepturi voluptatum harum voluptas\nquisquam sit ad eveniet delectus\ndoloribus odio qui non labore" 120 | }, 121 | { 122 | "postId": 4, 123 | "id": 18, 124 | "name": "aut et tenetur ducimus illum aut nulla ab", 125 | "email": "Vincenza_Klocko@albertha.name", 126 | "body": "veritatis voluptates necessitatibus maiores corrupti\nneque et exercitationem amet sit et\nullam velit sit magnam laborum\nmagni ut molestias" 127 | }, 128 | { 129 | "postId": 4, 130 | "id": 19, 131 | "name": "sed impedit rerum quia et et inventore unde officiis", 132 | "email": "Madelynn.Gorczany@darion.biz", 133 | "body": "doloribus est illo sed minima aperiam\nut dignissimos accusantium tempore atque et aut molestiae\nmagni ut accusamus voluptatem quos ut voluptates\nquisquam porro sed architecto ut" 134 | }, 135 | { 136 | "postId": 4, 137 | "id": 20, 138 | "name": "molestias expedita iste aliquid voluptates", 139 | "email": "Mariana_Orn@preston.org", 140 | "body": "qui harum consequatur fugiat\net eligendi perferendis at molestiae commodi ducimus\ndoloremque asperiores numquam qui\nut sit dignissimos reprehenderit tempore" 141 | }, 142 | { 143 | "postId": 5, 144 | "id": 21, 145 | "name": "aliquid rerum mollitia qui a consectetur eum sed", 146 | "email": "Noemie@marques.me", 147 | "body": "deleniti aut sed molestias explicabo\ncommodi odio ratione nesciunt\nvoluptate doloremque est\nnam autem error delectus" 148 | }, 149 | { 150 | "postId": 5, 151 | "id": 22, 152 | "name": "porro repellendus aut tempore quis hic", 153 | "email": "Khalil@emile.co.uk", 154 | "body": "qui ipsa animi nostrum praesentium voluptatibus odit\nqui non impedit cum qui nostrum aliquid fuga explicabo\nvoluptatem fugit earum voluptas exercitationem temporibus dignissimos distinctio\nesse inventore reprehenderit quidem ut incidunt nihil necessitatibus rerum" 155 | }, 156 | { 157 | "postId": 5, 158 | "id": 23, 159 | "name": "quis tempora quidem nihil iste", 160 | "email": "Sophia@arianna.co.uk", 161 | "body": "voluptates provident repellendus iusto perspiciatis ex fugiat ut\nut dolor nam aliquid et expedita voluptate\nsunt vitae illo rerum in quos\nvel eligendi enim quae fugiat est" 162 | }, 163 | { 164 | "postId": 5, 165 | "id": 24, 166 | "name": "in tempore eos beatae est", 167 | "email": "Jeffery@juwan.us", 168 | "body": "repudiandae repellat quia\nsequi est dolore explicabo nihil et\net sit et\net praesentium iste atque asperiores tenetur" 169 | }, 170 | { 171 | "postId": 5, 172 | "id": 25, 173 | "name": "autem ab ea sit alias hic provident sit", 174 | "email": "Isaias_Kuhic@jarrett.net", 175 | "body": "sunt aut quae laboriosam sit ut impedit\nadipisci harum laborum totam deleniti voluptas odit rem ea\nnon iure distinctio ut velit doloribus\net non ex" 176 | }, 177 | { 178 | "postId": 6, 179 | "id": 26, 180 | "name": "in deleniti sunt provident soluta ratione veniam quam praesentium", 181 | "email": "Russel.Parker@kameron.io", 182 | "body": "incidunt sapiente eaque dolor eos\nad est molestias\nquas sit et nihil exercitationem at cumque ullam\nnihil magnam et" 183 | }, 184 | { 185 | "postId": 6, 186 | "id": 27, 187 | "name": "doloribus quibusdam molestiae amet illum", 188 | "email": "Francesco.Gleason@nella.us", 189 | "body": "nisi vel quas ut laborum ratione\nrerum magni eum\nunde et voluptatem saepe\nvoluptas corporis modi amet ipsam eos saepe porro" 190 | }, 191 | { 192 | "postId": 6, 193 | "id": 28, 194 | "name": "quo voluptates voluptas nisi veritatis dignissimos dolores ut officiis", 195 | "email": "Ronny@rosina.org", 196 | "body": "voluptatem repellendus quo alias at laudantium\nmollitia quidem esse\ntemporibus consequuntur vitae rerum illum\nid corporis sit id" 197 | }, 198 | { 199 | "postId": 6, 200 | "id": 29, 201 | "name": "eum distinctio amet dolor", 202 | "email": "Jennings_Pouros@erica.biz", 203 | "body": "tempora voluptatem est\nmagnam distinctio autem est dolorem\net ipsa molestiae odit rerum itaque corporis nihil nam\neaque rerum error" 204 | }, 205 | { 206 | "postId": 6, 207 | "id": 30, 208 | "name": "quasi nulla ducimus facilis non voluptas aut", 209 | "email": "Lurline@marvin.biz", 210 | "body": "consequuntur quia voluptate assumenda et\nautem voluptatem reiciendis ipsum animi est provident\nearum aperiam sapiente ad vitae iste\naccusantium aperiam eius qui dolore voluptatem et" 211 | }, 212 | { 213 | "postId": 7, 214 | "id": 31, 215 | "name": "ex velit ut cum eius odio ad placeat", 216 | "email": "Buford@shaylee.biz", 217 | "body": "quia incidunt ut\naliquid est ut rerum deleniti iure est\nipsum quia ea sint et\nvoluptatem quaerat eaque repudiandae eveniet aut" 218 | }, 219 | { 220 | "postId": 7, 221 | "id": 32, 222 | "name": "dolorem architecto ut pariatur quae qui suscipit", 223 | "email": "Maria@laurel.name", 224 | "body": "nihil ea itaque libero illo\nofficiis quo quo dicta inventore consequatur voluptas voluptatem\ncorporis sed necessitatibus velit tempore\nrerum velit et temporibus" 225 | }, 226 | { 227 | "postId": 7, 228 | "id": 33, 229 | "name": "voluptatum totam vel voluptate omnis", 230 | "email": "Jaeden.Towne@arlene.tv", 231 | "body": "fugit harum quae vero\nlibero unde tempore\nsoluta eaque culpa sequi quibusdam nulla id\net et necessitatibus" 232 | }, 233 | { 234 | "postId": 7, 235 | "id": 34, 236 | "name": "omnis nemo sunt ab autem", 237 | "email": "Ethelyn.Schneider@emelia.co.uk", 238 | "body": "omnis temporibus quasi ab omnis\nfacilis et omnis illum quae quasi aut\nminus iure ex rem ut reprehenderit\nin non fugit" 239 | }, 240 | { 241 | "postId": 7, 242 | "id": 35, 243 | "name": "repellendus sapiente omnis praesentium aliquam ipsum id molestiae omnis", 244 | "email": "Georgianna@florence.io", 245 | "body": "dolor mollitia quidem facere et\nvel est ut\nut repudiandae est quidem dolorem sed atque\nrem quia aut adipisci sunt" 246 | }, 247 | { 248 | "postId": 8, 249 | "id": 36, 250 | "name": "sit et quis", 251 | "email": "Raheem_Heaney@gretchen.biz", 252 | "body": "aut vero est\ndolor non aut excepturi dignissimos illo nisi aut quas\naut magni quia nostrum provident magnam quas modi maxime\nvoluptatem et molestiae" 253 | }, 254 | { 255 | "postId": 8, 256 | "id": 37, 257 | "name": "beatae veniam nemo rerum voluptate quam aspernatur", 258 | "email": "Jacky@victoria.net", 259 | "body": "qui rem amet aut\ncumque maiores earum ut quia sit nam esse qui\niusto aspernatur quis voluptas\ndolorem distinctio ex temporibus rem" 260 | }, 261 | { 262 | "postId": 8, 263 | "id": 38, 264 | "name": "maiores dolores expedita", 265 | "email": "Piper@linwood.us", 266 | "body": "unde voluptatem qui dicta\nvel ad aut eos error consequatur voluptatem\nadipisci doloribus qui est sit aut\nvelit aut et ea ratione eveniet iure fuga" 267 | }, 268 | { 269 | "postId": 8, 270 | "id": 39, 271 | "name": "necessitatibus ratione aut ut delectus quae ut", 272 | "email": "Gaylord@russell.net", 273 | "body": "atque consequatur dolorem sunt\nadipisci autem et\nvoluptatibus et quae necessitatibus rerum eaque aperiam nostrum nemo\neligendi sed et beatae et inventore" 274 | }, 275 | { 276 | "postId": 8, 277 | "id": 40, 278 | "name": "non minima omnis deleniti pariatur facere quibusdam at", 279 | "email": "Clare.Aufderhar@nicole.ca", 280 | "body": "quod minus alias quos\nperferendis labore molestias quae ut ut corporis deserunt vitae\net quaerat ut et ullam unde asperiores\ncum voluptatem cumque" 281 | }, 282 | { 283 | "postId": 9, 284 | "id": 41, 285 | "name": "voluptas deleniti ut", 286 | "email": "Lucio@gladys.tv", 287 | "body": "facere repudiandae vitae ea aut sed quo ut et\nfacere nihil ut voluptates in\nsaepe cupiditate accusantium numquam dolores\ninventore sint mollitia provident" 288 | }, 289 | { 290 | "postId": 9, 291 | "id": 42, 292 | "name": "nam qui et", 293 | "email": "Shemar@ewell.name", 294 | "body": "aut culpa quaerat veritatis eos debitis\naut repellat eius explicabo et\nofficiis quo sint at magni ratione et iure\nincidunt quo sequi quia dolorum beatae qui" 295 | }, 296 | { 297 | "postId": 9, 298 | "id": 43, 299 | "name": "molestias sint est voluptatem modi", 300 | "email": "Jackeline@eva.tv", 301 | "body": "voluptatem ut possimus laborum quae ut commodi delectus\nin et consequatur\nin voluptas beatae molestiae\nest rerum laborum et et velit sint ipsum dolorem" 302 | }, 303 | { 304 | "postId": 9, 305 | "id": 44, 306 | "name": "hic molestiae et fuga ea maxime quod", 307 | "email": "Marianna_Wilkinson@rupert.io", 308 | "body": "qui sunt commodi\nsint vel optio vitae quis qui non distinctio\nid quasi modi dicta\neos nihil sit inventore est numquam officiis" 309 | }, 310 | { 311 | "postId": 9, 312 | "id": 45, 313 | "name": "autem illo facilis", 314 | "email": "Marcia@name.biz", 315 | "body": "ipsum odio harum voluptatem sunt cumque et dolores\nnihil laboriosam neque commodi qui est\nquos numquam voluptatum\ncorporis quo in vitae similique cumque tempore" 316 | }, 317 | { 318 | "postId": 10, 319 | "id": 46, 320 | "name": "dignissimos et deleniti voluptate et quod", 321 | "email": "Jeremy.Harann@waino.me", 322 | "body": "exercitationem et id quae cum omnis\nvoluptatibus accusantium et quidem\nut ipsam sint\ndoloremque illo ex atque necessitatibus sed" 323 | }, 324 | { 325 | "postId": 10, 326 | "id": 47, 327 | "name": "rerum commodi est non dolor nesciunt ut", 328 | "email": "Pearlie.Kling@sandy.com", 329 | "body": "occaecati laudantium ratione non cumque\nearum quod non enim soluta nisi velit similique voluptatibus\nesse laudantium consequatur voluptatem rem eaque voluptatem aut ut\net sit quam" 330 | }, 331 | { 332 | "postId": 10, 333 | "id": 48, 334 | "name": "consequatur animi dolorem saepe repellendus ut quo aut tenetur", 335 | "email": "Manuela_Stehr@chelsie.tv", 336 | "body": "illum et alias quidem magni voluptatum\nab soluta ea qui saepe corrupti hic et\ncum repellat esse\nest sint vel veritatis officia consequuntur cum" 337 | }, 338 | { 339 | "postId": 10, 340 | "id": 49, 341 | "name": "rerum placeat quae minus iusto eligendi", 342 | "email": "Camryn.Weimann@doris.io", 343 | "body": "id est iure occaecati quam similique enim\nab repudiandae non\nillum expedita quam excepturi soluta qui placeat\nperspiciatis optio maiores non doloremque aut iusto sapiente" 344 | }, 345 | { 346 | "postId": 10, 347 | "id": 50, 348 | "name": "dolorum soluta quidem ex quae occaecati dicta aut doloribus", 349 | "email": "Kiana_Predovic@yasmin.io", 350 | "body": "eum accusamus aut delectus\narchitecto blanditiis quia sunt\nrerum harum sit quos quia aspernatur vel corrupti inventore\nanimi dicta vel corporis" 351 | } 352 | ] 353 | 354 | -------------------------------------------------------------------------------- /SwiftMVVMDemoTests/Resources/posts.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "userId": 1, 4 | "id": 1, 5 | "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit", 6 | "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto" 7 | }, 8 | { 9 | "userId": 1, 10 | "id": 2, 11 | "title": "qui est esse", 12 | "body": "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla" 13 | }, 14 | { 15 | "userId": 1, 16 | "id": 3, 17 | "title": "ea molestias quasi exercitationem repellat qui ipsa sit aut", 18 | "body": "et iusto sed quo iure\nvoluptatem occaecati omnis eligendi aut ad\nvoluptatem doloribus vel accusantium quis pariatur\nmolestiae porro eius odio et labore et velit aut" 19 | }, 20 | { 21 | "userId": 1, 22 | "id": 4, 23 | "title": "eum et est occaecati", 24 | "body": "ullam et saepe reiciendis voluptatem adipisci\nsit amet autem assumenda provident rerum culpa\nquis hic commodi nesciunt rem tenetur doloremque ipsam iure\nquis sunt voluptatem rerum illo velit" 25 | }, 26 | { 27 | "userId": 1, 28 | "id": 5, 29 | "title": "nesciunt quas odio", 30 | "body": "repudiandae veniam quaerat sunt sed\nalias aut fugiat sit autem sed est\nvoluptatem omnis possimus esse voluptatibus quis\nest aut tenetur dolor neque" 31 | }, 32 | { 33 | "userId": 1, 34 | "id": 6, 35 | "title": "dolorem eum magni eos aperiam quia", 36 | "body": "ut aspernatur corporis harum nihil quis provident sequi\nmollitia nobis aliquid molestiae\nperspiciatis et ea nemo ab reprehenderit accusantium quas\nvoluptate dolores velit et doloremque molestiae" 37 | }, 38 | { 39 | "userId": 1, 40 | "id": 7, 41 | "title": "magnam facilis autem", 42 | "body": "dolore placeat quibusdam ea quo vitae\nmagni quis enim qui quis quo nemo aut saepe\nquidem repellat excepturi ut quia\nsunt ut sequi eos ea sed quas" 43 | }, 44 | { 45 | "userId": 1, 46 | "id": 8, 47 | "title": "dolorem dolore est ipsam", 48 | "body": "dignissimos aperiam dolorem qui eum\nfacilis quibusdam animi sint suscipit qui sint possimus cum\nquaerat magni maiores excepturi\nipsam ut commodi dolor voluptatum modi aut vitae" 49 | }, 50 | { 51 | "userId": 1, 52 | "id": 9, 53 | "title": "nesciunt iure omnis dolorem tempora et accusantium", 54 | "body": "consectetur animi nesciunt iure dolore\nenim quia ad\nveniam autem ut quam aut nobis\net est aut quod aut provident voluptas autem voluptas" 55 | }, 56 | { 57 | "userId": 1, 58 | "id": 10, 59 | "title": "optio molestias id quia eum", 60 | "body": "quo et expedita modi cum officia vel magni\ndoloribus qui repudiandae\nvero nisi sit\nquos veniam quod sed accusamus veritatis error" 61 | }, 62 | { 63 | "userId": 2, 64 | "id": 11, 65 | "title": "et ea vero quia laudantium autem", 66 | "body": "delectus reiciendis molestiae occaecati non minima eveniet qui voluptatibus\naccusamus in eum beatae sit\nvel qui neque voluptates ut commodi qui incidunt\nut animi commodi" 67 | }, 68 | { 69 | "userId": 2, 70 | "id": 12, 71 | "title": "in quibusdam tempore odit est dolorem", 72 | "body": "itaque id aut magnam\npraesentium quia et ea odit et ea voluptas et\nsapiente quia nihil amet occaecati quia id voluptatem\nincidunt ea est distinctio odio" 73 | }, 74 | { 75 | "userId": 2, 76 | "id": 13, 77 | "title": "dolorum ut in voluptas mollitia et saepe quo animi", 78 | "body": "aut dicta possimus sint mollitia voluptas commodi quo doloremque\niste corrupti reiciendis voluptatem eius rerum\nsit cumque quod eligendi laborum minima\nperferendis recusandae assumenda consectetur porro architecto ipsum ipsam" 79 | }, 80 | { 81 | "userId": 2, 82 | "id": 14, 83 | "title": "voluptatem eligendi optio", 84 | "body": "fuga et accusamus dolorum perferendis illo voluptas\nnon doloremque neque facere\nad qui dolorum molestiae beatae\nsed aut voluptas totam sit illum" 85 | }, 86 | { 87 | "userId": 2, 88 | "id": 15, 89 | "title": "eveniet quod temporibus", 90 | "body": "reprehenderit quos placeat\nvelit minima officia dolores impedit repudiandae molestiae nam\nvoluptas recusandae quis delectus\nofficiis harum fugiat vitae" 91 | }, 92 | { 93 | "userId": 2, 94 | "id": 16, 95 | "title": "sint suscipit perspiciatis velit dolorum rerum ipsa laboriosam odio", 96 | "body": "suscipit nam nisi quo aperiam aut\nasperiores eos fugit maiores voluptatibus quia\nvoluptatem quis ullam qui in alias quia est\nconsequatur magni mollitia accusamus ea nisi voluptate dicta" 97 | }, 98 | { 99 | "userId": 2, 100 | "id": 17, 101 | "title": "fugit voluptas sed molestias voluptatem provident", 102 | "body": "eos voluptas et aut odit natus earum\naspernatur fuga molestiae ullam\ndeserunt ratione qui eos\nqui nihil ratione nemo velit ut aut id quo" 103 | }, 104 | { 105 | "userId": 2, 106 | "id": 18, 107 | "title": "voluptate et itaque vero tempora molestiae", 108 | "body": "eveniet quo quis\nlaborum totam consequatur non dolor\nut et est repudiandae\nest voluptatem vel debitis et magnam" 109 | }, 110 | { 111 | "userId": 2, 112 | "id": 19, 113 | "title": "adipisci placeat illum aut reiciendis qui", 114 | "body": "illum quis cupiditate provident sit magnam\nea sed aut omnis\nveniam maiores ullam consequatur atque\nadipisci quo iste expedita sit quos voluptas" 115 | }, 116 | { 117 | "userId": 2, 118 | "id": 20, 119 | "title": "doloribus ad provident suscipit at", 120 | "body": "qui consequuntur ducimus possimus quisquam amet similique\nsuscipit porro ipsam amet\neos veritatis officiis exercitationem vel fugit aut necessitatibus totam\nomnis rerum consequatur expedita quidem cumque explicabo" 121 | }, 122 | { 123 | "userId": 3, 124 | "id": 21, 125 | "title": "asperiores ea ipsam voluptatibus modi minima quia sint", 126 | "body": "repellat aliquid praesentium dolorem quo\nsed totam minus non itaque\nnihil labore molestiae sunt dolor eveniet hic recusandae veniam\ntempora et tenetur expedita sunt" 127 | }, 128 | { 129 | "userId": 3, 130 | "id": 22, 131 | "title": "dolor sint quo a velit explicabo quia nam", 132 | "body": "eos qui et ipsum ipsam suscipit aut\nsed omnis non odio\nexpedita earum mollitia molestiae aut atque rem suscipit\nnam impedit esse" 133 | }, 134 | { 135 | "userId": 3, 136 | "id": 23, 137 | "title": "maxime id vitae nihil numquam", 138 | "body": "veritatis unde neque eligendi\nquae quod architecto quo neque vitae\nest illo sit tempora doloremque fugit quod\net et vel beatae sequi ullam sed tenetur perspiciatis" 139 | }, 140 | { 141 | "userId": 3, 142 | "id": 24, 143 | "title": "autem hic labore sunt dolores incidunt", 144 | "body": "enim et ex nulla\nomnis voluptas quia qui\nvoluptatem consequatur numquam aliquam sunt\ntotam recusandae id dignissimos aut sed asperiores deserunt" 145 | }, 146 | { 147 | "userId": 3, 148 | "id": 25, 149 | "title": "rem alias distinctio quo quis", 150 | "body": "ullam consequatur ut\nomnis quis sit vel consequuntur\nipsa eligendi ipsum molestiae et omnis error nostrum\nmolestiae illo tempore quia et distinctio" 151 | }, 152 | { 153 | "userId": 3, 154 | "id": 26, 155 | "title": "est et quae odit qui non", 156 | "body": "similique esse doloribus nihil accusamus\nomnis dolorem fuga consequuntur reprehenderit fugit recusandae temporibus\nperspiciatis cum ut laudantium\nomnis aut molestiae vel vero" 157 | }, 158 | { 159 | "userId": 3, 160 | "id": 27, 161 | "title": "quasi id et eos tenetur aut quo autem", 162 | "body": "eum sed dolores ipsam sint possimus debitis occaecati\ndebitis qui qui et\nut placeat enim earum aut odit facilis\nconsequatur suscipit necessitatibus rerum sed inventore temporibus consequatur" 163 | }, 164 | { 165 | "userId": 3, 166 | "id": 28, 167 | "title": "delectus ullam et corporis nulla voluptas sequi", 168 | "body": "non et quaerat ex quae ad maiores\nmaiores recusandae totam aut blanditiis mollitia quas illo\nut voluptatibus voluptatem\nsimilique nostrum eum" 169 | }, 170 | { 171 | "userId": 3, 172 | "id": 29, 173 | "title": "iusto eius quod necessitatibus culpa ea", 174 | "body": "odit magnam ut saepe sed non qui\ntempora atque nihil\naccusamus illum doloribus illo dolor\neligendi repudiandae odit magni similique sed cum maiores" 175 | }, 176 | { 177 | "userId": 3, 178 | "id": 30, 179 | "title": "a quo magni similique perferendis", 180 | "body": "alias dolor cumque\nimpedit blanditiis non eveniet odio maxime\nblanditiis amet eius quis tempora quia autem rem\na provident perspiciatis quia" 181 | }, 182 | { 183 | "userId": 4, 184 | "id": 31, 185 | "title": "ullam ut quidem id aut vel consequuntur", 186 | "body": "debitis eius sed quibusdam non quis consectetur vitae\nimpedit ut qui consequatur sed aut in\nquidem sit nostrum et maiores adipisci atque\nquaerat voluptatem adipisci repudiandae" 187 | }, 188 | { 189 | "userId": 4, 190 | "id": 32, 191 | "title": "doloremque illum aliquid sunt", 192 | "body": "deserunt eos nobis asperiores et hic\nest debitis repellat molestiae optio\nnihil ratione ut eos beatae quibusdam distinctio maiores\nearum voluptates et aut adipisci ea maiores voluptas maxime" 193 | }, 194 | { 195 | "userId": 4, 196 | "id": 33, 197 | "title": "qui explicabo molestiae dolorem", 198 | "body": "rerum ut et numquam laborum odit est sit\nid qui sint in\nquasi tenetur tempore aperiam et quaerat qui in\nrerum officiis sequi cumque quod" 199 | }, 200 | { 201 | "userId": 4, 202 | "id": 34, 203 | "title": "magnam ut rerum iure", 204 | "body": "ea velit perferendis earum ut voluptatem voluptate itaque iusto\ntotam pariatur in\nnemo voluptatem voluptatem autem magni tempora minima in\nest distinctio qui assumenda accusamus dignissimos officia nesciunt nobis" 205 | }, 206 | { 207 | "userId": 4, 208 | "id": 35, 209 | "title": "id nihil consequatur molestias animi provident", 210 | "body": "nisi error delectus possimus ut eligendi vitae\nplaceat eos harum cupiditate facilis reprehenderit voluptatem beatae\nmodi ducimus quo illum voluptas eligendi\net nobis quia fugit" 211 | }, 212 | { 213 | "userId": 4, 214 | "id": 36, 215 | "title": "fuga nam accusamus voluptas reiciendis itaque", 216 | "body": "ad mollitia et omnis minus architecto odit\nvoluptas doloremque maxime aut non ipsa qui alias veniam\nblanditiis culpa aut quia nihil cumque facere et occaecati\nqui aspernatur quia eaque ut aperiam inventore" 217 | }, 218 | { 219 | "userId": 4, 220 | "id": 37, 221 | "title": "provident vel ut sit ratione est", 222 | "body": "debitis et eaque non officia sed nesciunt pariatur vel\nvoluptatem iste vero et ea\nnumquam aut expedita ipsum nulla in\nvoluptates omnis consequatur aut enim officiis in quam qui" 223 | }, 224 | { 225 | "userId": 4, 226 | "id": 38, 227 | "title": "explicabo et eos deleniti nostrum ab id repellendus", 228 | "body": "animi esse sit aut sit nesciunt assumenda eum voluptas\nquia voluptatibus provident quia necessitatibus ea\nrerum repudiandae quia voluptatem delectus fugit aut id quia\nratione optio eos iusto veniam iure" 229 | }, 230 | { 231 | "userId": 4, 232 | "id": 39, 233 | "title": "eos dolorem iste accusantium est eaque quam", 234 | "body": "corporis rerum ducimus vel eum accusantium\nmaxime aspernatur a porro possimus iste omnis\nest in deleniti asperiores fuga aut\nvoluptas sapiente vel dolore minus voluptatem incidunt ex" 235 | }, 236 | { 237 | "userId": 4, 238 | "id": 40, 239 | "title": "enim quo cumque", 240 | "body": "ut voluptatum aliquid illo tenetur nemo sequi quo facilis\nipsum rem optio mollitia quas\nvoluptatem eum voluptas qui\nunde omnis voluptatem iure quasi maxime voluptas nam" 241 | }, 242 | { 243 | "userId": 5, 244 | "id": 41, 245 | "title": "non est facere", 246 | "body": "molestias id nostrum\nexcepturi molestiae dolore omnis repellendus quaerat saepe\nconsectetur iste quaerat tenetur asperiores accusamus ex ut\nnam quidem est ducimus sunt debitis saepe" 247 | }, 248 | { 249 | "userId": 5, 250 | "id": 42, 251 | "title": "commodi ullam sint et excepturi error explicabo praesentium voluptas", 252 | "body": "odio fugit voluptatum ducimus earum autem est incidunt voluptatem\nodit reiciendis aliquam sunt sequi nulla dolorem\nnon facere repellendus voluptates quia\nratione harum vitae ut" 253 | }, 254 | { 255 | "userId": 5, 256 | "id": 43, 257 | "title": "eligendi iste nostrum consequuntur adipisci praesentium sit beatae perferendis", 258 | "body": "similique fugit est\nillum et dolorum harum et voluptate eaque quidem\nexercitationem quos nam commodi possimus cum odio nihil nulla\ndolorum exercitationem magnam ex et a et distinctio debitis" 259 | }, 260 | { 261 | "userId": 5, 262 | "id": 44, 263 | "title": "optio dolor molestias sit", 264 | "body": "temporibus est consectetur dolore\net libero debitis vel velit laboriosam quia\nipsum quibusdam qui itaque fuga rem aut\nea et iure quam sed maxime ut distinctio quae" 265 | }, 266 | { 267 | "userId": 5, 268 | "id": 45, 269 | "title": "ut numquam possimus omnis eius suscipit laudantium iure", 270 | "body": "est natus reiciendis nihil possimus aut provident\nex et dolor\nrepellat pariatur est\nnobis rerum repellendus dolorem autem" 271 | }, 272 | { 273 | "userId": 5, 274 | "id": 46, 275 | "title": "aut quo modi neque nostrum ducimus", 276 | "body": "voluptatem quisquam iste\nvoluptatibus natus officiis facilis dolorem\nquis quas ipsam\nvel et voluptatum in aliquid" 277 | }, 278 | { 279 | "userId": 5, 280 | "id": 47, 281 | "title": "quibusdam cumque rem aut deserunt", 282 | "body": "voluptatem assumenda ut qui ut cupiditate aut impedit veniam\noccaecati nemo illum voluptatem laudantium\nmolestiae beatae rerum ea iure soluta nostrum\neligendi et voluptate" 283 | }, 284 | { 285 | "userId": 5, 286 | "id": 48, 287 | "title": "ut voluptatem illum ea doloribus itaque eos", 288 | "body": "voluptates quo voluptatem facilis iure occaecati\nvel assumenda rerum officia et\nillum perspiciatis ab deleniti\nlaudantium repellat ad ut et autem reprehenderit" 289 | }, 290 | { 291 | "userId": 5, 292 | "id": 49, 293 | "title": "laborum non sunt aut ut assumenda perspiciatis voluptas", 294 | "body": "inventore ab sint\nnatus fugit id nulla sequi architecto nihil quaerat\neos tenetur in in eum veritatis non\nquibusdam officiis aspernatur cumque aut commodi aut" 295 | }, 296 | { 297 | "userId": 5, 298 | "id": 50, 299 | "title": "repellendus qui recusandae incidunt voluptates tenetur qui omnis exercitationem", 300 | "body": "error suscipit maxime adipisci consequuntur recusandae\nvoluptas eligendi et est et voluptates\nquia distinctio ab amet quaerat molestiae et vitae\nadipisci impedit sequi nesciunt quis consectetur" 301 | }, 302 | { 303 | "userId": 6, 304 | "id": 51, 305 | "title": "soluta aliquam aperiam consequatur illo quis voluptas", 306 | "body": "sunt dolores aut doloribus\ndolore doloribus voluptates tempora et\ndoloremque et quo\ncum asperiores sit consectetur dolorem" 307 | }, 308 | { 309 | "userId": 6, 310 | "id": 52, 311 | "title": "qui enim et consequuntur quia animi quis voluptate quibusdam", 312 | "body": "iusto est quibusdam fuga quas quaerat molestias\na enim ut sit accusamus enim\ntemporibus iusto accusantium provident architecto\nsoluta esse reprehenderit qui laborum" 313 | }, 314 | { 315 | "userId": 6, 316 | "id": 53, 317 | "title": "ut quo aut ducimus alias", 318 | "body": "minima harum praesentium eum rerum illo dolore\nquasi exercitationem rerum nam\nporro quis neque quo\nconsequatur minus dolor quidem veritatis sunt non explicabo similique" 319 | }, 320 | { 321 | "userId": 6, 322 | "id": 54, 323 | "title": "sit asperiores ipsam eveniet odio non quia", 324 | "body": "totam corporis dignissimos\nvitae dolorem ut occaecati accusamus\nex velit deserunt\net exercitationem vero incidunt corrupti mollitia" 325 | }, 326 | { 327 | "userId": 6, 328 | "id": 55, 329 | "title": "sit vel voluptatem et non libero", 330 | "body": "debitis excepturi ea perferendis harum libero optio\neos accusamus cum fuga ut sapiente repudiandae\net ut incidunt omnis molestiae\nnihil ut eum odit" 331 | }, 332 | { 333 | "userId": 6, 334 | "id": 56, 335 | "title": "qui et at rerum necessitatibus", 336 | "body": "aut est omnis dolores\nneque rerum quod ea rerum velit pariatur beatae excepturi\net provident voluptas corrupti\ncorporis harum reprehenderit dolores eligendi" 337 | }, 338 | { 339 | "userId": 6, 340 | "id": 57, 341 | "title": "sed ab est est", 342 | "body": "at pariatur consequuntur earum quidem\nquo est laudantium soluta voluptatem\nqui ullam et est\net cum voluptas voluptatum repellat est" 343 | }, 344 | { 345 | "userId": 6, 346 | "id": 58, 347 | "title": "voluptatum itaque dolores nisi et quasi", 348 | "body": "veniam voluptatum quae adipisci id\net id quia eos ad et dolorem\naliquam quo nisi sunt eos impedit error\nad similique veniam" 349 | }, 350 | { 351 | "userId": 6, 352 | "id": 59, 353 | "title": "qui commodi dolor at maiores et quis id accusantium", 354 | "body": "perspiciatis et quam ea autem temporibus non voluptatibus qui\nbeatae a earum officia nesciunt dolores suscipit voluptas et\nanimi doloribus cum rerum quas et magni\net hic ut ut commodi expedita sunt" 355 | }, 356 | { 357 | "userId": 6, 358 | "id": 60, 359 | "title": "consequatur placeat omnis quisquam quia reprehenderit fugit veritatis facere", 360 | "body": "asperiores sunt ab assumenda cumque modi velit\nqui esse omnis\nvoluptate et fuga perferendis voluptas\nillo ratione amet aut et omnis" 361 | }, 362 | { 363 | "userId": 7, 364 | "id": 61, 365 | "title": "voluptatem doloribus consectetur est ut ducimus", 366 | "body": "ab nemo optio odio\ndelectus tenetur corporis similique nobis repellendus rerum omnis facilis\nvero blanditiis debitis in nesciunt doloribus dicta dolores\nmagnam minus velit" 367 | }, 368 | { 369 | "userId": 7, 370 | "id": 62, 371 | "title": "beatae enim quia vel", 372 | "body": "enim aspernatur illo distinctio quae praesentium\nbeatae alias amet delectus qui voluptate distinctio\nodit sint accusantium autem omnis\nquo molestiae omnis ea eveniet optio" 373 | }, 374 | { 375 | "userId": 7, 376 | "id": 63, 377 | "title": "voluptas blanditiis repellendus animi ducimus error sapiente et suscipit", 378 | "body": "enim adipisci aspernatur nemo\nnumquam omnis facere dolorem dolor ex quis temporibus incidunt\nab delectus culpa quo reprehenderit blanditiis asperiores\naccusantium ut quam in voluptatibus voluptas ipsam dicta" 379 | }, 380 | { 381 | "userId": 7, 382 | "id": 64, 383 | "title": "et fugit quas eum in in aperiam quod", 384 | "body": "id velit blanditiis\neum ea voluptatem\nmolestiae sint occaecati est eos perspiciatis\nincidunt a error provident eaque aut aut qui" 385 | }, 386 | { 387 | "userId": 7, 388 | "id": 65, 389 | "title": "consequatur id enim sunt et et", 390 | "body": "voluptatibus ex esse\nsint explicabo est aliquid cumque adipisci fuga repellat labore\nmolestiae corrupti ex saepe at asperiores et perferendis\nnatus id esse incidunt pariatur" 391 | }, 392 | { 393 | "userId": 7, 394 | "id": 66, 395 | "title": "repudiandae ea animi iusto", 396 | "body": "officia veritatis tenetur vero qui itaque\nsint non ratione\nsed et ut asperiores iusto eos molestiae nostrum\nveritatis quibusdam et nemo iusto saepe" 397 | }, 398 | { 399 | "userId": 7, 400 | "id": 67, 401 | "title": "aliquid eos sed fuga est maxime repellendus", 402 | "body": "reprehenderit id nostrum\nvoluptas doloremque pariatur sint et accusantium quia quod aspernatur\net fugiat amet\nnon sapiente et consequatur necessitatibus molestiae" 403 | }, 404 | { 405 | "userId": 7, 406 | "id": 68, 407 | "title": "odio quis facere architecto reiciendis optio", 408 | "body": "magnam molestiae perferendis quisquam\nqui cum reiciendis\nquaerat animi amet hic inventore\nea quia deleniti quidem saepe porro velit" 409 | }, 410 | { 411 | "userId": 7, 412 | "id": 69, 413 | "title": "fugiat quod pariatur odit minima", 414 | "body": "officiis error culpa consequatur modi asperiores et\ndolorum assumenda voluptas et vel qui aut vel rerum\nvoluptatum quisquam perspiciatis quia rerum consequatur totam quas\nsequi commodi repudiandae asperiores et saepe a" 415 | }, 416 | { 417 | "userId": 7, 418 | "id": 70, 419 | "title": "voluptatem laborum magni", 420 | "body": "sunt repellendus quae\nest asperiores aut deleniti esse accusamus repellendus quia aut\nquia dolorem unde\neum tempora esse dolore" 421 | }, 422 | { 423 | "userId": 8, 424 | "id": 71, 425 | "title": "et iusto veniam et illum aut fuga", 426 | "body": "occaecati a doloribus\niste saepe consectetur placeat eum voluptate dolorem et\nqui quo quia voluptas\nrerum ut id enim velit est perferendis" 427 | }, 428 | { 429 | "userId": 8, 430 | "id": 72, 431 | "title": "sint hic doloribus consequatur eos non id", 432 | "body": "quam occaecati qui deleniti consectetur\nconsequatur aut facere quas exercitationem aliquam hic voluptas\nneque id sunt ut aut accusamus\nsunt consectetur expedita inventore velit" 433 | }, 434 | { 435 | "userId": 8, 436 | "id": 73, 437 | "title": "consequuntur deleniti eos quia temporibus ab aliquid at", 438 | "body": "voluptatem cumque tenetur consequatur expedita ipsum nemo quia explicabo\naut eum minima consequatur\ntempore cumque quae est et\net in consequuntur voluptatem voluptates aut" 439 | }, 440 | { 441 | "userId": 8, 442 | "id": 74, 443 | "title": "enim unde ratione doloribus quas enim ut sit sapiente", 444 | "body": "odit qui et et necessitatibus sint veniam\nmollitia amet doloremque molestiae commodi similique magnam et quam\nblanditiis est itaque\nquo et tenetur ratione occaecati molestiae tempora" 445 | }, 446 | { 447 | "userId": 8, 448 | "id": 75, 449 | "title": "dignissimos eum dolor ut enim et delectus in", 450 | "body": "commodi non non omnis et voluptas sit\nautem aut nobis magnam et sapiente voluptatem\net laborum repellat qui delectus facilis temporibus\nrerum amet et nemo voluptate expedita adipisci error dolorem" 451 | }, 452 | { 453 | "userId": 8, 454 | "id": 76, 455 | "title": "doloremque officiis ad et non perferendis", 456 | "body": "ut animi facere\ntotam iusto tempore\nmolestiae eum aut et dolorem aperiam\nquaerat recusandae totam odio" 457 | }, 458 | { 459 | "userId": 8, 460 | "id": 77, 461 | "title": "necessitatibus quasi exercitationem odio", 462 | "body": "modi ut in nulla repudiandae dolorum nostrum eos\naut consequatur omnis\nut incidunt est omnis iste et quam\nvoluptates sapiente aliquam asperiores nobis amet corrupti repudiandae provident" 463 | }, 464 | { 465 | "userId": 8, 466 | "id": 78, 467 | "title": "quam voluptatibus rerum veritatis", 468 | "body": "nobis facilis odit tempore cupiditate quia\nassumenda doloribus rerum qui ea\nillum et qui totam\naut veniam repellendus" 469 | }, 470 | { 471 | "userId": 8, 472 | "id": 79, 473 | "title": "pariatur consequatur quia magnam autem omnis non amet", 474 | "body": "libero accusantium et et facere incidunt sit dolorem\nnon excepturi qui quia sed laudantium\nquisquam molestiae ducimus est\nofficiis esse molestiae iste et quos" 475 | }, 476 | { 477 | "userId": 8, 478 | "id": 80, 479 | "title": "labore in ex et explicabo corporis aut quas", 480 | "body": "ex quod dolorem ea eum iure qui provident amet\nquia qui facere excepturi et repudiandae\nasperiores molestias provident\nminus incidunt vero fugit rerum sint sunt excepturi provident" 481 | }, 482 | { 483 | "userId": 9, 484 | "id": 81, 485 | "title": "tempora rem veritatis voluptas quo dolores vero", 486 | "body": "facere qui nesciunt est voluptatum voluptatem nisi\nsequi eligendi necessitatibus ea at rerum itaque\nharum non ratione velit laboriosam quis consequuntur\nex officiis minima doloremque voluptas ut aut" 487 | }, 488 | { 489 | "userId": 9, 490 | "id": 82, 491 | "title": "laudantium voluptate suscipit sunt enim enim", 492 | "body": "ut libero sit aut totam inventore sunt\nporro sint qui sunt molestiae\nconsequatur cupiditate qui iste ducimus adipisci\ndolor enim assumenda soluta laboriosam amet iste delectus hic" 493 | }, 494 | { 495 | "userId": 9, 496 | "id": 83, 497 | "title": "odit et voluptates doloribus alias odio et", 498 | "body": "est molestiae facilis quis tempora numquam nihil qui\nvoluptate sapiente consequatur est qui\nnecessitatibus autem aut ipsa aperiam modi dolore numquam\nreprehenderit eius rem quibusdam" 499 | }, 500 | { 501 | "userId": 9, 502 | "id": 84, 503 | "title": "optio ipsam molestias necessitatibus occaecati facilis veritatis dolores aut", 504 | "body": "sint molestiae magni a et quos\neaque et quasi\nut rerum debitis similique veniam\nrecusandae dignissimos dolor incidunt consequatur odio" 505 | }, 506 | { 507 | "userId": 9, 508 | "id": 85, 509 | "title": "dolore veritatis porro provident adipisci blanditiis et sunt", 510 | "body": "similique sed nisi voluptas iusto omnis\nmollitia et quo\nassumenda suscipit officia magnam sint sed tempora\nenim provident pariatur praesentium atque animi amet ratione" 511 | }, 512 | { 513 | "userId": 9, 514 | "id": 86, 515 | "title": "placeat quia et porro iste", 516 | "body": "quasi excepturi consequatur iste autem temporibus sed molestiae beatae\net quaerat et esse ut\nvoluptatem occaecati et vel explicabo autem\nasperiores pariatur deserunt optio" 517 | }, 518 | { 519 | "userId": 9, 520 | "id": 87, 521 | "title": "nostrum quis quasi placeat", 522 | "body": "eos et molestiae\nnesciunt ut a\ndolores perspiciatis repellendus repellat aliquid\nmagnam sint rem ipsum est" 523 | }, 524 | { 525 | "userId": 9, 526 | "id": 88, 527 | "title": "sapiente omnis fugit eos", 528 | "body": "consequatur omnis est praesentium\nducimus non iste\nneque hic deserunt\nvoluptatibus veniam cum et rerum sed" 529 | }, 530 | { 531 | "userId": 9, 532 | "id": 89, 533 | "title": "sint soluta et vel magnam aut ut sed qui", 534 | "body": "repellat aut aperiam totam temporibus autem et\narchitecto magnam ut\nconsequatur qui cupiditate rerum quia soluta dignissimos nihil iure\ntempore quas est" 535 | }, 536 | { 537 | "userId": 9, 538 | "id": 90, 539 | "title": "ad iusto omnis odit dolor voluptatibus", 540 | "body": "minus omnis soluta quia\nqui sed adipisci voluptates illum ipsam voluptatem\neligendi officia ut in\neos soluta similique molestias praesentium blanditiis" 541 | }, 542 | { 543 | "userId": 10, 544 | "id": 91, 545 | "title": "aut amet sed", 546 | "body": "libero voluptate eveniet aperiam sed\nsunt placeat suscipit molestias\nsimilique fugit nam natus\nexpedita consequatur consequatur dolores quia eos et placeat" 547 | }, 548 | { 549 | "userId": 10, 550 | "id": 92, 551 | "title": "ratione ex tenetur perferendis", 552 | "body": "aut et excepturi dicta laudantium sint rerum nihil\nlaudantium et at\na neque minima officia et similique libero et\ncommodi voluptate qui" 553 | }, 554 | { 555 | "userId": 10, 556 | "id": 93, 557 | "title": "beatae soluta recusandae", 558 | "body": "dolorem quibusdam ducimus consequuntur dicta aut quo laboriosam\nvoluptatem quis enim recusandae ut sed sunt\nnostrum est odit totam\nsit error sed sunt eveniet provident qui nulla" 559 | }, 560 | { 561 | "userId": 10, 562 | "id": 94, 563 | "title": "qui qui voluptates illo iste minima", 564 | "body": "aspernatur expedita soluta quo ab ut similique\nexpedita dolores amet\nsed temporibus distinctio magnam saepe deleniti\nomnis facilis nam ipsum natus sint similique omnis" 565 | }, 566 | { 567 | "userId": 10, 568 | "id": 95, 569 | "title": "id minus libero illum nam ad officiis", 570 | "body": "earum voluptatem facere provident blanditiis velit laboriosam\npariatur accusamus odio saepe\ncumque dolor qui a dicta ab doloribus consequatur omnis\ncorporis cupiditate eaque assumenda ad nesciunt" 571 | }, 572 | { 573 | "userId": 10, 574 | "id": 96, 575 | "title": "quaerat velit veniam amet cupiditate aut numquam ut sequi", 576 | "body": "in non odio excepturi sint eum\nlabore voluptates vitae quia qui et\ninventore itaque rerum\nveniam non exercitationem delectus aut" 577 | }, 578 | { 579 | "userId": 10, 580 | "id": 97, 581 | "title": "quas fugiat ut perspiciatis vero provident", 582 | "body": "eum non blanditiis soluta porro quibusdam voluptas\nvel voluptatem qui placeat dolores qui velit aut\nvel inventore aut cumque culpa explicabo aliquid at\nperspiciatis est et voluptatem dignissimos dolor itaque sit nam" 583 | }, 584 | { 585 | "userId": 10, 586 | "id": 98, 587 | "title": "laboriosam dolor voluptates", 588 | "body": "doloremque ex facilis sit sint culpa\nsoluta assumenda eligendi non ut eius\nsequi ducimus vel quasi\nveritatis est dolores" 589 | }, 590 | { 591 | "userId": 10, 592 | "id": 99, 593 | "title": "temporibus sit alias delectus eligendi possimus magni", 594 | "body": "quo deleniti praesentium dicta non quod\naut est molestias\nmolestias et officia quis nihil\nitaque dolorem quia" 595 | }, 596 | { 597 | "userId": 10, 598 | "id": 100, 599 | "title": "at nam consequatur ea labore ea harum", 600 | "body": "cupiditate quo est a modi nesciunt soluta\nipsa voluptas error itaque dicta in\nautem qui minus magnam et distinctio eum\naccusamus ratione error aut" 601 | } 602 | ] 603 | -------------------------------------------------------------------------------- /SwiftMVVMDemoUITests/SwiftMVVMDemoUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftMVVMDemoUITests.swift 3 | // SwiftMVVMDemoUITests 4 | // 5 | // Created by Rushi Sangani on 05/12/2023. 6 | // 7 | 8 | import XCTest 9 | 10 | final class SwiftMVVMDemoUITests: 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 | -------------------------------------------------------------------------------- /SwiftMVVMDemoUITests/SwiftMVVMDemoUITestsLaunchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftMVVMDemoUITestsLaunchTests.swift 3 | // SwiftMVVMDemoUITests 4 | // 5 | // Created by Rushi Sangani on 05/12/2023. 6 | // 7 | 8 | import XCTest 9 | 10 | final class SwiftMVVMDemoUITestsLaunchTests: 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 | --------------------------------------------------------------------------------